feat: add policy pages
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* /u/me/collections/[id] — single-collection view.
|
||||
*
|
||||
* The whole page is the collection's CollectionScrollCard (carousel of
|
||||
* cover + posts) and an Edit button below it. All editing — title,
|
||||
* description, cover, member posts, delete — lives in the EditCollectionDrawer
|
||||
* so the page itself stays focused on the carousel preview.
|
||||
*/
|
||||
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import {
|
||||
collections,
|
||||
SocialError,
|
||||
type Collection,
|
||||
type Post,
|
||||
} from "@/lib/social";
|
||||
import { CollectionScrollCard } from "@/components/social/collection-scroll-card";
|
||||
import { EditCollectionDrawer } from "@/components/social/edit-collection-drawer";
|
||||
|
||||
export default function CollectionDetailPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = use(props.params);
|
||||
const router = useRouter();
|
||||
|
||||
const [c, setC] = useState<Collection | null>(null);
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
collections
|
||||
.get(id)
|
||||
.then(({ collection, posts }) => {
|
||||
if (cancelled) return;
|
||||
setC(collection);
|
||||
setPosts(posts ?? []);
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(
|
||||
err instanceof SocialError ? err.message : "Could not load collection.",
|
||||
);
|
||||
})
|
||||
.finally(() => !cancelled && setLoading(false));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading…</p>;
|
||||
if (error && !c) return <p className="text-sm text-destructive">{error}</p>;
|
||||
if (!c) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/u/me/collections")}
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" /> All collections
|
||||
</button>
|
||||
|
||||
<CollectionScrollCard
|
||||
collection={c}
|
||||
href={`/u/me/collections/${c.id}`}
|
||||
initialPosts={posts}
|
||||
onEdit={() => setEditOpen(true)}
|
||||
/>
|
||||
|
||||
<EditCollectionDrawer
|
||||
open={editOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
collection={c}
|
||||
currentPosts={posts}
|
||||
onSaved={({ collection, posts: rows }) => {
|
||||
setC(collection);
|
||||
setPosts(rows);
|
||||
}}
|
||||
onDeleted={() => router.push("/u/me/collections")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { collections, SocialError, type Collection } from "@/lib/social";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { CollectionViews } from "@/components/social/collection-views";
|
||||
|
||||
export default function MyCollectionsPage() {
|
||||
const { user } = useAuth();
|
||||
const [list, setList] = useState<Collection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Inline create form
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [isPublic, setIsPublic] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
collections
|
||||
.mine()
|
||||
.then((r) => setList(r ?? []))
|
||||
.catch((err) =>
|
||||
setError(
|
||||
err instanceof SocialError ? err.message : "Could not load collections.",
|
||||
),
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function createOne(e: React.SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
if (!title.trim()) return;
|
||||
setCreating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const c = await collections.create({
|
||||
title,
|
||||
description: description || undefined,
|
||||
is_public: isPublic,
|
||||
});
|
||||
setList((rows) => [c, ...rows]);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setShowCreate(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof SocialError ? err.message : "Could not create.");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
const isPremium = user?.plan === "premium";
|
||||
const publicCount = list.filter((c) => c.is_public).length;
|
||||
const privateCount = list.filter((c) => !c.is_public).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isPremium ? (
|
||||
<>Premium — unlimited collections.</>
|
||||
) : (
|
||||
<>
|
||||
<span className="tabular-nums">{publicCount}</span> / 5 public ·{" "}
|
||||
<span className="tabular-nums">{privateCount}</span> / 5 private
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{!showCreate ? (
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-primary text-primary-foreground px-3 h-10 text-sm font-medium"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
New
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showCreate ? (
|
||||
<form
|
||||
onSubmit={createOne}
|
||||
className="rounded-xl bg-card ring-1 ring-foreground/10 p-4 space-y-3"
|
||||
>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Collection title"
|
||||
maxLength={120}
|
||||
required
|
||||
className="w-full bg-background text-foreground text-base rounded-md border border-border px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
/>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Description (optional)"
|
||||
rows={2}
|
||||
className="w-full bg-background text-foreground text-sm rounded-md border border-border px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 resize-none"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isPublic}
|
||||
onChange={(e) => setIsPublic(e.target.checked)}
|
||||
/>
|
||||
<span className="text-foreground">Public</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowCreate(false);
|
||||
setTitle("");
|
||||
}}
|
||||
className="px-3 h-9 rounded-md text-muted-foreground hover:bg-muted"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || !title.trim()}
|
||||
className="px-3 h-9 rounded-md bg-primary text-primary-foreground disabled:opacity-60"
|
||||
>
|
||||
{creating ? "Creating…" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||
) : (
|
||||
<CollectionViews
|
||||
collections={list}
|
||||
hrefFor={(c) => `/u/me/collections/${c.id}`}
|
||||
emptyMessage="No collections yet — create one above, or save a quote into a new collection from the selection toolbar."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user