From a01c63d9720834ddfc418cad89179fa3c4fda899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Fri, 13 Feb 2026 12:39:51 +0900 Subject: [PATCH 1/5] =?UTF-8?q?PostEmbed=20=E3=81=AE=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7=E5=BC=B7=E5=8C=96=EF=BC=88#130=EF=BC=89=20(#263)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #130 #130 #130 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/263 --- frontend/src/components/PostEmbed.tsx | 28 ++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/PostEmbed.tsx b/frontend/src/components/PostEmbed.tsx index c375ca1..e229666 100644 --- a/frontend/src/components/PostEmbed.tsx +++ b/frontend/src/components/PostEmbed.tsx @@ -18,17 +18,35 @@ export default (({ post }: Props) => { { case 'nicovideo.jp': { - const [videoId] = url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)! + const mVideoId = url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/) + if (!(mVideoId)) + break + + const [videoId] = mVideoId + return } + case 'twitter.com': case 'x.com': - const [userId] = url.pathname.match (/(?<=\/)[^\/]+?(?=\/|$|\?)/)! - const [statusId] = url.pathname.match (/(?<=\/status\/)\d+?(?=\/|$|\?)/)! - return + { + const mUserId = url.pathname.match (/(?<=\/)[^\/]+?(?=\/|$|\?)/) + const mStatusId = url.pathname.match (/(?<=\/status\/)\d+?(?=\/|$|\?)/) + if (!(mUserId) || !(mStatusId)) + break + + const [userId] = mUserId + const [statusId] = mStatusId + + return + } + case 'youtube.com': { - const videoId = url.searchParams.get ('v')! + const videoId = url.searchParams.get ('v') + if (!(videoId)) + break + return ( Date: Sun, 22 Feb 2026 01:50:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Representations=20=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=EF=BC=88#241=EF=BC=89=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #241 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/267 --- .../app/controllers/nico_tags_controller.rb | 8 +++----- backend/app/controllers/posts_controller.rb | 20 ++++++------------- backend/app/controllers/tags_controller.rb | 11 +++++----- .../app/controllers/wiki_pages_controller.rb | 11 ++++------ backend/app/representations/WikiPageRepr.rb | 16 +++++++++++++++ backend/app/representations/post_repr.rb | 16 +++++++++++++++ backend/app/representations/tag_repr.rb | 16 +++++++++++++++ 7 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 backend/app/representations/WikiPageRepr.rb create mode 100644 backend/app/representations/post_repr.rb create mode 100644 backend/app/representations/tag_repr.rb diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index 2b28175..9058fa7 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -1,6 +1,4 @@ class NicoTagsController < ApplicationController - TAG_JSON = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze - def index limit = (params[:limit] || 20).to_i cursor = params[:cursor].presence @@ -19,8 +17,8 @@ class NicoTagsController < ApplicationController end render json: { tags: tags.map { |tag| - tag.as_json(TAG_JSON).merge(linked_tags: tag.linked_tags.map { |lt| - lt.as_json(TAG_JSON) + TagRepr.base(tag).merge(linked_tags: tag.linked_tags.map { |lt| + TagRepr.base(lt) }) }, next_cursor: } end @@ -41,6 +39,6 @@ class NicoTagsController < ApplicationController tag.linked_tags = linked_tags tag.save! - render json: tag.linked_tags.map { |t| t.as_json(TAG_JSON) }, status: :ok + render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok end end diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index eb4dee6..33fef9a 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -36,8 +36,7 @@ class PostsController < ApplicationController end render json: { posts: posts.map { |post| - post.as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name, :has_wiki] } }).tap do |json| + PostRepr.base(post).tap do |json| json['thumbnail'] = if post.thumbnail.attached? rails_storage_proxy_url(post.thumbnail, only_path: false) @@ -60,10 +59,7 @@ class PostsController < ApplicationController viewed = current_user&.viewed?(post) || false - render json: (post - .as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name, :has_wiki] } }) - .merge(viewed:)) + render json: PostRepr.base(post).merge(viewed:) end def show @@ -102,9 +98,7 @@ class PostsController < ApplicationController sync_post_tags!(post, tags) post.reload - render json: post.as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name, :has_wiki] } }), - status: :created + render json: PostRepr.base(post), status: :created else render json: { errors: post.errors.full_messages }, status: :unprocessable_entity end @@ -170,7 +164,7 @@ class PostsController < ApplicationController events = [] pts.each do |pt| - tag = pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]) + tag = TagRepr.base(pt.tag) post = pt.post events << Event.new( @@ -269,8 +263,7 @@ class PostsController < ApplicationController return nil unless tag if path.include?(tag_id) - return tag.as_json(only: [:id, :category, :post_count], - methods: [:name, :has_wiki]).merge(children: []) + return TagRepr.base(tag).merge(children: []) end if memo.key?(tag_id) @@ -282,8 +275,7 @@ class PostsController < ApplicationController children = child_ids.filter_map { |cid| build_node.(cid, new_path) } - memo[tag_id] = tag.as_json(only: [:id, :category, :post_count], - methods: [:name, :has_wiki]).merge(children:) + memo[tag_id] = TagRepr.base(tag).merge(children:) end root_ids.filter_map { |id| build_node.call(id, []) } diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 5251e72..c4d5b0d 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -13,7 +13,7 @@ class TagsController < ApplicationController tags = tags.where(posts: { id: post_id }) end - render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) + render json: TagRepr.base(tags) end def autocomplete @@ -57,8 +57,7 @@ class TagsController < ApplicationController tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a render json: tags.map { |tag| - tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) - .merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id]) + TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id]) } end @@ -67,7 +66,7 @@ class TagsController < ApplicationController .includes(:tag_name, tag_name: :wiki_page) .find_by(id: params[:id]) if tag - render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) + render json: TagRepr.base(tag) else head :not_found end @@ -81,7 +80,7 @@ class TagsController < ApplicationController .includes(:tag_name, tag_name: :wiki_page) .find_by(tag_names: { name: }) if tag - render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) + render json: TagRepr.base(tag) else head :not_found end @@ -104,6 +103,6 @@ class TagsController < ApplicationController tag.update!(category:) end - render json: tag.as_json(methods: [:name]) + render json: TagRepr.base(tag) end end diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index 7097422..53c1b05 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -4,14 +4,12 @@ class WikiPagesController < ApplicationController def index title = params[:title].to_s.strip if title.blank? - return render json: WikiPage.joins(:tag_name) - .includes(:tag_name) - .as_json(methods: [:title]) + return render json: WikiPageRepr.base(WikiPage.joins(:tag_name).includes(:tag_name)) end q = WikiPage.joins(:tag_name).includes(:tag_name) .where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") - render json: q.limit(20).as_json(methods: [:title]) + render json: WikiPageRepr.base(q.limit(20)) end def show @@ -98,7 +96,7 @@ class WikiPagesController < ApplicationController message = params[:message].presence Wiki::Commit.content!(page:, body:, created_user: current_user, message:) - render json: page.as_json(methods: [:title]), status: :created + render json: WikiPageRepr.base(page), status: :created else render json: { errors: page.errors.full_messages }, status: :unprocessable_entity @@ -174,8 +172,7 @@ class WikiPagesController < ApplicationController succ = page.succ_revision_id(revision_id) updated_at = rev.created_at - render json: page.as_json(methods: [:title]) - .merge(body:, revision_id:, pred:, succ:, updated_at:) + render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:) end def find_revision page diff --git a/backend/app/representations/WikiPageRepr.rb b/backend/app/representations/WikiPageRepr.rb new file mode 100644 index 0000000..3fb712c --- /dev/null +++ b/backend/app/representations/WikiPageRepr.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + + +module WikiPageRepr + BASE = { methods: [:title] }.freeze + + module_function + + def base wiki_page + wiki_page.as_json(BASE) + end + + def many wiki_pages + wiki_pages.map { |p| base(p) } + end +end diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb new file mode 100644 index 0000000..438ccc1 --- /dev/null +++ b/backend/app/representations/post_repr.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + + +module PostRepr + BASE = { include: { tags: TagRepr::BASE } }.freeze + + module_function + + def base post + post.as_json(BASE) + end + + def many posts + posts.map { |p| base(p) } + end +end diff --git a/backend/app/representations/tag_repr.rb b/backend/app/representations/tag_repr.rb new file mode 100644 index 0000000..cb2c470 --- /dev/null +++ b/backend/app/representations/tag_repr.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + + +module TagRepr + BASE = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze + + module_function + + def base tag + tag.as_json(BASE) + end + + def many tags + tags.map { |t| base(t) } + end +end From be983e4ad19235926569a5c80be8db5b7cfdea24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 22 Feb 2026 02:15:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E3=82=AA=E3=83=AA=E3=82=B8=E3=83=8A?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E6=8A=95=E7=A8=BF=E6=97=A5=E6=99=82=20Safari?= =?UTF-8?q?=20=E3=81=A7=E3=81=AE=E3=83=90=E3=82=B0=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=EF=BC=88#129=EF=BC=89=20(#265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge branch 'main' into feature/129 #129 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/265 --- .../PostOriginalCreatedTimeField.tsx | 75 ++++++++++++------- .../src/components/common/DateTimeField.tsx | 6 +- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/PostOriginalCreatedTimeField.tsx b/frontend/src/components/PostOriginalCreatedTimeField.tsx index ffabc6b..3709ae1 100644 --- a/frontend/src/components/PostOriginalCreatedTimeField.tsx +++ b/frontend/src/components/PostOriginalCreatedTimeField.tsx @@ -1,5 +1,6 @@ import DateTimeField from '@/components/common/DateTimeField' import Label from '@/components/common/Label' +import { Button } from '@/components/ui/button' import type { FC } from 'react' @@ -16,34 +17,52 @@ export default (({ originalCreatedFrom, setOriginalCreatedBefore }: Props) => (
-
- { - const v = ev.target.value - if (!(v)) - return - const d = new Date (v) - if (d.getSeconds () === 0) - { - if (d.getMinutes () === 0 && d.getHours () === 0) - d.setDate (d.getDate () + 1) - else - d.setMinutes (d.getMinutes () + 1) - } - else - d.setSeconds (d.getSeconds () + 1) - setOriginalCreatedBefore (d.toISOString ()) - }}/> - 以降 +
+
+ { + const v = ev.target.value + if (!(v)) + return + + const d = new Date (v) + if (d.getMinutes () === 0 && d.getHours () === 0) + d.setDate (d.getDate () + 1) + else + d.setMinutes (d.getMinutes () + 1) + setOriginalCreatedBefore (d.toISOString ()) + }}/> + 以降 +
+
+ +
-
- - より前 +
+
+ + より前 +
+
+ +
)) satisfies FC diff --git a/frontend/src/components/common/DateTimeField.tsx b/frontend/src/components/common/DateTimeField.tsx index 5dbaae8..2481045 100644 --- a/frontend/src/components/common/DateTimeField.tsx +++ b/frontend/src/components/common/DateTimeField.tsx @@ -5,7 +5,7 @@ import { cn } from '@/lib/utils' import type { FC, FocusEvent } from 'react' -const pad = (n: number) => n.toString ().padStart (2, '0') +const pad = (n: number): string => n.toString ().padStart (2, '0') const toDateTimeLocalValue = (d: Date) => { @@ -14,8 +14,7 @@ const toDateTimeLocalValue = (d: Date) => { const day = pad (d.getDate ()) const h = pad (d.getHours ()) const min = pad (d.getMinutes ()) - const s = pad (d.getSeconds ()) - return `${ y }-${ m }-${ day }T${ h }:${ min }:${ s }` + return `${ y }-${ m }-${ day }T${ h }:${ min }:00` } @@ -37,7 +36,6 @@ export default (({ value, onChange, className, onBlur }: Props) => { { const v = ev.target.value From a0d6aeb91e4776b6c6cc2712c57403ebcfa4a3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 22 Feb 2026 23:32:01 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E3=82=BF=E3=82=B0=E8=A3=9C=E5=AE=8C?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=B3=E3=83=89=E3=82=A6=EF=BC=88#103?= =?UTF-8?q?=EF=BC=89=20(#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #103 Merge remote-tracking branch 'origin/main' into feature/103 #103 #103 タグ補完からニコタグ除外 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/269 --- frontend/src/components/PostFormTagsArea.tsx | 39 ++++++++++++++------ frontend/src/lib/utils.ts | 9 +++-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/PostFormTagsArea.tsx b/frontend/src/components/PostFormTagsArea.tsx index b5fac1a..c7775af 100644 --- a/frontend/src/components/PostFormTagsArea.tsx +++ b/frontend/src/components/PostFormTagsArea.tsx @@ -25,8 +25,8 @@ const getTokenAt = (value: string, pos: number) => { } -const replaceToken = (value: string, start: number, end: number, text: string) => ( - `${ value.slice (0, start) }${ text }${ value.slice (end) }`) +const replaceToken = (value: string, start: number, end: number, text: string) => + `${ value.slice (0, start) }${ text }${ value.slice (end) }` type Props = { @@ -38,16 +38,17 @@ export default (({ tags, setTags }: Props) => { const ref = useRef (null) const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 }) + const [focused, setFocused] = useState (false) const [suggestions, setSuggestions] = useState ([]) const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) const handleTagSelect = (tag: Tag) => { setSuggestionsVsbl (false) const textarea = ref.current! - const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name) + const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name + ' ') setTags (newValue) requestAnimationFrame (async () => { - const p = bounds.start + tag.name.length + const p = bounds.start + tag.name.length + 1 textarea.selectionStart = textarea.selectionEnd = p textarea.focus () await recompute (p, newValue) @@ -56,14 +57,21 @@ export default (({ tags, setTags }: Props) => { const recompute = async (pos: number, v: string = tags) => { const { start, end, token } = getTokenAt (v, pos) + if (!(token.trim ())) + { + setSuggestionsVsbl (false) + return + } + setBounds ({ start, end }) - const data = await apiGet ('/tags/autocomplete', { params: { q: token } }) + + const data = await apiGet ('/tags/autocomplete', { params: { q: token, nico: '0' } }) setSuggestions (data.filter (t => t.postCount > 0)) setSuggestionsVsbl (suggestions.length > 0) } return ( -
+