| @@ -109,7 +109,7 @@ class PostsController < ApplicationController | |||
| render json: PostRepr.base(post, current_user) | |||
| .merge(tags: build_tag_tree_for(post.tags), | |||
| related: post.related(limit: 20)) | |||
| related: PostRepr.many(post.related(limit: 20))) | |||
| end | |||
| def create | |||
| @@ -37,18 +37,26 @@ class Post < ApplicationRecord | |||
| def parent_posts = parents | |||
| def child_posts = children | |||
| def sibling_posts | |||
| parent_post_ids = parent_posts.order(:id).pluck(:id) | |||
| parent_post_ids.to_h { [_1, PostImplication.where(parent_post: _1).map(&:post)] } | |||
| end | |||
| def as_json options = { } | |||
| super(options).merge({ thumbnail: thumbnail.attached? ? | |||
| Rails.application.routes.url_helpers.rails_blob_url( | |||
| thumbnail, only_path: false) : | |||
| nil }) | |||
| super(options).merge(thumbnail: thumbnail.attached? ? | |||
| Rails.application.routes.url_helpers.rails_blob_url( | |||
| thumbnail, only_path: false) : | |||
| nil) | |||
| rescue | |||
| super(options).merge(thumbnail: nil) | |||
| end | |||
| def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') | |||
| def snapshot_parent_post_ids = parents.order(:parent_post_id).pulck(:parent_post_id) | |||
| def snapshot_parent_post_ids = parents.order(:parent_post_id).pluck(:parent_post_id) | |||
| def related limit: nil | |||
| ids = post_similarities.order(cos: :desc) | |||
| @@ -3,7 +3,7 @@ | |||
| module PostRepr | |||
| BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE }, | |||
| methods: [:parent_posts] }.freeze | |||
| methods: [:parent_posts, :child_posts, :sibling_posts] }.freeze | |||
| module_function | |||
| @@ -4,6 +4,7 @@ import PostFormTagsArea from '@/components/PostFormTagsArea' | |||
| import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' | |||
| import Label from '@/components/common/Label' | |||
| import { Button } from '@/components/ui/button' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { apiPut } from '@/lib/api' | |||
| import type { FC } from 'react' | |||
| @@ -35,20 +36,30 @@ export default (({ post, onSave }: Props) => { | |||
| useState<string | null> (post.originalCreatedBefore) | |||
| const [originalCreatedFrom, setOriginalCreatedFrom] = | |||
| useState<string | null> (post.originalCreatedFrom) | |||
| const [title, setTitle] = useState (post.title) | |||
| const [parentPostIds, setParentPostIds] = useState (post.parentPosts!.map (p => p.id).join (' ')) | |||
| const [tags, setTags] = useState<string> ('') | |||
| const [title, setTitle] = useState (post.title) | |||
| const handleSubmit = async () => { | |||
| const data = await apiPut<Post> ( | |||
| `/posts/${ post.id }`, | |||
| { title, tags, original_created_from: originalCreatedFrom, | |||
| original_created_before: originalCreatedBefore }, | |||
| { headers: { 'Content-Type': 'multipart/form-data' } }) | |||
| onSave ({ ...post, | |||
| title: data.title, | |||
| tags: data.tags, | |||
| originalCreatedFrom: data.originalCreatedFrom, | |||
| originalCreatedBefore: data.originalCreatedBefore } as Post) | |||
| try | |||
| { | |||
| const data = await apiPut<Post> ( | |||
| `/posts/${ post.id }`, | |||
| { title, tags, parent_post_ids: parentPostIds, | |||
| original_created_from: originalCreatedFrom, | |||
| original_created_before: originalCreatedBefore }, | |||
| { headers: { 'Content-Type': 'multipart/form-data' } }) | |||
| onSave ({ ...post, | |||
| title: data.title, | |||
| tags: data.tags, | |||
| originalCreatedFrom: data.originalCreatedFrom, | |||
| originalCreatedBefore: data.originalCreatedBefore } as Post) | |||
| toast ({ description: '更新しました.' }) | |||
| } | |||
| catch | |||
| { | |||
| toast ({ description: '更新はできなかったよ……' }) | |||
| } | |||
| } | |||
| useEffect (() => { | |||
| @@ -66,6 +77,16 @@ export default (({ post, onSave }: Props) => { | |||
| onChange={ev => setTitle (ev.target.value)}/> | |||
| </div> | |||
| {/* 親投稿 */} | |||
| <div> | |||
| <Label>親投稿</Label> | |||
| <input | |||
| type="text" | |||
| value={parentPostIds} | |||
| onChange={e => setParentPostIds (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* タグ */} | |||
| <PostFormTagsArea tags={tags} setTags={setTags}/> | |||
| @@ -3,6 +3,7 @@ import { useRef } from 'react' | |||
| import { useLocation } from 'react-router-dom' | |||
| import PrefetchLink from '@/components/PrefetchLink' | |||
| import { cn } from '@/lib/utils' | |||
| import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' | |||
| import type { FC, MouseEvent } from 'react' | |||
| @@ -39,8 +40,10 @@ export default (({ posts, onClick }: Props) => { | |||
| <motion.div | |||
| ref={cardRef} | |||
| layoutId={layoutId} | |||
| className="w-full h-full overflow-hidden rounded-xl shadow | |||
| transform-gpu will-change-transform" | |||
| className={cn ('w-full h-full overflow-hidden rounded-xl shadow', | |||
| 'transform-gpu will-change-transform', | |||
| (post.childPosts ?? []).length > 0 && 'border-4 border-green-500', | |||
| (post.parentPosts ?? []).length > 0 && 'border-4 border-yellow-500')} | |||
| whileHover={{ scale: 1.02 }} | |||
| onLayoutAnimationStart={() => { | |||
| if (!(cardRef.current)) | |||
| @@ -21,7 +21,7 @@ import ServiceUnavailable from '@/pages/ServiceUnavailable' | |||
| import type { FC } from 'react' | |||
| import type { NiconicoViewerHandle, User } from '@/types' | |||
| import type { NiconicoViewerHandle, Post, User } from '@/types' | |||
| type Props = { user: User | null } | |||
| @@ -108,7 +108,34 @@ export default (({ user }: Props) => { | |||
| {post | |||
| ? ( | |||
| <> | |||
| {/* TODO: 親投稿リスト */} | |||
| {(post.childPosts ?? []).length > 0 && ( | |||
| <div className="mb-4 bg-green-200 dark:bg-green-800 text-sm p-2 rounded-md"> | |||
| <p>この投稿には {post.childPosts!.length} 件の子投稿があります.</p> | |||
| <PostList posts={[{ ...post, childPosts: [{ } as Post] }, | |||
| ...post.childPosts!.map (p => ({ | |||
| ...p, parentPosts: [{ } as Post] }))]}/> | |||
| </div> | |||
| )} | |||
| {(post.parentPosts ?? []).map (pp => { | |||
| const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`] | |||
| if (!(siblings)) | |||
| return | |||
| return ( | |||
| <div | |||
| key={pp.id} | |||
| className="mb-4 bg-yellow-200 dark:bg-yellow-800 text-sm p-2 rounded-md"> | |||
| <p> | |||
| この投稿には 1 件の親投稿{ | |||
| siblings.length > 1 | |||
| && `と ${ siblings.length - 1 } 件の姉妹投稿`}があります. | |||
| </p> | |||
| <PostList posts={[{ ...pp, childPosts: [{ } as Post] }, | |||
| ...siblings.map (p => ({ | |||
| ...p, parentPosts: [{ } as Post] }))]}/> | |||
| </div>) | |||
| })} | |||
| {(post.thumbnail || post.thumbnailBase) && ( | |||
| <motion.div | |||
| layoutId={`page-${ id }`} | |||
| @@ -147,7 +174,6 @@ export default (({ user }: Props) => { | |||
| (prev: any) => newPost ?? prev) | |||
| qc.invalidateQueries ({ queryKey: postsKeys.root }) | |||
| qc.invalidateQueries ({ queryKey: tagsKeys.root }) | |||
| toast ({ description: '更新しました.' }) | |||
| }}/> | |||
| </Tab>)} | |||
| </TabGroup> | |||
| @@ -95,6 +95,8 @@ export default (() => { | |||
| <col className="w-96"/> | |||
| {/* タグ */} | |||
| <col className="w-[48rem]"/> | |||
| {/* 親投稿 */} | |||
| <col className="w-[48rem]"/> | |||
| {/* オリジナルの投稿日時 */} | |||
| <col className="w-96"/> | |||
| {/* 更新日時 */} | |||
| @@ -110,6 +112,7 @@ export default (() => { | |||
| <th className="p-2 text-left">タイトル</th> | |||
| <th className="p-2 text-left">URL</th> | |||
| <th className="p-2 text-left">タグ</th> | |||
| <th className="p-2 text-left">親投稿</th> | |||
| <th className="p-2 text-left">オリジナルの投稿日時</th> | |||
| <th className="p-2 text-left">更新日時</th> | |||
| <th className="p-2"/> | |||
| @@ -180,6 +183,28 @@ export default (() => { | |||
| {tag.name} | |||
| </span>))))} | |||
| </td> | |||
| <td className="p-2"> | |||
| {change.parentPosts.map ((pp, i) => ( | |||
| pp.type === 'added' | |||
| ? ( | |||
| <ins | |||
| key={i} | |||
| className="mr-2 text-green-600 dark:text-green-400"> | |||
| {pp.title} | |||
| </ins>) | |||
| : ( | |||
| pp.type === 'removed' | |||
| ? ( | |||
| <del | |||
| key={i} | |||
| className="mr-2 text-red-600 dark:text-red-400"> | |||
| {pp.title} | |||
| </del>) | |||
| : ( | |||
| <span key={i} className="mr-2"> | |||
| {pp.title} | |||
| </span>))))} | |||
| </td> | |||
| <td className="p-2"> | |||
| {change.versionNo === 1 | |||
| ? originalCreatedAtString (change.originalCreatedFrom.current, | |||
| @@ -225,6 +250,8 @@ export default (() => { | |||
| .map (t => t.name) | |||
| .filter (t => t.slice (0, 5) !== 'nico:') | |||
| .join (' '), | |||
| parent_post_ids: | |||
| change.parentPosts.map (p => p.id).join (' '), | |||
| original_created_from: | |||
| change.originalCreatedFrom.current, | |||
| original_created_before: | |||
| @@ -29,6 +29,7 @@ export default (({ user }: Props) => { | |||
| const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null) | |||
| const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null) | |||
| const [parentPostIds, setParentPostIds] = useState ('') | |||
| const [tags, setTags] = useState ('') | |||
| const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true) | |||
| const [thumbnailFile, setThumbnailFile] = useState<File | null> (null) | |||
| @@ -46,6 +47,7 @@ export default (({ user }: Props) => { | |||
| formData.append ('title', title) | |||
| formData.append ('url', url) | |||
| formData.append ('tags', tags) | |||
| formData.append ('parent_post_ids', parentPostIds) | |||
| if (thumbnailFile) | |||
| formData.append ('thumbnail', thumbnailFile) | |||
| if (originalCreatedFrom) | |||
| @@ -177,6 +179,16 @@ export default (({ user }: Props) => { | |||
| className="mt-2 max-h-48 rounded border"/>)} | |||
| </div> | |||
| {/* 親投稿 */} | |||
| <div> | |||
| <Label>親投稿</Label> | |||
| <input | |||
| type="text" | |||
| value={parentPostIds} | |||
| onChange={e => setParentPostIds (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* タグ */} | |||
| <PostFormTagsArea tags={tags} setTags={setTags}/> | |||
| @@ -121,6 +121,9 @@ export type Post = { | |||
| thumbnail: string | null | |||
| thumbnailBase: string | null | |||
| tags: Tag[] | |||
| parentPosts?: Post[] | |||
| childPosts?: Post[] | |||
| siblingPosts?: Record<`${ number }`, Post[]> | |||
| viewed: boolean | |||
| related: Post[] | |||
| originalCreatedFrom: string | null | |||
| @@ -144,7 +147,11 @@ export type PostVersion = { | |||
| url: { current: string; prev: string | null } | |||
| thumbnail: { current: string | null; prev: string | null } | |||
| thumbnailBase: { current: string | null; prev: string | null } | |||
| tags: { name: string; type: 'context' | 'added' | 'removed' }[] | |||
| tags: { name: string | |||
| type: 'context' | 'added' | 'removed' }[] | |||
| parentPosts: { id: number | |||
| title: string | |||
| type: 'context' | 'added' | 'removed' }[] | |||
| originalCreatedFrom: { current: string | null; prev: string | null } | |||
| originalCreatedBefore: { current: string | null; prev: string | null } | |||
| createdAt: string | |||