diff --git a/src/app/page.tsx b/src/app/page.tsx index 64d7c4c5..aa0cf9cd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,9 +2,8 @@ import { createServerClient, type Storyline } from "../../lib/supabase"; import { STORY_FACTORY } from "../../lib/contracts/constants"; import { getTrendingStorylines, getRisingStorylines } from "../../lib/ranking"; import { StoryCard } from "../components/StoryCard"; -import { SortDropdown } from "../components/SortDropdown"; -import { WriterFilter, type WriterFilterValue } from "../components/WriterFilter"; -import { GenreFilter, LanguageFilter } from "../components/GenreLanguageFilter"; +import { FilterBar } from "../components/FilterBar"; +import { type WriterFilterValue } from "../components/WriterFilter"; import { GENRES, LANGUAGES } from "../../lib/genres"; import Link from "next/link"; @@ -42,8 +41,6 @@ export default async function Home({ storylines = await queryTab(supabase, tab, writer, page, genre, lang); } - const extraParams = writer !== "all" ? { writer } : undefined; - return (
{/* Compact hero */} @@ -57,12 +54,7 @@ export default async function Home({ {/* Filter bar */} -
- - - - -
+ {/* Story grid */}
diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx new file mode 100644 index 00000000..e3d521d3 --- /dev/null +++ b/src/components/FilterBar.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { GENRES, LANGUAGES } from "../../lib/genres"; + +const WRITER_OPTIONS = ["All", "Human", "AI"] as const; +const SORT_OPTIONS = [ + { value: "new", label: "Recent" }, + { value: "trending", label: "Trending" }, + { value: "rising", label: "Rising" }, + { value: "completed", label: "Completed" }, +] as const; + +type FilterKey = "writer" | "genre" | "lang" | "sort"; + +interface FilterBarProps { + writer: string; + genre: string; + lang: string; + tab: string; +} + +function buildHref(params: { tab: string; writer: string; genre: string; lang: string }) { + const sp = new URLSearchParams({ tab: params.tab }); + if (params.writer !== "all") sp.set("writer", params.writer); + if (params.genre !== "all") sp.set("genre", params.genre); + if (params.lang !== "all") sp.set("lang", params.lang); + return `/?${sp.toString()}`; +} + +function writerDisplay(v: string) { + if (v === "agent") return "AI"; + return v.charAt(0).toUpperCase() + v.slice(1); +} + +function sortLabel(tab: string) { + return SORT_OPTIONS.find((o) => o.value === tab)?.label ?? "Recent"; +} + +export function FilterBar({ writer, genre, lang, tab }: FilterBarProps) { + const [open, setOpen] = useState(null); + const barRef = useRef(null); + const router = useRouter(); + + useEffect(() => { + function handleClick(e: MouseEvent) { + if (barRef.current && !barRef.current.contains(e.target as Node)) { + setOpen(null); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, []); + + function toggle(key: FilterKey) { + setOpen(open === key ? null : key); + } + + function navigate(params: { tab: string; writer: string; genre: string; lang: string }) { + setOpen(null); + router.push(buildHref(params)); + } + + return ( +
+
+ {/* Writer token + dropdown */} +
+ toggle("writer")} + /> + {open === "writer" && ( + + {WRITER_OPTIONS.map((opt) => { + const val = opt.toLowerCase() === "ai" ? "agent" : opt.toLowerCase(); + return ( + navigate({ tab, writer: val, genre, lang })} + /> + ); + })} + + )} +
+ + {/* Genre token + dropdown */} +
+ toggle("genre")} + /> + {open === "genre" && ( + + navigate({ tab, writer, genre: "all", lang })} + /> + {GENRES.map((g) => ( + navigate({ tab, writer, genre: g, lang })} + /> + ))} + + )} +
+ + {/* Language token + dropdown */} +
+ toggle("lang")} + /> + {open === "lang" && ( + + navigate({ tab, writer, genre, lang: "all" })} + /> + {LANGUAGES.map((l) => ( + navigate({ tab, writer, genre, lang: l })} + /> + ))} + + )} +
+ + {/* Spacer */} +
+ + {/* Sort token + dropdown */} +
+ + {open === "sort" && ( + + {SORT_OPTIONS.map(({ value, label }) => ( + navigate({ tab: value, writer, genre, lang })} + /> + ))} + + )} +
+
+
+ ); +} + +function Token({ + label, + value, + active, + onClick, +}: { + label: string; + value: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +function Dropdown({ children, align }: { children: React.ReactNode; align?: "right" }) { + return ( +
+ {children} +
+ ); +} + +function DropdownItem({ + label, + active, + onClick, +}: { + label: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +}