設定画面 (#34) #397
+6
-1
@@ -12,7 +12,8 @@ import TopNav from '@/components/TopNav'
|
|||||||
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
import { apiPost, isApiError } from '@/lib/api'
|
import { apiPost, isApiError } from '@/lib/api'
|
||||||
import { applyClientAppearance,
|
import { applyClientAnimationMode,
|
||||||
|
applyClientAppearance,
|
||||||
fetchUserThemeSlots,
|
fetchUserThemeSlots,
|
||||||
fetchUserSettings,
|
fetchUserSettings,
|
||||||
getClientThemeMode,
|
getClientThemeMode,
|
||||||
@@ -142,6 +143,10 @@ const App: FC = () => {
|
|||||||
return () => mediaQuery.removeEventListener ('change', handleThemeChange)
|
return () => mediaQuery.removeEventListener ('change', handleThemeChange)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
applyClientAnimationMode (animationMode)
|
||||||
|
}, [animationMode])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
const data = await apiPost<{ code: string; user: User }> ('/users')
|
const data = await apiPost<{ code: string; user: User }> ('/users')
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ import type { FC, MutableRefObject, ReactNode } from 'react'
|
|||||||
import type { Category, Post, TagWithSections } from '@/types'
|
import type { Category, Post, TagWithSections } from '@/types'
|
||||||
|
|
||||||
type TagByCategory = { [key in Category]: TagWithSections[] }
|
type TagByCategory = { [key in Category]: TagWithSections[] }
|
||||||
type FlatTagRow = {
|
|
||||||
tag: TagWithSections
|
|
||||||
parentTagId?: number }
|
|
||||||
|
|
||||||
|
|
||||||
const renderTagTree = (
|
const renderTagTree = (
|
||||||
@@ -64,34 +61,6 @@ const renderTagTree = (
|
|||||||
?? [])]
|
?? [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const flattenTagTree = (
|
|
||||||
tags: TagWithSections[],
|
|
||||||
): FlatTagRow[] => {
|
|
||||||
const seen = new Set<number> ()
|
|
||||||
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 = (
|
const isDescendant = (
|
||||||
root: TagWithSections,
|
root: TagWithSections,
|
||||||
targetId: number,
|
targetId: number,
|
||||||
@@ -158,6 +127,37 @@ const buildTagByCategory = (post: Post): TagByCategory => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const buildFlatTagByCategory = (
|
||||||
|
byCategory: TagByCategory,
|
||||||
|
): TagByCategory => {
|
||||||
|
const tagsTmp = { } as TagByCategory
|
||||||
|
const seen = new Set<number> ()
|
||||||
|
|
||||||
|
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 (
|
const changeCategory = async (
|
||||||
tagId: number,
|
tagId: number,
|
||||||
category: Category): Promise<void> => {
|
category: Category): Promise<void> => {
|
||||||
@@ -210,6 +210,10 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
|
|||||||
const [dragging, setDragging] = useState (false)
|
const [dragging, setDragging] = useState (false)
|
||||||
const [saving, setSaving] = useState (false)
|
const [saving, setSaving] = useState (false)
|
||||||
const [tags, setTags] = useState (baseTags)
|
const [tags, setTags] = useState (baseTags)
|
||||||
|
const flatTagsByCategory = useMemo<TagByCategory> (
|
||||||
|
() => buildFlatTagByCategory (tags),
|
||||||
|
[tags],
|
||||||
|
)
|
||||||
|
|
||||||
const suppressClickRef = useRef (false)
|
const suppressClickRef = useRef (false)
|
||||||
|
|
||||||
@@ -343,41 +347,50 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
|
|||||||
document.body.style.userSelect = ''
|
document.body.style.userSelect = ''
|
||||||
}}
|
}}
|
||||||
modifiers={[restrictToWindowEdges]}>
|
modifiers={[restrictToWindowEdges]}>
|
||||||
{CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
|
{CATEGORIES.map ((cat: Category) => {
|
||||||
<div className="my-3" key={cat}>
|
const categoryTags =
|
||||||
<SubsectionTitle>
|
tagRelationDisplay === 'grouped'
|
||||||
<motion.div
|
? (tags[cat] ?? [])
|
||||||
layoutId={`tag-${ sp ? 'sp-' : '' }${ cat }`}
|
: (flatTagsByCategory[cat] ?? [])
|
||||||
transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
|
|
||||||
{CATEGORY_NAMES[cat]}
|
|
||||||
</motion.div>
|
|
||||||
</SubsectionTitle>
|
|
||||||
|
|
||||||
<ul>
|
if (!(categoryTags.length > 0 || dragging))
|
||||||
{(tagRelationDisplay === 'grouped'
|
return null
|
||||||
? (tags[cat] ?? []).flatMap (tag =>
|
|
||||||
renderTagTree (
|
return (
|
||||||
tag,
|
<div className="my-3" key={cat}>
|
||||||
0,
|
<SubsectionTitle>
|
||||||
`cat-${ cat }`,
|
<motion.div
|
||||||
suppressClickRef,
|
layoutId={`tag-${ sp ? 'sp-' : '' }${ cat }`}
|
||||||
undefined,
|
transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
|
||||||
sp,
|
{CATEGORY_NAMES[cat]}
|
||||||
),
|
</motion.div>
|
||||||
)
|
</SubsectionTitle>
|
||||||
: flattenTagTree (tags[cat] ?? []).map (row => (
|
|
||||||
<li key={`flat-${ cat }-${ row.tag.id }`} className="mb-1">
|
<ul>
|
||||||
<DraggableDroppableTagRow
|
{(tagRelationDisplay === 'grouped'
|
||||||
tag={row.tag}
|
? (tags[cat] ?? []).flatMap (tag =>
|
||||||
nestLevel={0}
|
renderTagTree (
|
||||||
pathKey={`flat-${ cat }-${ row.tag.id }`}
|
tag,
|
||||||
parentTagId={row.parentTagId}
|
0,
|
||||||
suppressClickRef={suppressClickRef}
|
`cat-${ cat }`,
|
||||||
sp={sp}/>
|
suppressClickRef,
|
||||||
</li>)))}
|
undefined,
|
||||||
<DropSlot cat={cat}/>
|
sp,
|
||||||
</ul>
|
),
|
||||||
</div>))}
|
)
|
||||||
|
: (flatTagsByCategory[cat] ?? []).map (tag => (
|
||||||
|
<li key={`flat-${ cat }-${ tag.id }`} className="mb-1">
|
||||||
|
<DraggableDroppableTagRow
|
||||||
|
tag={tag}
|
||||||
|
nestLevel={0}
|
||||||
|
pathKey={`flat-${ cat }-${ tag.id }`}
|
||||||
|
suppressClickRef={suppressClickRef}
|
||||||
|
sp={sp}/>
|
||||||
|
</li>)))}
|
||||||
|
<DropSlot cat={cat}/>
|
||||||
|
</ul>
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
{post && (
|
{post && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId={`post-info-${ sp }`}
|
layoutId={`post-info-${ sp }`}
|
||||||
|
|||||||
@@ -161,13 +161,17 @@ const TopNav: FC<Props> = ({ user }) => {
|
|||||||
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
|
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
|
||||||
const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40)
|
const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40)
|
||||||
const topNavTransition =
|
const topNavTransition =
|
||||||
reducedAnimations
|
animationsOff
|
||||||
? { duration: .08, ease: 'linear' as const }
|
? { duration: 0 }
|
||||||
: { duration: .2, ease: 'easeOut' as const }
|
: reducedAnimations
|
||||||
|
? { duration: .08, ease: 'linear' as const }
|
||||||
|
: { duration: .2, ease: 'easeOut' as const }
|
||||||
const opacityTransition =
|
const opacityTransition =
|
||||||
reducedAnimations
|
animationsOff
|
||||||
? { duration: .08 }
|
? { duration: 0 }
|
||||||
: { duration: .12 }
|
: reducedAnimations
|
||||||
|
? { duration: .08 }
|
||||||
|
: { duration: .12 }
|
||||||
const highlightTransitionClass =
|
const highlightTransitionClass =
|
||||||
animationsOff
|
animationsOff
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする