From 1a776e348a911a2d9aba4416129b9eff5c273143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Wed, 11 Feb 2026 13:26:10 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=83=AA=E3=83=BB?= =?UTF-8?q?=E3=83=91=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E6=94=B9=E5=96=84=EF=BC=88#258=EF=BC=89=20(#259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #258 #258 #258 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/259 --- .../app/controllers/nico_tags_controller.rb | 4 +- backend/app/controllers/posts_controller.rb | 43 +++++++++++-------- backend/app/controllers/tags_controller.rb | 19 +++++--- .../app/controllers/wiki_pages_controller.rb | 20 ++++++--- backend/app/models/post.rb | 10 +++-- backend/app/models/tag.rb | 4 +- 6 files changed, 64 insertions(+), 36 deletions(-) diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index da3d831..2b28175 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -6,11 +6,11 @@ class NicoTagsController < ApplicationController cursor = params[:cursor].presence q = Tag.nico_tags - .includes(:tag_name, linked_tags: :tag_name) + .includes(:tag_name, tag_name: :wiki_page, linked_tags: { tag_name: :wiki_page }) .order(updated_at: :desc) q = q.where('tags.updated_at < ?', Time.iso8601(cursor)) if cursor - tags = q.limit(limit + 1) + tags = q.limit(limit + 1).to_a next_cursor = nil if tags.size > limit diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 57340eb..eb4dee6 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -17,16 +17,17 @@ class PostsController < ApplicationController 'posts.created_at)' q = filtered_posts - .preload(tags: :tag_name) + .preload(tags: { tag_name: :wiki_page }) .with_attached_thumbnail .select("posts.*, #{ sort_sql } AS sort_ts") .order(Arel.sql("#{ sort_sql } DESC")) - posts = ( + posts = if cursor q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1) else q.limit(limit).offset(offset) - end).to_a + end + .to_a next_cursor = nil if cursor && posts.length > limit @@ -52,7 +53,9 @@ class PostsController < ApplicationController end def random - post = filtered_posts.preload(tags: :tag_name).order('RAND()').first + post = filtered_posts.preload(tags: { tag_name: :wiki_page }) + .order('RAND()') + .first return head :not_found unless post viewed = current_user&.viewed?(post) || false @@ -64,7 +67,7 @@ class PostsController < ApplicationController end def show - post = Post.includes(tags: :tag_name).find(params[:id]) + post = Post.includes(tags: { tag_name: :wiki_page }).find(params[:id]) return head :not_found unless post viewed = current_user&.viewed?(post) || false @@ -151,7 +154,7 @@ class PostsController < ApplicationController end def changes - id = params[:id] + id = params[:id].presence page = (params[:page].presence || 1).to_i limit = (params[:limit].presence || 20).to_i @@ -162,30 +165,34 @@ class PostsController < ApplicationController pts = PostTag.with_discarded pts = pts.where(post_id: id) if id.present? - pts = pts.includes(:post, { tag: :tag_name }, :created_user, :deleted_user) + pts = pts.includes(:post, :created_user, :deleted_user, + tag: { tag_name: :wiki_page }) events = [] pts.each do |pt| + tag = pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]) + post = pt.post + events << Event.new( - post: pt.post, - tag: pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]), - user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name }, - change_type: 'add', - timestamp: pt.created_at) + post:, + tag:, + user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name }, + change_type: 'add', + timestamp: pt.created_at) if pt.discarded_at events << Event.new( - post: pt.post, - tag: pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]), - user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name }, - change_type: 'remove', - timestamp: pt.discarded_at) + post:, + tag:, + user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name }, + change_type: 'remove', + timestamp: pt.discarded_at) end end events.sort_by!(&:timestamp) events.reverse! - render json: { changes: events.slice(offset, limit).as_json, count: events.size } + render json: { changes: (events.slice(offset, limit) || []).as_json, count: events.size } end private diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 1c5dcb2..5251e72 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -4,10 +4,14 @@ class TagsController < ApplicationController tags = if post_id.present? - Tag.joins(:posts).where(posts: { id: post_id }) + Tag.joins(:posts, :tag_name) else - Tag.all + Tag.joins(:tag_name) end + .includes(:tag_name, tag_name: :wiki_page) + if post_id.present? + tags = tags.where(posts: { id: post_id }) + end render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) end @@ -33,7 +37,8 @@ class TagsController < ApplicationController matched_alias_by_tag_name_id[canonical_id] ||= alias_name end - base = Tag.joins(:tag_name).includes(:tag_name) + base = Tag.joins(:tag_name) + .includes(:tag_name, tag_name: :wiki_page) base = base.where('tags.post_count > 0') if present_only canonical_hit = @@ -58,7 +63,9 @@ class TagsController < ApplicationController end def show - tag = Tag.find_by(id: params[:id]) + tag = Tag.joins(:tag_name) + .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]) else @@ -70,7 +77,9 @@ class TagsController < ApplicationController name = params[:name].to_s.strip return head :bad_request if name.blank? - tag = Tag.joins(:tag_name).includes(:tag_name).find_by(tag_names: { name: }) + tag = Tag.joins(:tag_name) + .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]) else diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index 20b2912..6dc4216 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -3,7 +3,11 @@ class WikiPagesController < ApplicationController def index title = params[:title].to_s.strip - return render json: WikiPage.all.as_json(methods: [:title]) if title.blank? + if title.blank? + return render json: WikiPage.joins(:tag_name) + .includes(:tag_name) + .as_json(methods: [:title]) + end q = WikiPage.joins(:tag_name).includes(:tag_name) .where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") @@ -11,7 +15,9 @@ class WikiPagesController < ApplicationController end def show - page = WikiPage.find_by(id: params[:id]) + page = WikiPage.joins(:tag_name) + .includes(:tag_name) + .find_by(id: params[:id]) render_wiki_page_or_404 page end @@ -19,7 +25,7 @@ class WikiPagesController < ApplicationController title = params[:title].to_s.strip page = WikiPage.joins(:tag_name) .includes(:tag_name) - .find_by(tag_names: { name: title }) + .find_by(tag_name: { name: title }) render_wiki_page_or_404 page end @@ -47,7 +53,7 @@ class WikiPagesController < ApplicationController from = params[:from].presence to = params[:to].presence - page = WikiPage.find(id) + page = WikiPage.joins(:tag_name).includes(:tag_name).find(id) from_rev = from && page.wiki_revisions.find(from) to_rev = to ? page.wiki_revisions.find(to) : page.current_revision @@ -131,7 +137,9 @@ class WikiPagesController < ApplicationController def changes id = params[:id].presence - q = WikiRevision.includes(:wiki_page, :created_user).order(id: :desc) + q = WikiRevision.joins(wiki_page: :tag_name) + .includes(:created_user, wiki_page: :tag_name) + .order(id: :desc) q = q.where(wiki_page_id: id) if id render json: q.limit(200).map { |rev| @@ -139,7 +147,7 @@ class WikiPagesController < ApplicationController pred: rev.base_revision_id, succ: nil, wiki_page: { id: rev.wiki_page_id, title: rev.wiki_page.title }, - user: { id: rev.created_user.id, name: rev.created_user.name }, + user: rev.created_user && { id: rev.created_user.id, name: rev.created_user.name }, kind: rev.kind, message: rev.message, timestamp: rev.created_at } diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index e25575f..c898615 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -30,13 +30,15 @@ class Post < ApplicationRecord super(options).merge(thumbnail: nil) end - def related(limit: nil) - ids = post_similarities.select(:target_post_id).order(cos: :desc) + def related limit: nil + ids = post_similarities.order(cos: :desc) ids = ids.limit(limit) if limit ids = ids.pluck(:target_post_id) - return [] if ids.empty? + return Post.none if ids.empty? - Post.where(id: ids).order(Arel.sql("FIELD(id, #{ ids.join(',') })")) + Post.where(id: ids) + .with_attached_thumbnail + .order(Arel.sql("FIELD(posts.id, #{ ids.join(',') })")) end def resized_thumbnail! diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index c4b36e6..60cc79b 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -25,6 +25,8 @@ class Tag < ApplicationRecord has_many :tag_similarities, dependent: :delete_all belongs_to :tag_name + delegate :wiki_page, to: :tag_name + delegate :name, to: :tag_name, allow_nil: true validates :tag_name, presence: true @@ -56,7 +58,7 @@ class Tag < ApplicationRecord end def has_wiki - tag_name&.wiki_page.present? + wiki_page.present? end def self.tagme From eb975e530147ff50d4f5a3a212d0b2215e91d3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Wed, 11 Feb 2026 13:27:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E3=83=97=E3=83=AA=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=83=81=E5=AE=9F=E8=A3=85=EF=BC=88#140=EF=BC=89=20(#?= =?UTF-8?q?256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge branch 'main' into feature/140 #140 Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 #140 #140 Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 #140 #140 #140 #140 #140 #140 #140 #140 Merge remote-tracking branch 'origin/main' into feature/140 Merge remote-tracking branch 'origin/main' into feature/140 #140 ぼちぼち Merge remote-tracking branch 'origin/main' into feature/140 #140 #140 #140 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/256 --- frontend/src/App.tsx | 18 +-- frontend/src/components/PostEditForm.tsx | 15 +- frontend/src/components/PostFormTagsArea.tsx | 7 +- frontend/src/components/PostList.tsx | 93 ++++++------ frontend/src/components/TagDetailSidebar.tsx | 41 ++---- frontend/src/components/TagLink.tsx | 41 +++--- frontend/src/components/TagSearch.tsx | 15 +- frontend/src/components/TagSidebar.tsx | 9 +- frontend/src/components/TopNav.tsx | 41 +++--- frontend/src/components/TopNavUser.tsx | 7 +- frontend/src/components/WikiBody.tsx | 35 ++--- .../src/components/users/InheritDialogue.tsx | 10 +- .../src/components/users/UserCodeDialogue.tsx | 10 +- frontend/src/lib/api.ts | 14 +- frontend/src/lib/posts.ts | 17 ++- frontend/src/lib/prefetchers.ts | 94 +++++++++++-- frontend/src/lib/queryKeys.ts | 13 +- frontend/src/lib/tags.ts | 12 +- frontend/src/lib/wiki.ts | 25 +++- frontend/src/pages/posts/PostDetailPage.tsx | 122 ++++++++-------- frontend/src/pages/posts/PostHistoryPage.tsx | 133 +++++++++--------- frontend/src/pages/posts/PostNewPage.tsx | 20 +-- frontend/src/pages/tags/NicoTagListPage.tsx | 17 +-- frontend/src/pages/users/SettingPage.tsx | 11 +- frontend/src/pages/wiki/WikiDetailPage.tsx | 106 ++++++-------- frontend/src/pages/wiki/WikiDiffPage.tsx | 8 +- frontend/src/pages/wiki/WikiEditPage.tsx | 19 ++- frontend/src/pages/wiki/WikiHistoryPage.tsx | 26 ++-- frontend/src/pages/wiki/WikiNewPage.tsx | 10 +- frontend/src/pages/wiki/WikiSearchPage.tsx | 20 +-- 30 files changed, 519 insertions(+), 490 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7e950f5..98fa8ce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,3 @@ -import axios from 'axios' -import toCamel from 'camelcase-keys' import { AnimatePresence, LayoutGroup } from 'framer-motion' import { useEffect, useState } from 'react' import { BrowserRouter, @@ -11,7 +9,7 @@ import { BrowserRouter, import RouteBlockerOverlay from '@/components/RouteBlockerOverlay' import TopNav from '@/components/TopNav' import { Toaster } from '@/components/ui/toaster' -import { API_BASE_URL } from '@/config' +import { apiPost, isApiError } from '@/lib/api' import NicoTagListPage from '@/pages/tags/NicoTagListPage' import NotFound from '@/pages/NotFound' import PostDetailPage from '@/pages/posts/PostDetailPage' @@ -75,12 +73,11 @@ export default (() => { useEffect (() => { const createUser = async () => { - const res = await axios.post (`${ API_BASE_URL }/users`) - const data = res.data as { code: string; user: any } + const data = await apiPost<{ code: string; user: User }> ('/users') if (data.code) { localStorage.setItem ('user_code', data.code) - setUser (toCamel (data.user, { deep: true }) as User) + setUser (data.user) } } @@ -90,17 +87,16 @@ export default (() => { void (async () => { try { - const res = await axios.post (`${ API_BASE_URL }/users/verify`, { code }) - const data = res.data as { valid: boolean, user: any } + const data = await apiPost<{ valid: boolean; user: User }> ('/users/verify', { code }) if (data.valid) - setUser (toCamel (data.user, { deep: true })) + setUser (data.user) else await createUser () } catch (err) { - if (axios.isAxiosError (err)) - setStatus (err.status ?? 200) + if (isApiError (err)) + setStatus (err.response?.status ?? 200) } }) () } diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index 38b03b4..d4421e8 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -1,12 +1,10 @@ -import axios from 'axios' -import toCamel from 'camelcase-keys' import { useEffect, useState } from 'react' import PostFormTagsArea from '@/components/PostFormTagsArea' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import Label from '@/components/common/Label' import { Button } from '@/components/ui/button' -import { API_BASE_URL } from '@/config' +import { apiPut } from '@/lib/api' import type { FC } from 'react' @@ -41,14 +39,11 @@ export default (({ post, onSave }: Props) => { const [tags, setTags] = useState ('') const handleSubmit = async () => { - const res = await axios.put ( - `${ API_BASE_URL }/posts/${ post.id }`, - { title, tags, - original_created_from: originalCreatedFrom, + const data = await apiPut ( + `/posts/${ post.id }`, + { title, tags, original_created_from: originalCreatedFrom, original_created_before: originalCreatedBefore }, - { headers: { 'Content-Type': 'multipart/form-data', - 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - const data = toCamel (res.data as any, { deep: true }) as Post + { headers: { 'Content-Type': 'multipart/form-data' } }) onSave ({ ...post, title: data.title, tags: data.tags, diff --git a/frontend/src/components/PostFormTagsArea.tsx b/frontend/src/components/PostFormTagsArea.tsx index de97ada..b5fac1a 100644 --- a/frontend/src/components/PostFormTagsArea.tsx +++ b/frontend/src/components/PostFormTagsArea.tsx @@ -1,11 +1,9 @@ -import axios from 'axios' -import toCamel from 'camelcase-keys' import { useRef, useState } from 'react' import TagSearchBox from '@/components/TagSearchBox' import Label from '@/components/common/Label' import TextArea from '@/components/common/TextArea' -import { API_BASE_URL } from '@/config' +import { apiGet } from '@/lib/api' import type { FC, SyntheticEvent } from 'react' @@ -59,8 +57,7 @@ export default (({ tags, setTags }: Props) => { const recompute = async (pos: number, v: string = tags) => { const { start, end, token } = getTokenAt (v, pos) setBounds ({ start, end }) - const res = await axios.get (`${ API_BASE_URL }/tags/autocomplete`, { params: { q: token } }) - const data = toCamel (res.data as any, { deep: true }) as Tag[] + const data = await apiGet ('/tags/autocomplete', { params: { q: token } }) setSuggestions (data.filter (t => t.postCount > 0)) setSuggestionsVsbl (suggestions.length > 0) } diff --git a/frontend/src/components/PostList.tsx b/frontend/src/components/PostList.tsx index 8e01fd4..39adbb3 100644 --- a/frontend/src/components/PostList.tsx +++ b/frontend/src/components/PostList.tsx @@ -21,53 +21,50 @@ export default (({ posts, onClick }: Props) => { const cardRef = useRef (null) return ( - <> -
- {posts.map ((post, i) => { - const id2 = `page-${ post.id }` - const layoutId = id2 +
+ {posts.map ((post, i) => { + const sharedId = `page-${ post.id }` + const layoutId = sharedId - return ( - { - const sharedId = `page-${ post.id }` - setForLocationKey (location.key, sharedId) - onClick?.(e) - }}> - { - if (cardRef.current) - { - cardRef.current.style.position = 'relative' - cardRef.current.style.zIndex = '9999' - } - }} - onLayoutAnimationComplete={() => { - if (cardRef.current) - { - cardRef.current.style.zIndex = '' - cardRef.current.style.position = '' - } - }} - transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}> - {post.title - - ) - })} -
- ) + return ( + { + setForLocationKey (location.key, sharedId) + onClick?.(e) + }}> + { + if (!(cardRef.current)) + return + + cardRef.current.style.position = 'relative' + cardRef.current.style.zIndex = '9999' + }} + onLayoutAnimationComplete={() => { + if (!(cardRef.current)) + return + + cardRef.current.style.zIndex = '' + cardRef.current.style.position = '' + }} + transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}> + {post.title + + ) + })} +
) }) satisfies FC diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 1362724..64dbfe3 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -6,21 +6,19 @@ import { DndContext, useSensor, useSensors } from '@dnd-kit/core' import { restrictToWindowEdges } from '@dnd-kit/modifiers' -import axios from 'axios' -import toCamel from 'camelcase-keys' import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useRef, useState } from 'react' -import { Link } from 'react-router-dom' import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow' +import PrefetchLink from '@/components/PrefetchLink' import TagLink from '@/components/TagLink' import TagSearch from '@/components/TagSearch' import SectionTitle from '@/components/common/SectionTitle' import SubsectionTitle from '@/components/common/SubsectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' import { toast } from '@/components/ui/use-toast' -import { API_BASE_URL } from '@/config' import { CATEGORIES } from '@/consts' +import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api' import type { DragEndEvent } from '@dnd-kit/core' import type { FC, MutableRefObject, ReactNode } from 'react' @@ -132,10 +130,7 @@ const changeCategory = async ( tagId: number, category: Category, ): Promise => { - await axios.patch ( - `${ API_BASE_URL }/tags/${ tagId }`, - { category }, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) + await apiPatch (`/tags/${ tagId }`, { category }) } @@ -170,12 +165,7 @@ export default (({ post }: Props) => { if (!(post)) return - const res = await axios.get ( - `${ API_BASE_URL }/posts/${ post.id }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - const data = toCamel (res.data as any, { deep: true }) as Post - - setTags (buildTagByCategory (data)) + setTags (buildTagByCategory (await apiGet (`/posts/${ post.id }`))) } const onDragEnd = async (e: DragEndEvent) => { @@ -216,16 +206,9 @@ export default (({ post }: Props) => { return if (fromParentId != null) - { - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - } + await apiDelete (`/tags/${ fromParentId }/children/${ childId }`) - await axios.post ( - `${ API_BASE_URL }/tags/${ parentId }/children/${ childId }`, - { }, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) + await apiPost (`/tags/${ parentId }/children/${ childId }`, { }) await reloadTags () toast ({ @@ -245,11 +228,7 @@ export default (({ post }: Props) => { await changeCategory (childId, cat) if (fromParentId != null) - { - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - } + await apiDelete (`/tags/${ fromParentId }/children/${ childId }`) const fromParent = fromParentId == null ? null : findTag (tags, fromParentId) @@ -358,9 +337,9 @@ export default (({ post }: Props) => { <>耕作者: {post.uploadedUser ? ( - + {post.uploadedUser.name || '名もなきニジラー'} - ) + ) : 'bot操作'} */} @@ -389,7 +368,7 @@ export default (({ post }: Props) => { )}
  • - 履歴 + 履歴
  • )} diff --git a/frontend/src/components/TagLink.tsx b/frontend/src/components/TagLink.tsx index 9147e45..b3a926c 100644 --- a/frontend/src/components/TagLink.tsx +++ b/frontend/src/components/TagLink.tsx @@ -1,29 +1,34 @@ -import axios from 'axios' import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' import PrefetchLink from '@/components/PrefetchLink' -import { API_BASE_URL } from '@/config' import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' +import { apiGet } from '@/lib/api' import { cn } from '@/lib/utils' import type { ComponentProps, FC, HTMLAttributes } from 'react' import type { Tag } from '@/types' -type CommonProps = { tag: Tag - nestLevel?: number - withWiki?: boolean - withCount?: boolean - prefetch?: boolean } +type CommonProps = { + tag: Tag + nestLevel?: number + withWiki?: boolean + withCount?: boolean + prefetch?: boolean } type PropsWithLink = - CommonProps & { linkFlg?: true } & Partial> + & CommonProps + & { linkFlg?: true } + & Partial> type PropsWithoutLink = - CommonProps & { linkFlg: false } & Partial> + & CommonProps + & { linkFlg: false } + & Partial> -type Props = PropsWithLink | PropsWithoutLink +type Props = + | PropsWithLink + | PropsWithoutLink export default (({ tag, @@ -46,7 +51,7 @@ export default (({ tag, try { - await axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (tagName) }/exists`) + await apiGet (`/wiki/title/${ encodeURIComponent (tagName) }/exists`) setHavingWiki (true) } catch @@ -76,17 +81,17 @@ export default (({ tag, {havingWiki ? ( - ? - ) + ) : ( - ! - )} + )} )} {nestLevel > 0 && ( {tag.name} - : {tag.name} - ) + ) : ( diff --git a/frontend/src/components/TagSearch.tsx b/frontend/src/components/TagSearch.tsx index de1cde1..6e7a8bd 100644 --- a/frontend/src/components/TagSearch.tsx +++ b/frontend/src/components/TagSearch.tsx @@ -1,13 +1,11 @@ -import axios from 'axios' -import toCamel from 'camelcase-keys' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useNavigate, useLocation } from 'react-router-dom' -import { API_BASE_URL } from '@/config' +import { apiGet } from '@/lib/api' import TagSearchBox from './TagSearchBox' -import type { FC } from 'react' +import type { ChangeEvent, FC, KeyboardEvent } from 'react' import type { Tag } from '@/types' @@ -21,7 +19,7 @@ export default (() => { const [suggestions, setSuggestions] = useState ([]) const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) - const whenChanged = async (ev: React.ChangeEvent) => { + const whenChanged = async (ev: ChangeEvent) => { setSearch (ev.target.value) const q = ev.target.value.trim ().split (' ').at (-1) @@ -31,14 +29,13 @@ export default (() => { return } - const res = await axios.get (`${ API_BASE_URL }/tags/autocomplete`, { params: { q } }) - const data = toCamel (res.data, { deep: true }) as Tag[] + const data = await apiGet ('/tags/autocomplete', { params: { q } }) setSuggestions (data.filter (t => t.postCount > 0)) if (suggestions.length > 0) setSuggestionsVsbl (true) } - const handleKeyDown = (ev: React.KeyboardEvent) => { + const handleKeyDown = (ev: KeyboardEvent) => { switch (ev.key) { case 'ArrowDown': diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index 9959e47..ae06196 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -1,4 +1,3 @@ -import axios from 'axios' import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' @@ -7,8 +6,8 @@ import TagLink from '@/components/TagLink' import TagSearch from '@/components/TagSearch' import SectionTitle from '@/components/common/SectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' -import { API_BASE_URL } from '@/config' import { CATEGORIES } from '@/consts' +import { apiGet } from '@/lib/api' import type { FC, MouseEvent } from 'react' @@ -77,10 +76,10 @@ export default (({ posts, onClick }: Props) => { void ((async () => { try { - const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, - { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), + const data = await apiGet ('/posts/random', + { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), match: (anyFlg ? 'any' : 'all') } }) - navigate (`/posts/${ (data as Post).id }`) + navigate (`/posts/${ data.id }`) } catch { diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 0158ddd..144f517 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useLocation } from 'react-router-dom' @@ -6,6 +7,7 @@ import Separator from '@/components/MenuSeparator' import PrefetchLink from '@/components/PrefetchLink' import TopNavUser from '@/components/TopNavUser' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' +import { tagsKeys, wikiKeys } from '@/lib/queryKeys' import { fetchTagByName } from '@/lib/tags' import { cn } from '@/lib/utils' import { fetchWikiPage } from '@/lib/wiki' @@ -44,11 +46,26 @@ export default (({ user }: Props) => { visible: false }) const [menuOpen, setMenuOpen] = useState (false) const [openItemIdx, setOpenItemIdx] = useState (-1) - const [postCount, setPostCount] = useState (null) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) + const wikiIdStr = String (wikiId ?? '') + + const { data: wikiPage } = useQuery ({ + enabled: Boolean (wikiIdStr), + queryKey: wikiKeys.show (wikiIdStr, { }), + queryFn: () => fetchWikiPage (wikiIdStr, { }) }) + + const effectiveTitle = wikiPage?.title ?? '' + + const { data: tag } = useQuery ({ + enabled: Boolean (effectiveTitle), + queryKey: tagsKeys.show (effectiveTitle), + queryFn: () => fetchTagByName (effectiveTitle) }) + + const postCount = tag?.postCount ?? 0 + const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) - const wikiTitle = location.pathname.split ('/')[2] + const wikiTitle = location.pathname.split ('/')[2] ?? '' const menu: Menu = [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, @@ -113,26 +130,6 @@ export default (({ user }: Props) => { location.pathname.startsWith (item.base || item.to)))) }, [location]) - useEffect (() => { - if (!(wikiId)) - return - - const fetchPostCount = async () => { - try - { - const wikiPage = await fetchWikiPage (String (wikiId ?? '')) - const tag = await fetchTagByName (wikiPage.title) - - setPostCount (tag.postCount) - } - catch - { - setPostCount (0) - } - } - fetchPostCount () - }, [wikiId]) - return ( <>