68 lines
4.8 KiB
TypeScript
68 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import type { MessagePage, SuggestionDetail, SuggestionMessage } from "@/lib/discord/suggestion-types";
|
|
import { SuggestionBadges, SuggestionFailure, SuggestionLoading, SuggestionPagination, suggestionControl, useSuggestionRead, useSuggestionPages, type SuggestionPaging } from "./suggestion-shared";
|
|
|
|
export function SuggestionReader({ id }: { id: string }) {
|
|
const { data, error, retry } = useSuggestionRead<SuggestionDetail>(`/api/suggestions/${id}`);
|
|
if (error) return <SuggestionFailure error={error} retry={retry} />;
|
|
if (!data) return <SuggestionLoading>Loading suggestion…</SuggestionLoading>;
|
|
return <>
|
|
<header className="border-b border-line pb-8">
|
|
<p className="mb-5 font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Community proposal / Read-only</p>
|
|
<SuggestionBadges suggestion={data} />
|
|
<h1 className="mt-5 break-words font-display text-4xl font-black sm:text-6xl">{data.title}</h1>
|
|
<div className="mt-6 flex flex-wrap items-center justify-between gap-5">
|
|
<p className="break-all font-mono text-[10px] leading-5 text-muted">Thread / {data.id}<br /><time dateTime={data.createdAt}>{data.createdAt}</time></p>
|
|
<a className={`${suggestionControl} bg-ink text-panel`} href={data.discordUrl} rel="noopener noreferrer" target="_blank">Open in Discord</a>
|
|
</div>
|
|
</header>
|
|
<section aria-labelledby="original-post-heading" className="mt-10 border border-ink bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
|
|
<div className="border-b border-line px-6 py-4"><h2 className="font-mono text-[10px] font-bold uppercase tracking-[0.2em]" id="original-post-heading">Original suggestion</h2></div>
|
|
{data.originalPost ? <MessageCard message={data.originalPost} /> : <p className="p-6 text-sm text-muted">The original post was deleted or is unavailable.</p>}
|
|
</section>
|
|
<Discussion id={id} />
|
|
<p className="mt-6 text-xs leading-5 text-muted">Discord remains the source of truth. Open Discord for attachments, formatting, replies, and reactions. Message text may be unavailable if the bot lacks Message Content access.</p>
|
|
</>;
|
|
}
|
|
|
|
function Discussion({ id }: { id: string }) {
|
|
const paging = useSuggestionPages();
|
|
const query = new URLSearchParams({ limit: "25" });
|
|
if (paging.cursor) query.set("cursor", paging.cursor);
|
|
const url = `/api/suggestions/${id}/messages?${query}`;
|
|
return <section aria-labelledby="discussion-heading" className="mt-12">
|
|
<header className="mb-5 flex flex-wrap items-end justify-between gap-3">
|
|
<h2 className="font-display text-3xl font-black uppercase" id="discussion-heading">Discussion</h2>
|
|
<p className="font-mono text-[10px] uppercase tracking-wider text-muted">Newest messages first / Read-only</p>
|
|
</header>
|
|
<DiscussionPage id={id} key={url} paging={paging} url={url} />
|
|
</section>;
|
|
}
|
|
|
|
function DiscussionPage({ id, url, paging }: { id: string; url: string; paging: SuggestionPaging }) {
|
|
const { data, error, retry } = useSuggestionRead<MessagePage>(url);
|
|
if (error) return <SuggestionFailure error={error} retry={retry} />;
|
|
if (!data) return <SuggestionLoading>Loading discussion…</SuggestionLoading>;
|
|
const replies = data.items.filter((message) => message.id !== id);
|
|
return <>
|
|
<div className="divide-y divide-line border border-line bg-panel">
|
|
{replies.map((message) => <MessageCard key={message.id} message={message} />)}
|
|
{!replies.length && <p className="p-8 text-sm text-muted">No replies on this page.</p>}
|
|
</div>
|
|
<SuggestionPagination nextCursor={data.nextCursor} {...paging} />
|
|
</>;
|
|
}
|
|
|
|
function MessageCard({ message }: { message: SuggestionMessage }) {
|
|
return <article className="min-w-0 p-6 sm:p-8">
|
|
<header className="flex flex-wrap items-baseline justify-between gap-3">
|
|
<h3 className="break-words font-display text-lg font-black">{message.author.name}</h3>
|
|
<a aria-label={`Open message by ${message.author.name} in Discord`} className="font-mono text-[10px] text-muted underline underline-offset-4 hover:text-accent" href={message.discordUrl} rel="noopener noreferrer" target="_blank"><time dateTime={message.createdAt}>{message.createdAt}</time></a>
|
|
</header>
|
|
<p className="mt-5 whitespace-pre-wrap break-words text-sm leading-7 [overflow-wrap:anywhere]">{message.content || "No text was returned. View this message in Discord."}</p>
|
|
{message.editedAt && <p className="mt-3 font-mono text-[10px] text-muted">Edited <time dateTime={message.editedAt}>{message.editedAt}</time></p>}
|
|
{!!message.reactions.length && <ul aria-label="Reaction counts" className="mt-5 flex flex-wrap gap-2">{message.reactions.map((reaction, index) => <li className="border border-line bg-canvas px-3 py-1 font-mono text-xs" key={`${reaction.emoji}-${index}`}>{reaction.emoji} {reaction.count}</li>)}</ul>}
|
|
</article>;
|
|
}
|