Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { formatRingLabel } from "@/lib/forumRings";
|
|
import { displayScore, isPinnedThread, isReadOnlyThread, type ForumThread } from "@/lib/forumState";
|
|
import { ThreadVoteColumn } from "@/components/forum/ThreadVoteColumn";
|
|
|
|
export function ForumPostCard({
|
|
thread,
|
|
onVoteChange,
|
|
}: {
|
|
thread: ForumThread;
|
|
/** Fire when vote changes so list meta (score) re-renders from localStorage */
|
|
onVoteChange?: () => void;
|
|
}) {
|
|
const pinned = isPinnedThread(thread);
|
|
const locked = isReadOnlyThread(thread.id);
|
|
const score = displayScore(thread);
|
|
|
|
return (
|
|
<div
|
|
className={`flex gap-3 rounded-lg border bg-[#120e0a]/90 p-3 transition-colors ${
|
|
pinned
|
|
? "border-[#c45c26]/65 shadow-[0_0_0_1px_rgba(196,92,38,0.12)] hover:border-[#ff9a3c]/55"
|
|
: "border-[#3d3228] hover:border-[#c45c26]/45"
|
|
}`}
|
|
>
|
|
<div onClick={(e) => e.stopPropagation()} className="shrink-0">
|
|
<ThreadVoteColumn thread={thread} dense onVoteChange={onVoteChange} />
|
|
</div>
|
|
<Link href={`/forum/post/${thread.id}`} className="min-w-0 flex-1 outline-none">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{pinned ? (
|
|
<span className="rounded bg-[#c45c26]/35 px-2 py-0.5 font-mono text-[9px] font-black uppercase tracking-widest text-[#ffcb8a]">
|
|
Pinned
|
|
</span>
|
|
) : null}
|
|
{locked ? (
|
|
<span className="rounded border border-[#5c5044] px-2 py-0.5 font-mono text-[9px] uppercase tracking-wider text-[#8a7d6b]">
|
|
Staff
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<p className="mt-1 font-mono text-[10px] uppercase tracking-wider text-[#8a7d6b]">
|
|
<span className="text-[#ff9a3c]/90">{formatRingLabel(thread.topicSlug)}</span>
|
|
<span className="mx-2 text-[#3d3228]">·</span>
|
|
<span>{thread.author || "Anonymous"}</span>
|
|
<span className="mx-2 text-[#3d3228]">·</span>
|
|
<span>{formatAgo(thread.ts)}</span>
|
|
<span className="mx-2 text-[#3d3228]">·</span>
|
|
<span className="tabular-nums text-[#ffcb8a]/80">{score} pts</span>
|
|
</p>
|
|
<h3 className="mt-1 font-mono text-base font-bold leading-snug text-[#ffcb8a] hover:underline md:text-lg">
|
|
{thread.title}
|
|
</h3>
|
|
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-[#b8a995]">{thread.body}</p>
|
|
<div className="mt-3 flex flex-wrap gap-3 font-mono text-[11px] text-[#8a7d6b]">
|
|
<span className="rounded border border-[#3d3228] px-2 py-0.5">{thread.replies.length} comments</span>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatAgo(ts: number): string {
|
|
const s = Math.floor((Date.now() - ts) / 1000);
|
|
if (s < 60) return `${s}s ago`;
|
|
const m = Math.floor(s / 60);
|
|
if (m < 60) return `${m}m ago`;
|
|
const h = Math.floor(m / 60);
|
|
if (h < 48) return `${h}h ago`;
|
|
const d = Math.floor(h / 24);
|
|
return `${d}d ago`;
|
|
}
|