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] =?UTF-8?q?=E3=83=97=E3=83=AA=E3=83=95=E3=82=A7=E3=83=83?= =?UTF-8?q?=E3=83=81=E5=AE=9F=E8=A3=85=EF=BC=88#140=EF=BC=89=20(#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 ( <>