101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Client body for the public collection page (/c/[id]). Split out from
|
|
* page.tsx so the page itself can stay a server component that exports
|
|
* `generateMetadata` (rich OG card on link shares). Anyone can view a
|
|
* public collection; private ones return 403 from the BFF, which we treat
|
|
* as "not found" to avoid leaking existence.
|
|
*/
|
|
|
|
import { use, useEffect, useState } from "react";
|
|
import { notFound } from "next/navigation";
|
|
import { Globe, Lock, FolderPlus } from "lucide-react";
|
|
import {
|
|
collections,
|
|
SocialError,
|
|
type Collection,
|
|
type Post,
|
|
} from "@/lib/social";
|
|
import { PostViews } from "@/components/social/post/post-views";
|
|
|
|
export function CollectionContent(props: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = use(props.params);
|
|
const [c, setC] = useState<Collection | null>(null);
|
|
const [posts, setPosts] = useState<Post[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [missing, setMissing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
collections
|
|
.get(id)
|
|
.then(({ collection, posts }) => {
|
|
if (cancelled) return;
|
|
setC(collection);
|
|
setPosts(posts ?? []);
|
|
})
|
|
.catch((err) => {
|
|
if (
|
|
err instanceof SocialError &&
|
|
(err.status === 404 || err.status === 403)
|
|
) {
|
|
setMissing(true);
|
|
}
|
|
})
|
|
.finally(() => !cancelled && setLoading(false));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [id]);
|
|
|
|
if (missing) notFound();
|
|
if (loading || !c)
|
|
return (
|
|
<main className="flex-1 max-w-3xl w-full mx-auto px-4 py-12">
|
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
</main>
|
|
);
|
|
|
|
return (
|
|
<main className="flex-1 max-w-3xl w-full mx-auto px-4 sm:px-6 py-10 space-y-8">
|
|
<header className="rounded-2xl bg-card ring-1 ring-foreground/10 p-6 sm:p-8">
|
|
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
{c.is_public ? (
|
|
<Globe className="size-3.5" />
|
|
) : (
|
|
<Lock className="size-3.5" />
|
|
)}
|
|
{c.is_public ? "Public collection" : "Private collection"}
|
|
</div>
|
|
<h1 className="mt-3 font-[family-name:var(--font-title)] font-semibold text-3xl sm:text-4xl text-foreground leading-tight">
|
|
{c.title}
|
|
</h1>
|
|
{c.description ? (
|
|
<p className="mt-3 text-base text-muted-foreground leading-relaxed">
|
|
{c.description}
|
|
</p>
|
|
) : null}
|
|
<p className="mt-4 text-xs text-muted-foreground tabular-nums">
|
|
{c.item_count} post{c.item_count === 1 ? "" : "s"}
|
|
</p>
|
|
</header>
|
|
|
|
<section className="space-y-3">
|
|
{posts.length === 0 ? (
|
|
<div className="rounded-xl border border-dashed border-border p-8 text-center">
|
|
<FolderPlus className="size-6 text-muted-foreground mx-auto" />
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
This collection has no items yet.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<PostViews posts={posts} />
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|