45 lines
3.4 KiB
TypeScript
45 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useState } from "react";
|
|
import type { SuggestionPage } from "@/lib/discord/suggestion-types";
|
|
import { SuggestionBadges, SuggestionFailure, SuggestionLoading, SuggestionPagination, suggestionControl, useSuggestionRead, useSuggestionPages, type SuggestionPaging } from "./suggestion-shared";
|
|
|
|
export function SuggestionsBrowser() {
|
|
const [status, setStatus] = useState<"active" | "archived">("active");
|
|
const paging = useSuggestionPages();
|
|
const query = new URLSearchParams({ status, limit: "25" });
|
|
if (paging.cursor) query.set("cursor", paging.cursor);
|
|
const url = `/api/suggestions?${query}`;
|
|
return <section aria-label="Suggestions" className="mt-10">
|
|
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
|
|
<div aria-label="Suggestion status" className="flex gap-2">
|
|
{(["active", "archived"] as const).map((value) => <button aria-pressed={status === value} className={`${suggestionControl} ${status === value ? "bg-ink text-panel" : "bg-panel"}`} key={value} onClick={() => { setStatus(value); paging.reset(); }} type="button">{value === "active" ? "Active" : "Archived"}</button>)}
|
|
</div>
|
|
<p className="font-mono text-[10px] uppercase tracking-wider text-muted">{status === "active" ? "Newest ideas first" : "Most recently archived first"}</p>
|
|
</div>
|
|
<SuggestionList key={url} paging={paging} url={url} />
|
|
</section>;
|
|
}
|
|
|
|
function SuggestionList({ url, paging }: { url: string; paging: SuggestionPaging }) {
|
|
const { data, error, retry } = useSuggestionRead<SuggestionPage>(url);
|
|
if (error) return <SuggestionFailure error={error} retry={retry} />;
|
|
if (!data) return <SuggestionLoading>Loading suggestions…</SuggestionLoading>;
|
|
return <><ol className="divide-y divide-line border border-line bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
|
|
{!data.items.length && <li className="p-10"><h2 className="font-display text-2xl font-black uppercase">No suggestions on this page.</h2><p className="mt-3 text-sm text-muted">Try the other status, or return to the previous page.</p></li>}
|
|
{data.items.map((suggestion, index) => <li className="grid gap-5 p-6 sm:grid-cols-[3rem_1fr_auto] sm:p-8" key={suggestion.id}>
|
|
<span aria-hidden="true" className="font-mono text-sm text-muted">{String(index + 1).padStart(2, "0")}</span>
|
|
<div className="min-w-0">
|
|
<SuggestionBadges suggestion={suggestion} />
|
|
<h2 className="mt-4 break-words font-display text-2xl font-black sm:text-3xl"><Link className="underline decoration-line underline-offset-4 hover:text-accent" href={`/admin/suggestions/${suggestion.id}`}>{suggestion.title}</Link></h2>
|
|
<p className="mt-3 break-words font-mono text-[10px] leading-5 text-muted"><time dateTime={suggestion.createdAt}>{suggestion.createdAt}</time><br />Author / {suggestion.authorId}</p>
|
|
</div>
|
|
<div className="flex items-end justify-between gap-6 sm:flex-col sm:items-end">
|
|
<p className="font-mono text-[10px] uppercase text-muted">~{suggestion.messageCount} messages</p>
|
|
<a aria-label={`Open ${suggestion.title} in Discord`} className="font-mono text-[10px] font-bold uppercase underline underline-offset-4 hover:text-accent" href={suggestion.discordUrl} rel="noopener noreferrer" target="_blank">Discord ↗</a>
|
|
</div>
|
|
</li>)}
|
|
</ol><SuggestionPagination nextCursor={data.nextCursor} {...paging} /></>;
|
|
}
|