|
|
@@ -1,14 +1,16 @@ |
|
|
|
|
|
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' |
|
|
import { AnimatePresence, motion } from 'framer-motion' |
|
|
import { AnimatePresence, motion } from 'framer-motion' |
|
|
import { useEffect, useState } from 'react' |
|
|
|
|
|
|
|
|
import { useEffect, useRef, useState } from 'react' |
|
|
|
|
|
|
|
|
import TagLink from '@/components/TagLink' |
|
|
|
|
|
|
|
|
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow' |
|
|
import TagSearch from '@/components/TagSearch' |
|
|
import TagSearch from '@/components/TagSearch' |
|
|
import SectionTitle from '@/components/common/SectionTitle' |
|
|
import SectionTitle from '@/components/common/SectionTitle' |
|
|
import SubsectionTitle from '@/components/common/SubsectionTitle' |
|
|
import SubsectionTitle from '@/components/common/SubsectionTitle' |
|
|
import SidebarComponent from '@/components/layout/SidebarComponent' |
|
|
import SidebarComponent from '@/components/layout/SidebarComponent' |
|
|
import { CATEGORIES } from '@/consts' |
|
|
import { CATEGORIES } from '@/consts' |
|
|
|
|
|
|
|
|
import type { FC, ReactNode } from 'react' |
|
|
|
|
|
|
|
|
import type { DragEndEvent } from '@dnd-kit/core' |
|
|
|
|
|
import type { FC, MutableRefObject, ReactNode } from 'react' |
|
|
|
|
|
|
|
|
import type { Category, Post, Tag } from '@/types' |
|
|
import type { Category, Post, Tag } from '@/types' |
|
|
|
|
|
|
|
|
@@ -16,9 +18,11 @@ type TagByCategory = { [key in Category]: Tag[] } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderTagTree = ( |
|
|
const renderTagTree = ( |
|
|
tag: Tag, |
|
|
|
|
|
nestLevel: number, |
|
|
|
|
|
path: string, |
|
|
|
|
|
|
|
|
tag: Tag, |
|
|
|
|
|
nestLevel: number, |
|
|
|
|
|
path: string, |
|
|
|
|
|
suppressClickRef: MutableRefObject<boolean>, |
|
|
|
|
|
parentTagId?: number, |
|
|
): ReactNode[] => { |
|
|
): ReactNode[] => { |
|
|
const key = `${ path }-${ tag.id }` |
|
|
const key = `${ path }-${ tag.id }` |
|
|
|
|
|
|
|
|
@@ -28,23 +32,176 @@ const renderTagTree = ( |
|
|
layout |
|
|
layout |
|
|
transition={{ duration: .2, ease: 'easeOut' }} |
|
|
transition={{ duration: .2, ease: 'easeOut' }} |
|
|
className="mb-1"> |
|
|
className="mb-1"> |
|
|
<TagLink tag={tag} nestLevel={nestLevel}/> |
|
|
|
|
|
|
|
|
<DraggableDroppableTagRow |
|
|
|
|
|
tag={tag} |
|
|
|
|
|
nestLevel={nestLevel} |
|
|
|
|
|
pathKey={key} |
|
|
|
|
|
parentTagId={parentTagId} |
|
|
|
|
|
suppressClickRef={suppressClickRef}/> |
|
|
</motion.li>) |
|
|
</motion.li>) |
|
|
|
|
|
|
|
|
return [self, |
|
|
return [self, |
|
|
...((tag.children |
|
|
...((tag.children |
|
|
?.sort ((a, b) => a.name < b.name ? -1 : 1) |
|
|
?.sort ((a, b) => a.name < b.name ? -1 : 1) |
|
|
.flatMap (child => renderTagTree (child, nestLevel + 1, key))) |
|
|
|
|
|
|
|
|
.flatMap (child => ( |
|
|
|
|
|
renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id)))) |
|
|
?? [])] |
|
|
?? [])] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const removeEverywhere = ( |
|
|
|
|
|
list: Tag[], |
|
|
|
|
|
tagId: number, |
|
|
|
|
|
): { next: Tag[] |
|
|
|
|
|
picked?: Tag } => { |
|
|
|
|
|
let picked: Tag | undefined |
|
|
|
|
|
|
|
|
|
|
|
const walk = (nodes: Tag[]): Tag[] => ( |
|
|
|
|
|
nodes |
|
|
|
|
|
.map (t => { |
|
|
|
|
|
const children = t.children ? walk (t.children) : undefined |
|
|
|
|
|
return children ? { ...t, children } : t |
|
|
|
|
|
}) |
|
|
|
|
|
.filter (t => { |
|
|
|
|
|
if (t.id === tagId) |
|
|
|
|
|
{ |
|
|
|
|
|
picked = picked ?? t |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
return true |
|
|
|
|
|
})) |
|
|
|
|
|
|
|
|
|
|
|
return { next: walk (list), picked } |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const addAsChild = ( |
|
|
|
|
|
list: Tag[], |
|
|
|
|
|
parentId: number, |
|
|
|
|
|
picked: Tag, |
|
|
|
|
|
): Tag[] => { |
|
|
|
|
|
const walk = (nodes: Tag[]): Tag[] => ( |
|
|
|
|
|
nodes.map (t => { |
|
|
|
|
|
if (t.id !== parentId) |
|
|
|
|
|
return t.children ? { ...t, children: walk (t.children) } : t |
|
|
|
|
|
|
|
|
|
|
|
const cur = t.children ?? [] |
|
|
|
|
|
const exists = cur.some (c => c.id === picked.id) |
|
|
|
|
|
const children = exists ? cur : [...cur, { ...picked, children: picked.children ?? [] }] |
|
|
|
|
|
|
|
|
|
|
|
return { ...t, children } |
|
|
|
|
|
})) |
|
|
|
|
|
|
|
|
|
|
|
return walk (list) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const attachChildOptimistic = ( |
|
|
|
|
|
prev: TagByCategory, |
|
|
|
|
|
parentId: number, |
|
|
|
|
|
childId: number, |
|
|
|
|
|
): TagByCategory => { |
|
|
|
|
|
const next: TagByCategory = { ...prev } |
|
|
|
|
|
|
|
|
|
|
|
let picked: Tag | undefined |
|
|
|
|
|
for (const cat of Object.keys (next) as (keyof typeof next)[]) |
|
|
|
|
|
{ |
|
|
|
|
|
const r = removeEverywhere (next[cat], childId) |
|
|
|
|
|
next[cat] = r.next |
|
|
|
|
|
picked = picked ?? r.picked |
|
|
|
|
|
} |
|
|
|
|
|
if (!(picked)) |
|
|
|
|
|
return prev |
|
|
|
|
|
|
|
|
|
|
|
for (const cat of Object.keys (next) as (keyof typeof next)[]) |
|
|
|
|
|
next[cat] = addAsChild (next[cat], parentId, picked) |
|
|
|
|
|
|
|
|
|
|
|
return next |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isDescendant = ( |
|
|
|
|
|
root: Tag, |
|
|
|
|
|
targetId: number, |
|
|
|
|
|
): boolean => { |
|
|
|
|
|
if (!(root.children)) |
|
|
|
|
|
return false |
|
|
|
|
|
|
|
|
|
|
|
for (const c of root.children) |
|
|
|
|
|
{ |
|
|
|
|
|
if (c.id === targetId) |
|
|
|
|
|
return true |
|
|
|
|
|
if (isDescendant (c, targetId)) |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const findTag = ( |
|
|
|
|
|
byCat: TagByCategory, |
|
|
|
|
|
id: number, |
|
|
|
|
|
): Tag | undefined => { |
|
|
|
|
|
const walk = (nodes: Tag[]): Tag | undefined => { |
|
|
|
|
|
for (const t of nodes) |
|
|
|
|
|
{ |
|
|
|
|
|
if (t.id === id) |
|
|
|
|
|
return t |
|
|
|
|
|
|
|
|
|
|
|
const found = t.children ? walk (t.children) : undefined |
|
|
|
|
|
if (found) |
|
|
|
|
|
return found |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return undefined |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (const cat of Object.keys (byCat) as (keyof typeof byCat)[]) |
|
|
|
|
|
{ |
|
|
|
|
|
const found = walk (byCat[cat] ?? []) |
|
|
|
|
|
if (found) |
|
|
|
|
|
return found |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return undefined |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Props = { post: Post | null } |
|
|
type Props = { post: Post | null } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default (({ post }: Props) => { |
|
|
export default (({ post }: Props) => { |
|
|
const [tags, setTags] = useState ({ } as TagByCategory) |
|
|
const [tags, setTags] = useState ({ } as TagByCategory) |
|
|
|
|
|
|
|
|
|
|
|
const suppressClickRef = useRef (false) |
|
|
|
|
|
|
|
|
|
|
|
const sensors = useSensors ( |
|
|
|
|
|
useSensor (PointerSensor, { activationConstraint: { distance: 6 } })) |
|
|
|
|
|
|
|
|
|
|
|
const onDragEnd = async (e: DragEndEvent) => { |
|
|
|
|
|
const childId: number | undefined = e.active.data.current?.tagId |
|
|
|
|
|
const parentId: number | undefined = e.over?.data.current?.tagId |
|
|
|
|
|
|
|
|
|
|
|
if (!(childId) || !(parentId)) |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
if (childId === parentId) |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
setTags (prev => { |
|
|
|
|
|
const child = findTag (prev, childId) |
|
|
|
|
|
const parent = findTag (prev, parentId) |
|
|
|
|
|
if (!(child) || !(parent)) |
|
|
|
|
|
return prev |
|
|
|
|
|
|
|
|
|
|
|
if (isDescendant (child, parentId)) |
|
|
|
|
|
return prev |
|
|
|
|
|
|
|
|
|
|
|
return attachChildOptimistic (prev, parentId, childId) |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const categoryNames: Record<Category, string> = { |
|
|
const categoryNames: Record<Category, string> = { |
|
|
deerjikist: 'ニジラー', |
|
|
deerjikist: 'ニジラー', |
|
|
meme: '原作・ネタ元・ミーム等', |
|
|
meme: '原作・ネタ元・ミーム等', |
|
|
@@ -76,60 +233,81 @@ export default (({ post }: Props) => { |
|
|
return ( |
|
|
return ( |
|
|
<SidebarComponent> |
|
|
<SidebarComponent> |
|
|
<TagSearch/> |
|
|
<TagSearch/> |
|
|
<motion.div layout> |
|
|
|
|
|
{CATEGORIES.map ((cat: Category) => cat in tags && ( |
|
|
|
|
|
<motion.div layout className="my-3" key={cat}> |
|
|
|
|
|
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> |
|
|
|
|
|
|
|
|
|
|
|
<motion.ul layout> |
|
|
|
|
|
<AnimatePresence initial={false}> |
|
|
|
|
|
{tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} |
|
|
|
|
|
</AnimatePresence> |
|
|
|
|
|
</motion.ul> |
|
|
|
|
|
</motion.div>))} |
|
|
|
|
|
{post && ( |
|
|
|
|
|
<div> |
|
|
|
|
|
<SectionTitle>情報</SectionTitle> |
|
|
|
|
|
<ul> |
|
|
|
|
|
<li>Id.: {post.id}</li> |
|
|
|
|
|
{/* TODO: uploadedUser の取得を対応したらコメント外す */} |
|
|
|
|
|
{/* |
|
|
|
|
|
<li> |
|
|
|
|
|
<>耕作者: </> |
|
|
|
|
|
{post.uploadedUser |
|
|
|
|
|
? ( |
|
|
|
|
|
<Link to={`/users/${ post.uploadedUser.id }`}> |
|
|
|
|
|
{post.uploadedUser.name || '名もなきニジラー'} |
|
|
|
|
|
</Link>) |
|
|
|
|
|
: 'bot操作'} |
|
|
|
|
|
</li> |
|
|
|
|
|
*/} |
|
|
|
|
|
<li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li> |
|
|
|
|
|
<li> |
|
|
|
|
|
<>リンク: </> |
|
|
|
|
|
<a |
|
|
|
|
|
className="break-all" |
|
|
|
|
|
href={post.url} |
|
|
|
|
|
target="_blank" |
|
|
|
|
|
rel="noopener noreferrer nofollow"> |
|
|
|
|
|
{post.url} |
|
|
|
|
|
</a> |
|
|
|
|
|
</li> |
|
|
|
|
|
<li> |
|
|
|
|
|
{/* TODO: 表示形式きしょすぎるので何とかする */} |
|
|
|
|
|
<>オリジナルの投稿日時: </> |
|
|
|
|
|
{!(post.originalCreatedFrom) && !(post.originalCreatedBefore) |
|
|
|
|
|
? '不明' |
|
|
|
|
|
: ( |
|
|
|
|
|
<> |
|
|
|
|
|
{post.originalCreatedFrom |
|
|
|
|
|
&& `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} |
|
|
|
|
|
{post.originalCreatedBefore |
|
|
|
|
|
&& `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} |
|
|
|
|
|
</>)} |
|
|
|
|
|
</li> |
|
|
|
|
|
</ul> |
|
|
|
|
|
</div>)} |
|
|
|
|
|
</motion.div> |
|
|
|
|
|
|
|
|
<DndContext |
|
|
|
|
|
sensors={sensors} |
|
|
|
|
|
onDragStart={() => { |
|
|
|
|
|
suppressClickRef.current = true |
|
|
|
|
|
document.body.style.userSelect = 'none' |
|
|
|
|
|
getSelection?.()?.removeAllRanges?.() |
|
|
|
|
|
addEventListener ('click', e => { |
|
|
|
|
|
e.preventDefault () |
|
|
|
|
|
e.stopPropagation () |
|
|
|
|
|
suppressClickRef.current = false |
|
|
|
|
|
}, { capture: true, once: true }) |
|
|
|
|
|
}} |
|
|
|
|
|
onDragCancel={() => { |
|
|
|
|
|
document.body.style.userSelect = '' |
|
|
|
|
|
}} |
|
|
|
|
|
onDragEnd={e => { |
|
|
|
|
|
onDragEnd (e) |
|
|
|
|
|
document.body.style.userSelect = '' |
|
|
|
|
|
}}> |
|
|
|
|
|
<motion.div key={post?.id ?? 0} layout> |
|
|
|
|
|
{CATEGORIES.map ((cat: Category) => cat in tags && ( |
|
|
|
|
|
<motion.div layout className="my-3" key={cat}> |
|
|
|
|
|
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> |
|
|
|
|
|
|
|
|
|
|
|
<motion.ul layout> |
|
|
|
|
|
<AnimatePresence initial={false}> |
|
|
|
|
|
{tags[cat].map (tag => ( |
|
|
|
|
|
renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))} |
|
|
|
|
|
</AnimatePresence> |
|
|
|
|
|
</motion.ul> |
|
|
|
|
|
</motion.div>))} |
|
|
|
|
|
{post && ( |
|
|
|
|
|
<div> |
|
|
|
|
|
<SectionTitle>情報</SectionTitle> |
|
|
|
|
|
<ul> |
|
|
|
|
|
<li>Id.: {post.id}</li> |
|
|
|
|
|
{/* TODO: uploadedUser の取得を対応したらコメント外す */} |
|
|
|
|
|
{/* |
|
|
|
|
|
<li> |
|
|
|
|
|
<>耕作者: </> |
|
|
|
|
|
{post.uploadedUser |
|
|
|
|
|
? ( |
|
|
|
|
|
<Link to={`/users/${ post.uploadedUser.id }`}> |
|
|
|
|
|
{post.uploadedUser.name || '名もなきニジラー'} |
|
|
|
|
|
</Link>) |
|
|
|
|
|
: 'bot操作'} |
|
|
|
|
|
</li> |
|
|
|
|
|
*/} |
|
|
|
|
|
<li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li> |
|
|
|
|
|
<li> |
|
|
|
|
|
<>リンク: </> |
|
|
|
|
|
<a |
|
|
|
|
|
className="break-all" |
|
|
|
|
|
href={post.url} |
|
|
|
|
|
target="_blank" |
|
|
|
|
|
rel="noopener noreferrer nofollow"> |
|
|
|
|
|
{post.url} |
|
|
|
|
|
</a> |
|
|
|
|
|
</li> |
|
|
|
|
|
<li> |
|
|
|
|
|
{/* TODO: 表示形式きしょすぎるので何とかする */} |
|
|
|
|
|
<>オリジナルの投稿日時: </> |
|
|
|
|
|
{!(post.originalCreatedFrom) && !(post.originalCreatedBefore) |
|
|
|
|
|
? '不明' |
|
|
|
|
|
: ( |
|
|
|
|
|
<> |
|
|
|
|
|
{post.originalCreatedFrom |
|
|
|
|
|
&& `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} |
|
|
|
|
|
{post.originalCreatedBefore |
|
|
|
|
|
&& `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} |
|
|
|
|
|
</>)} |
|
|
|
|
|
</li> |
|
|
|
|
|
</ul> |
|
|
|
|
|
</div>)} |
|
|
|
|
|
</motion.div> |
|
|
|
|
|
</DndContext> |
|
|
</SidebarComponent>) |
|
|
</SidebarComponent>) |
|
|
}) satisfies FC<Props> |
|
|
}) satisfies FC<Props> |