Visibility phase 5: public /explore directory + search #48
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
import Link from "next/link";
|
import { PublicHeader } from "@/components/public-header";
|
||||||
|
|
||||||
// Public viewing surface — no auth, no app sidebar. A slim header only.
|
// Public viewing surface — no auth, no app sidebar. A slim header only.
|
||||||
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<header className="border-b border-[var(--border)]">
|
<PublicHeader />
|
||||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
|
|
||||||
<Link href="/" aria-label="Provenance — home" className="flex items-center">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-6 w-auto" />
|
|
||||||
</Link>
|
|
||||||
<Link href="/login" className="text-sm text-bronze hover:underline">
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main className="mx-auto max-w-5xl px-4 py-8">{children}</main>
|
<main className="mx-auto max-w-5xl px-4 py-8">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Archive,
|
Archive,
|
||||||
ArrowDownUp,
|
ArrowDownUp,
|
||||||
BookText,
|
BookText,
|
||||||
|
Compass,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
LogOut,
|
LogOut,
|
||||||
@@ -92,6 +93,7 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Item href="/trees" label="Trees" icon={FolderTree} active={pathname === "/trees"} />
|
<Item href="/trees" label="Trees" icon={FolderTree} active={pathname === "/trees"} />
|
||||||
|
<Item href="/explore" label="Explore" icon={Compass} active={pathname === "/explore"} />
|
||||||
<Item href="/import" label="Import" icon={ArrowDownUp} active={pathname === "/import"} />
|
<Item href="/import" label="Import" icon={ArrowDownUp} active={pathname === "/import"} />
|
||||||
|
|
||||||
{treeId && (
|
{treeId && (
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
// Slim header for the public (no-auth) surface: /p/* and /explore.
|
||||||
|
export function PublicHeader() {
|
||||||
|
return (
|
||||||
|
<header className="border-b border-[var(--border)]">
|
||||||
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
|
||||||
|
<Link href="/" aria-label="Provenance — home" className="flex items-center">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-6 w-auto" />
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-4 text-sm">
|
||||||
|
<Link href="/explore" className="text-[var(--muted)] hover:text-[var(--foreground)]">
|
||||||
|
Explore
|
||||||
|
</Link>
|
||||||
|
<Link href="/login" className="text-bronze hover:underline">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user