Visibility phase 5: public /explore directory + search
A no-login directory of shared trees, backed by GET /api/v1/public/trees: - /explore: searchable grid of public trees; debounced name search. Because the backend adds `site_members` trees when a valid session is present, signed-in users see more with no client-side branching. - PublicHeader extracted and shared by /p and /explore (logo, Explore, Sign in). - "Explore" entry added to the authed sidebar. tsc clean; next build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { PublicHeader } from "@/components/public-header";
|
||||
|
||||
export default function ExploreLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<PublicHeader />
|
||||
<main className="mx-auto max-w-5xl px-4 py-8">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import type { components } from "@/lib/api/schema";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type Tree = components["schemas"]["PublicTreeRead"];
|
||||
|
||||
// Public directory of trees. The backend returns `public` to everyone and adds
|
||||
// `site_members` trees when the request carries a valid session — so signed-in
|
||||
// users see more here without any client-side branching.
|
||||
export default function ExplorePage() {
|
||||
const [trees, setTrees] = useState<Tree[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const q = search.trim();
|
||||
const t = setTimeout(async () => {
|
||||
const { data } = await api.GET("/api/v1/public/trees", {
|
||||
params: { query: q ? { q } : {} },
|
||||
});
|
||||
setTrees(data ?? []);
|
||||
setReady(true);
|
||||
}, 200);
|
||||
return () => clearTimeout(t);
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="font-serif text-3xl font-semibold">Explore public trees</h1>
|
||||
<p className="mt-1 text-[var(--muted)]">
|
||||
Browse family trees shared on this site. Living people are always hidden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className="w-72"
|
||||
placeholder="Search trees by name…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
{!ready ? (
|
||||
<p className="text-[var(--muted)]">Loading…</p>
|
||||
) : trees.length === 0 ? (
|
||||
<p className="text-[var(--muted)]">No public trees{search.trim() ? " match that search" : " yet"}.</p>
|
||||
) : (
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{trees.map((t) => (
|
||||
<li key={t.id}>
|
||||
<Link href={`/p/${t.id}`}>
|
||||
<Card className="h-full transition-colors hover:border-bronze/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate font-medium">{t.name}</span>
|
||||
<span className="shrink-0 text-xs uppercase tracking-wide text-[var(--muted)]">
|
||||
{t.visibility === "site_members" ? "Members" : "Public"}
|
||||
</span>
|
||||
</div>
|
||||
{t.description && (
|
||||
<p className="mt-1 line-clamp-2 text-sm text-[var(--muted)]">{t.description}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user