diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 980a2e9..2bf91cf 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -100,23 +100,16 @@ class PostsController < ApplicationController .first return head :not_found unless post - viewed = current_user&.viewed?(post) || false - - render json: PostRepr.base(post).merge(viewed:) + render json: PostRepr.base(post, current_user) end def show post = Post.includes(tags: { tag_name: :wiki_page }).find_by(id: params[:id]) return head :not_found unless post - viewed = current_user&.viewed?(post) || false - - json = post.as_json - json['tags'] = build_tag_tree_for(post.tags) - json['related'] = post.related(limit: 20) - json['viewed'] = viewed - - render json: + render json: PostRepr.base(post, current_user) + .merge(tags: build_tag_tree_for(post.tags), + related: post.related(limit: 20)) end def create diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb index 438ccc1..2dc444f 100644 --- a/backend/app/representations/post_repr.rb +++ b/backend/app/representations/post_repr.rb @@ -2,15 +2,19 @@ module PostRepr - BASE = { include: { tags: TagRepr::BASE } }.freeze + BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE } }.freeze module_function - def base post - post.as_json(BASE) + def base post, current_user = nil + json = post.as_json(BASE) + return json unless current_user + + viewed = current_user.viewed?(post) + json.merge(viewed:) end - def many posts - posts.map { |p| base(p) } + def many posts, current_user = nil + posts.map { |p| base(p, current_user) } end end diff --git a/backend/app/representations/user_repr.rb b/backend/app/representations/user_repr.rb new file mode 100644 index 0000000..bbb7bec --- /dev/null +++ b/backend/app/representations/user_repr.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + + +module UserRepr + BASE = { only: [:id, :name] }.freeze + + module_function + + def base user + user.as_json(BASE) + end + + def many users + users.map { |u| base(u) } + end +end diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index af8d390..6660f7e 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -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 } + suppressClickRef: MutableRefObject + 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}> - + + + ) }) satisfies FC diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 1b995a1..8579442 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -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, parentTagId?: number, + sp?: boolean, ): ReactNode[] => { const key = `${ path }-${ tag.id }` const self = ( - +
  • - ) + suppressClickRef={suppressClickRef} + sp={sp}/> +
  • ) 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, sp }: Props) => { + sp = Boolean (sp) + + const baseTags = useMemo (() => { + 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]) -export default (({ post }: Props) => { const [activeTagId, setActiveTagId] = useState (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 => { - if (!(post)) - return - setTags (buildTagByCategory (await apiGet (`/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 ( @@ -305,60 +304,57 @@ export default (({ post }: Props) => { document.body.style.userSelect = '' }} modifiers={[restrictToWindowEdges]}> - - {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && ( - - {CATEGORY_NAMES[cat]} - - - - {(tags[cat] ?? []).flatMap (tag => ( - renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))} - - - - ))} - {post && ( -
    - 情報 -
      -
    • Id.: {post.id}
    • - {/* TODO: uploadedUser の取得を対応したらコメント外す */} - {/* -
    • - <>耕作者: - {post.uploadedUser - ? ( - - {post.uploadedUser.name || '名もなきニジラー'} - ) - : 'bot操作'} -
    • - */} -
    • 耕作日時: {dateString (post.createdAt)}
    • -
    • - <>リンク: - - {post.url} - -
    • -
    • - <>オリジナルの投稿日時: - {originalCreatedAtString (post.originalCreatedFrom, - post.originalCreatedBefore)} -
    • -
    • - - 履歴 - -
    • -
    -
    )} -
    + {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && ( +
    + + + {CATEGORY_NAMES[cat]} + + + +
      + {(tags[cat] ?? []).flatMap (tag => ( + renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined, sp)))} + +
    +
    ))} + {post && ( + + 情報 +
      +
    • Id.: {post.id}
    • +
    • + <>耕作者: + {post.uploadedUser + ? ( + + {post.uploadedUser.name || '名もなきニジラー'} + ) + : 'bot操作'} +
    • +
    • 耕作日時: {dateString (post.createdAt)}
    • +
    • + <>リンク: + + {post.url} + +
    • +
    • + <>オリジナルの投稿日時: + {originalCreatedAtString (post.originalCreatedFrom, + post.originalCreatedBefore)} +
    • +
    • + + 履歴 + +
    • +
    +
    )}
    diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index ae06196..a91966e 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -65,30 +65,31 @@ export default (({ posts, onClick }: Props) => { {CATEGORIES.flatMap (cat => cat in tags ? ( tags[cat].map (tag => (
  • - + + +
  • ))) : [])} 関聯 - {posts.length > 0 && ( - { - ev.preventDefault () - void ((async () => { - try - { - const data = await apiGet ('/posts/random', - { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), - match: (anyFlg ? 'any' : 'all') } }) - navigate (`/posts/${ data.id }`) - } - catch - { - ; - } - }) ()) - }}> - ランダム - )} + { + ev.preventDefault () + void ((async () => { + try + { + const data = await apiGet ('/posts/random', + { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), + match: (anyFlg ? 'any' : 'all') } }) + navigate (`/posts/${ data.id }`) + } + catch + { + ; + } + }) ()) + }}> + ランダム + ) return ( @@ -96,7 +97,7 @@ export default (({ posts, onClick }: Props) => {
    - {TagBlock} + {posts.length > 0 && TagBlock}
    @@ -112,7 +113,7 @@ export default (({ posts, onClick }: Props) => { animate="visible" exit="hidden" transition={{ duration: .2, ease: 'easeOut' }}> - {TagBlock} + {posts.length > 0 && TagBlock} )} diff --git a/frontend/src/components/common/Pagination.tsx b/frontend/src/components/common/Pagination.tsx index 53d045d..8e1b365 100644 --- a/frontend/src/components/common/Pagination.tsx +++ b/frontend/src/components/common/Pagination.tsx @@ -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 }) => {
    {(page > 1) ? ( - - < - ) - : <} + <> + + |< + + + < + + ) + : ( + <> + + |< + + + < + + )} {pages.map ((p, idx) => ( (p === '…') - ? + ? : ((p === page) ? {p} - : {p})))} + : ( + + {p} + ))))} {(page < totalPages) ? ( - - > - ) - : >} + <> + + > + + + >| + + ) + : ( + <> + > + >| + )}
    ) }) satisfies FC diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx index 828e09e..4c11c62 100644 --- a/frontend/src/pages/posts/PostDetailPage.tsx +++ b/frontend/src/pages/posts/PostDetailPage.tsx @@ -99,7 +99,7 @@ export default (({ user }: Props) => {
    - + {post && }
    @@ -149,7 +149,7 @@ export default (({ user }: Props) => {
    - + {post && }
    ) }) satisfies FC diff --git a/frontend/src/pages/tags/TagListPage.tsx b/frontend/src/pages/tags/TagListPage.tsx index 9059a30..fd9dc60 100644 --- a/frontend/src/pages/tags/TagListPage.tsx +++ b/frontend/src/pages/tags/TagListPage.tsx @@ -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') ?? '' diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 4dc8af5..441edf4 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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