diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d95cbcf..307695e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,7 +12,8 @@ import TopNav from '@/components/TopNav' import DialogueProvider from '@/components/dialogues/DialogueProvider' import { Toaster } from '@/components/ui/toaster' import { apiPost, isApiError } from '@/lib/api' -import { applyClientAppearance, +import { applyClientAnimationMode, + applyClientAppearance, fetchUserThemeSlots, fetchUserSettings, getClientThemeMode, @@ -142,6 +143,10 @@ const App: FC = () => { return () => mediaQuery.removeEventListener ('change', handleThemeChange) }, []) + useEffect (() => { + applyClientAnimationMode (animationMode) + }, [animationMode]) + useEffect (() => { const createUser = async () => { const data = await apiPost<{ code: string; user: User }> ('/users') diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 47d1daa..e90f4f5 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -30,9 +30,6 @@ import type { FC, MutableRefObject, ReactNode } from 'react' import type { Category, Post, TagWithSections } from '@/types' type TagByCategory = { [key in Category]: TagWithSections[] } -type FlatTagRow = { - tag: TagWithSections - parentTagId?: number } const renderTagTree = ( @@ -64,34 +61,6 @@ const renderTagTree = ( ?? [])] } - -const flattenTagTree = ( - tags: TagWithSections[], -): FlatTagRow[] => { - const seen = new Set () - const rows: FlatTagRow[] = [] - - const visit = ( - tag: TagWithSections, - parentTagId?: number, - ) => { - if (seen.has (tag.id)) - return - - seen.add (tag.id) - rows.push ({ tag, parentTagId }) - - for (const child of tag.children ?? []) - visit (child, tag.id) - } - - for (const tag of tags) - visit (tag) - - return rows.sort ((rowA, rowB) => rowA.tag.name < rowB.tag.name ? -1 : 1) -} - - const isDescendant = ( root: TagWithSections, targetId: number, @@ -158,6 +127,37 @@ const buildTagByCategory = (post: Post): TagByCategory => { } +const buildFlatTagByCategory = ( + byCategory: TagByCategory, +): TagByCategory => { + const tagsTmp = { } as TagByCategory + const seen = new Set () + + for (const category of CATEGORIES) + tagsTmp[category] = [] + + const visit = (tag: TagWithSections) => { + if (seen.has (tag.id)) + return + + seen.add (tag.id) + tagsTmp[tag.category].push ({ ...tag, children: [] }) + + for (const child of tag.children ?? []) + visit (child) + } + + for (const category of CATEGORIES) + for (const tag of byCategory[category] ?? []) + visit (tag) + + for (const category of CATEGORIES) + tagsTmp[category].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) + + return tagsTmp +} + + const changeCategory = async ( tagId: number, category: Category): Promise => { @@ -210,6 +210,10 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { const [dragging, setDragging] = useState (false) const [saving, setSaving] = useState (false) const [tags, setTags] = useState (baseTags) + const flatTagsByCategory = useMemo ( + () => buildFlatTagByCategory (tags), + [tags], + ) const suppressClickRef = useRef (false) @@ -343,41 +347,50 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { document.body.style.userSelect = '' }} modifiers={[restrictToWindowEdges]}> - {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && ( -
- - - {CATEGORY_NAMES[cat]} - - + {CATEGORIES.map ((cat: Category) => { + const categoryTags = + tagRelationDisplay === 'grouped' + ? (tags[cat] ?? []) + : (flatTagsByCategory[cat] ?? []) -
    - {(tagRelationDisplay === 'grouped' - ? (tags[cat] ?? []).flatMap (tag => - renderTagTree ( - tag, - 0, - `cat-${ cat }`, - suppressClickRef, - undefined, - sp, - ), - ) - : flattenTagTree (tags[cat] ?? []).map (row => ( -
  • - -
  • )))} - -
-
))} + if (!(categoryTags.length > 0 || dragging)) + return null + + return ( +
+ + + {CATEGORY_NAMES[cat]} + + + +
    + {(tagRelationDisplay === 'grouped' + ? (tags[cat] ?? []).flatMap (tag => + renderTagTree ( + tag, + 0, + `cat-${ cat }`, + suppressClickRef, + undefined, + sp, + ), + ) + : (flatTagsByCategory[cat] ?? []).map (tag => ( +
  • + +
  • )))} + +
+
) + })} {post && ( = ({ user }) => { visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to)) const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40) const topNavTransition = - reducedAnimations - ? { duration: .08, ease: 'linear' as const } - : { duration: .2, ease: 'easeOut' as const } + animationsOff + ? { duration: 0 } + : reducedAnimations + ? { duration: .08, ease: 'linear' as const } + : { duration: .2, ease: 'easeOut' as const } const opacityTransition = - reducedAnimations - ? { duration: .08 } - : { duration: .12 } + animationsOff + ? { duration: 0 } + : reducedAnimations + ? { duration: .08 } + : { duration: .12 } const highlightTransitionClass = animationsOff ? undefined