This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import TagLink from '@/components/TagLink'
|
||||
@@ -14,10 +15,11 @@ type Props = {
|
||||
nestLevel: number
|
||||
pathKey: string
|
||||
parentTagId?: number
|
||||
suppressClickRef: MutableRefObject<boolean> }
|
||||
suppressClickRef: MutableRefObject<boolean>
|
||||
sp?: boolean }
|
||||
|
||||
|
||||
export default (({ tag, nestLevel, pathKey, parentTagId, suppressClickRef }: Props) => {
|
||||
export default (({ tag, nestLevel, pathKey, parentTagId, suppressClickRef, sp }: Props) => {
|
||||
const dndId = `tag-node:${ pathKey }`
|
||||
|
||||
const downPosRef = useRef<{ x: number; y: number } | null> (null)
|
||||
@@ -88,6 +90,8 @@ export default (({ tag, nestLevel, pathKey, parentTagId, suppressClickRef }: Pro
|
||||
className={cn ('rounded select-none', over && 'ring-2 ring-offset-2')}
|
||||
{...attributes}
|
||||
{...listeners}>
|
||||
<TagLink tag={tag} nestLevel={nestLevel}/>
|
||||
<motion.div layoutId={`tag-${ sp ? 'sp-' : '' }${ tag.id }`}>
|
||||
<TagLink tag={tag} nestLevel={nestLevel}/>
|
||||
</motion.div>
|
||||
</div>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { DndContext,
|
||||
useSensor,
|
||||
useSensors } from '@dnd-kit/core'
|
||||
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
@@ -35,28 +35,27 @@ const renderTagTree = (
|
||||
path: string,
|
||||
suppressClickRef: MutableRefObject<boolean>,
|
||||
parentTagId?: number,
|
||||
sp?: boolean,
|
||||
): ReactNode[] => {
|
||||
const key = `${ path }-${ tag.id }`
|
||||
|
||||
const self = (
|
||||
<motion.li
|
||||
key={key}
|
||||
layout
|
||||
transition={{ duration: .2, ease: 'easeOut' }}
|
||||
className="mb-1">
|
||||
<li key={key} className="mb-1">
|
||||
<DraggableDroppableTagRow
|
||||
tag={tag}
|
||||
nestLevel={nestLevel}
|
||||
pathKey={key}
|
||||
parentTagId={parentTagId}
|
||||
suppressClickRef={suppressClickRef}/>
|
||||
</motion.li>)
|
||||
suppressClickRef={suppressClickRef}
|
||||
sp={sp}/>
|
||||
</li>)
|
||||
|
||||
return [
|
||||
self,
|
||||
...((tag.children
|
||||
?.sort ((a, b) => a.name < b.name ? -1 : 1)
|
||||
.flatMap (child => renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id)))
|
||||
.flatMap (child =>
|
||||
renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id, sp)))
|
||||
?? [])]
|
||||
}
|
||||
|
||||
@@ -147,14 +146,32 @@ const DropSlot = ({ cat }: { cat: Category }) => {
|
||||
}
|
||||
|
||||
|
||||
type Props = { post: Post | null }
|
||||
type Props = { post: Post; sp?: boolean }
|
||||
|
||||
|
||||
export default (({ post }: Props) => {
|
||||
export default (({ post, sp }: Props) => {
|
||||
sp = Boolean (sp)
|
||||
|
||||
const baseTags = useMemo<TagByCategory> (() => {
|
||||
const tagsTmp = { } as TagByCategory
|
||||
|
||||
for (const tag of post.tags)
|
||||
{
|
||||
if (!(tag.category in tagsTmp))
|
||||
tagsTmp[tag.category] = []
|
||||
tagsTmp[tag.category].push (tag)
|
||||
}
|
||||
|
||||
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
|
||||
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)
|
||||
|
||||
return tagsTmp
|
||||
}, [post])
|
||||
|
||||
const [activeTagId, setActiveTagId] = useState<number | null> (null)
|
||||
const [dragging, setDragging] = useState (false)
|
||||
const [saving, setSaving] = useState (false)
|
||||
const [tags, setTags] = useState ({ } as TagByCategory)
|
||||
const [tags, setTags] = useState (baseTags)
|
||||
|
||||
const suppressClickRef = useRef (false)
|
||||
|
||||
@@ -163,9 +180,6 @@ export default (({ post }: Props) => {
|
||||
useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }))
|
||||
|
||||
const reloadTags = async (): Promise<void> => {
|
||||
if (!(post))
|
||||
return
|
||||
|
||||
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
|
||||
}
|
||||
|
||||
@@ -256,23 +270,8 @@ export default (({ post }: Props) => {
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
if (!(post))
|
||||
return
|
||||
|
||||
const tagsTmp = { } as TagByCategory
|
||||
|
||||
for (const tag of post.tags)
|
||||
{
|
||||
if (!(tag.category in tagsTmp))
|
||||
tagsTmp[tag.category] = []
|
||||
tagsTmp[tag.category].push (tag)
|
||||
}
|
||||
|
||||
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
|
||||
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)
|
||||
|
||||
setTags (tagsTmp)
|
||||
}, [post])
|
||||
setTags (baseTags)
|
||||
}, [baseTags])
|
||||
|
||||
return (
|
||||
<SidebarComponent>
|
||||
@@ -305,60 +304,57 @@ export default (({ post }: Props) => {
|
||||
document.body.style.userSelect = ''
|
||||
}}
|
||||
modifiers={[restrictToWindowEdges]}>
|
||||
<motion.div key={post?.id ?? 0} layout>
|
||||
{CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
|
||||
<motion.div layout className="my-3" key={cat}>
|
||||
<SubsectionTitle>{CATEGORY_NAMES[cat]}</SubsectionTitle>
|
||||
{CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && (
|
||||
<div className="my-3" key={cat}>
|
||||
<SubsectionTitle>
|
||||
<motion.div layoutId={`tag-${ sp ? 'sp-' : '' }${ cat }`}>
|
||||
{CATEGORY_NAMES[cat]}
|
||||
</motion.div>
|
||||
</SubsectionTitle>
|
||||
|
||||
<motion.ul layout>
|
||||
<AnimatePresence initial={false}>
|
||||
{(tags[cat] ?? []).flatMap (tag => (
|
||||
renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))}
|
||||
<DropSlot cat={cat}/>
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</motion.div>))}
|
||||
{post && (
|
||||
<div>
|
||||
<SectionTitle>情報</SectionTitle>
|
||||
<ul>
|
||||
<li>Id.: {post.id}</li>
|
||||
{/* TODO: uploadedUser の取得を対応したらコメント外す */}
|
||||
{/*
|
||||
<li>
|
||||
<>耕作者: </>
|
||||
{post.uploadedUser
|
||||
? (
|
||||
<PrefetchLink to={`/users/${ post.uploadedUser.id }`}>
|
||||
{post.uploadedUser.name || '名もなきニジラー'}
|
||||
</PrefetchLink>)
|
||||
: 'bot操作'}
|
||||
</li>
|
||||
*/}
|
||||
<li>耕作日時: {dateString (post.createdAt)}</li>
|
||||
<li>
|
||||
<>リンク: </>
|
||||
<a
|
||||
className="break-all"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow">
|
||||
{post.url}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<>オリジナルの投稿日時: </>
|
||||
{originalCreatedAtString (post.originalCreatedFrom,
|
||||
post.originalCreatedBefore)}
|
||||
</li>
|
||||
<li>
|
||||
<PrefetchLink to={`/posts/changes?id=${ post.id }`}>
|
||||
履歴
|
||||
</PrefetchLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>)}
|
||||
</motion.div>
|
||||
<ul>
|
||||
{(tags[cat] ?? []).flatMap (tag => (
|
||||
renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined, sp)))}
|
||||
<DropSlot cat={cat}/>
|
||||
</ul>
|
||||
</div>))}
|
||||
{post && (
|
||||
<motion.div layoutId={`post-info-${ sp }`}>
|
||||
<SectionTitle>情報</SectionTitle>
|
||||
<ul>
|
||||
<li>Id.: {post.id}</li>
|
||||
<li>
|
||||
<>耕作者: </>
|
||||
{post.uploadedUser
|
||||
? (
|
||||
<PrefetchLink to={`/users/${ post.uploadedUser.id }`}>
|
||||
{post.uploadedUser.name || '名もなきニジラー'}
|
||||
</PrefetchLink>)
|
||||
: 'bot操作'}
|
||||
</li>
|
||||
<li>耕作日時: {dateString (post.createdAt)}</li>
|
||||
<li>
|
||||
<>リンク: </>
|
||||
<a
|
||||
className="break-all"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow">
|
||||
{post.url}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<>オリジナルの投稿日時: </>
|
||||
{originalCreatedAtString (post.originalCreatedFrom,
|
||||
post.originalCreatedBefore)}
|
||||
</li>
|
||||
<li>
|
||||
<PrefetchLink to={`/posts/changes?id=${ post.id }`}>
|
||||
履歴
|
||||
</PrefetchLink>
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>)}
|
||||
|
||||
<DragOverlay adjustScale={false}>
|
||||
<div className="pointer-events-none">
|
||||
|
||||
@@ -65,30 +65,31 @@ export default (({ posts, onClick }: Props) => {
|
||||
{CATEGORIES.flatMap (cat => cat in tags ? (
|
||||
tags[cat].map (tag => (
|
||||
<li key={tag.id} className="mb-1">
|
||||
<TagLink tag={tag} prefetch onClick={onClick}/>
|
||||
<motion.div layoutId={`tag-${ tag.id }`}>
|
||||
<TagLink tag={tag} prefetch onClick={onClick}/>
|
||||
</motion.div>
|
||||
</li>))) : [])}
|
||||
</ul>
|
||||
<SectionTitle>関聯</SectionTitle>
|
||||
{posts.length > 0 && (
|
||||
<a href="#"
|
||||
onClick={ev => {
|
||||
ev.preventDefault ()
|
||||
void ((async () => {
|
||||
try
|
||||
{
|
||||
const data = await apiGet<Post> ('/posts/random',
|
||||
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
||||
match: (anyFlg ? 'any' : 'all') } })
|
||||
navigate (`/posts/${ data.id }`)
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ())
|
||||
}}>
|
||||
ランダム
|
||||
</a>)}
|
||||
<a href="#"
|
||||
onClick={ev => {
|
||||
ev.preventDefault ()
|
||||
void ((async () => {
|
||||
try
|
||||
{
|
||||
const data = await apiGet<Post> ('/posts/random',
|
||||
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
||||
match: (anyFlg ? 'any' : 'all') } })
|
||||
navigate (`/posts/${ data.id }`)
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ())
|
||||
}}>
|
||||
ランダム
|
||||
</a>
|
||||
</>)
|
||||
|
||||
return (
|
||||
@@ -96,7 +97,7 @@ export default (({ posts, onClick }: Props) => {
|
||||
<TagSearch/>
|
||||
|
||||
<div className="hidden md:block mt-4">
|
||||
{TagBlock}
|
||||
{posts.length > 0 && TagBlock}
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
@@ -112,7 +113,7 @@ export default (({ posts, onClick }: Props) => {
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||
{TagBlock}
|
||||
{posts.length > 0 && TagBlock}
|
||||
</motion.div>)}
|
||||
</AnimatePresence>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const getPages = (
|
||||
}
|
||||
|
||||
|
||||
export default (({ page, totalPages, siblingCount = 2 }) => {
|
||||
export default (({ page, totalPages, siblingCount = 3 }) => {
|
||||
const location = useLocation ()
|
||||
|
||||
const buildTo = (p: number) => {
|
||||
@@ -64,30 +64,64 @@ export default (({ page, totalPages, siblingCount = 2 }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
{(page > 1)
|
||||
? (
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page - 1)}
|
||||
aria-label="前のページ">
|
||||
<
|
||||
</PrefetchLink>)
|
||||
: <span className="p-2" aria-hidden><</span>}
|
||||
<>
|
||||
<PrefetchLink
|
||||
className="md:hidden p-2"
|
||||
to={buildTo (1)}
|
||||
aria-label="最初のページ">
|
||||
|<
|
||||
</PrefetchLink>
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page - 1)}
|
||||
aria-label="前のページ">
|
||||
<
|
||||
</PrefetchLink>
|
||||
</>)
|
||||
: (
|
||||
<>
|
||||
<span className="md:hidden p-2" aria-hidden>
|
||||
|<
|
||||
</span>
|
||||
<span className="p-2" aria-hidden>
|
||||
<
|
||||
</span>
|
||||
</>)}
|
||||
|
||||
{pages.map ((p, idx) => (
|
||||
(p === '…')
|
||||
? <span key={`dots-${ idx }`} className="p-2">…</span>
|
||||
? <span key={`dots-${ idx }`} className="hidden md:block p-2">…</span>
|
||||
: ((p === page)
|
||||
? <span key={p} className="font-bold p-2" aria-current="page">{p}</span>
|
||||
: <PrefetchLink key={p} className="p-2" to={buildTo (p)}>{p}</PrefetchLink>)))}
|
||||
: (
|
||||
<PrefetchLink
|
||||
key={p}
|
||||
className="hidden md:block p-2"
|
||||
to={buildTo (p)}>
|
||||
{p}
|
||||
</PrefetchLink>))))}
|
||||
|
||||
{(page < totalPages)
|
||||
? (
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page + 1)}
|
||||
aria-label="次のページ">
|
||||
>
|
||||
</PrefetchLink>)
|
||||
: <span className="p-2" aria-hidden>></span>}
|
||||
<>
|
||||
<PrefetchLink
|
||||
className="p-2"
|
||||
to={buildTo (page + 1)}
|
||||
aria-label="次のページ">
|
||||
>
|
||||
</PrefetchLink>
|
||||
<PrefetchLink
|
||||
className="md:hidden p-2"
|
||||
to={buildTo (totalPages)}
|
||||
aria-label="最後のページ">
|
||||
>|
|
||||
</PrefetchLink>
|
||||
</>)
|
||||
: (
|
||||
<>
|
||||
<span className="p-2" aria-hidden>></span>
|
||||
<span className="md:hidden p-2" aria-hidden>>|</span>
|
||||
</>)}
|
||||
</div>
|
||||
</nav>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default (({ user }: Props) => {
|
||||
</Helmet>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
{post && <TagDetailSidebar post={post}/>}
|
||||
</div>
|
||||
|
||||
<MainArea className="relative">
|
||||
@@ -149,7 +149,7 @@ export default (({ user }: Props) => {
|
||||
</MainArea>
|
||||
|
||||
<div className="md:hidden">
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
{post && <TagDetailSidebar post={post} sp/>}
|
||||
</div>
|
||||
</div>)
|
||||
}) satisfies FC<Props>
|
||||
|
||||
@@ -41,8 +41,8 @@ export default (() => {
|
||||
const qName = query.get ('name') ?? ''
|
||||
const qCategory = (query.get ('category') || null) as Category | null
|
||||
const qPostCountGTE = Number (query.get ('post_count_gte') ?? 1)
|
||||
const qPostCountLTE =
|
||||
query.get ('post_count_lte') ? Number (query.get ('post_count_lte')) : null
|
||||
const qPostCountLTERaw = query.get ('post_count_lte')
|
||||
const qPostCountLTE = qPostCountLTERaw ? Number (qPostCountLTERaw) : null
|
||||
const qCreatedFrom = query.get ('created_from') ?? ''
|
||||
const qCreatedTo = query.get ('created_to') ?? ''
|
||||
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
||||
|
||||
@@ -73,7 +73,8 @@ export type Post = {
|
||||
originalCreatedFrom: string | null
|
||||
originalCreatedBefore: string | null
|
||||
createdAt: string
|
||||
updatedAt: string }
|
||||
updatedAt: string
|
||||
uploadedUser: { id: number; name: string } | null }
|
||||
|
||||
export type PostTagChange = {
|
||||
post: Post
|
||||
|
||||
Reference in New Issue
Block a user