feat: add tweet detail page

This commit is contained in:
2026-07-14 00:50:08 +09:00
parent 8e6ff70d81
commit da72094e07
9 changed files with 272 additions and 30 deletions
+58 -25
View File
@@ -1,12 +1,33 @@
import { useEffect, useRef } from 'react'
import { flattenPostPages } from '../page'
import {
flattenConversationPages,
flattenPostPages,
focalPostFromPages,
} from '../page'
import type { Post } from '../types'
import { type FeedRequest, PostLoadError, usePostFeed } from '../use-post-feed'
import { PostCard } from './post-card'
function FocalPost({ post }: { post: Post }) {
return (
<section aria-labelledby="focal-post-label" className="focal-post">
<p className="focal-label" id="focal-post-label">
表示中の投稿
</p>
<PostCard current post={post} />
</section>
)
}
export function PostFeed({ request }: { request: FeedRequest | undefined }) {
const query = usePostFeed(request)
const sentinel = useRef<HTMLDivElement>(null)
const posts = query.data ? flattenPostPages(query.data.pages) : []
const pages = query.data?.pages ?? []
const focalPost =
request?.kind === 'thread' ? focalPostFromPages(pages) : undefined
const posts = focalPost
? flattenConversationPages(pages, focalPost.id)
: flattenPostPages(pages)
useEffect(() => {
if (
@@ -62,31 +83,43 @@ export function PostFeed({ request }: { request: FeedRequest | undefined }) {
)
}
if (posts.length === 0 && !query.hasNextPage) {
return <p className="state">条件に一致する投稿はありません。</p>
return (
<>
{focalPost ? <FocalPost post={focalPost} /> : null}
<p className="state">
{focalPost
? '会話にほかの投稿はありません。'
: '条件に一致する投稿はありません。'}
</p>
</>
)
}
return (
<section aria-live="polite" className="feed">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
<div aria-hidden="true" ref={sentinel} />
{query.isFetchingNextPage ? (
<div className="loading-rail" role="status">
次の投稿を取得しています…
</div>
) : null}
{query.isFetchNextPageError ? (
<div className="state" role="alert">
<p>続きの投稿を取得できませんでした。</p>
<button onClick={() => void query.fetchNextPage()} type="button">
再試行
</button>
</div>
) : null}
{!query.hasNextPage ? (
<p className="state">これ以上の投稿はありません。</p>
) : null}
</section>
<>
{focalPost ? <FocalPost post={focalPost} /> : null}
<section aria-live="polite" className="feed">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
<div aria-hidden="true" ref={sentinel} />
{query.isFetchingNextPage ? (
<div className="loading-rail" role="status">
次の投稿を取得しています…
</div>
) : null}
{query.isFetchNextPageError ? (
<div className="state" role="alert">
<p>続きの投稿を取得できませんでした。</p>
<button onClick={() => void query.fetchNextPage()} type="button">
再試行
</button>
</div>
) : null}
{!query.hasNextPage ? (
<p className="state">これ以上の投稿はありません。</p>
) : null}
</section>
</>
)
}