Files
spsays-training/app/(app)/c/[id]/collection-content.tsx
T
2026-06-14 01:29:08 +05:30

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>
);
}