diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx
new file mode 100644
index 00000000..8a5a1d02
--- /dev/null
+++ b/src/app/discover/page.tsx
@@ -0,0 +1,82 @@
+import { createServerClient, type Storyline } from "../../../lib/supabase";
+import { StoryCard } from "../../components/StoryCard";
+import { TabNav } from "../../components/TabNav";
+
+type SearchParams = Promise<{ tab?: string }>;
+
+const TABS = ["new", "trending", "rising", "completed"] as const;
+type Tab = (typeof TABS)[number];
+
+export default async function DiscoverPage({
+ searchParams,
+}: {
+ searchParams: SearchParams;
+}) {
+ const { tab: rawTab } = await searchParams;
+ const tab: Tab = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : "new";
+
+ const supabase = createServerClient();
+ if (!supabase) {
+ return (
+
+ );
+ }
+
+ let storylines: Storyline[] = [];
+
+ if (tab === "completed") {
+ const { data } = await supabase
+ .from("storylines")
+ .select("*")
+ .eq("hidden", false)
+ .eq("sunset", true)
+ .order("plot_count", { ascending: false })
+ .limit(50)
+ .returns();
+ storylines = data ?? [];
+ } else {
+ // "new" is the default; "trending" and "rising" fall back to "new" ordering
+ // until trading data is available (Phase 5)
+ const { data } = await supabase
+ .from("storylines")
+ .select("*")
+ .eq("hidden", false)
+ .eq("sunset", false)
+ .order("block_timestamp", { ascending: false })
+ .limit(50)
+ .returns();
+ storylines = data ?? [];
+ }
+
+ return (
+
+
+ Discover
+
+
+ Browse stories on PlotLink
+
+
+
+
+ {(tab === "trending" || tab === "rising") && (
+
+ Ranking by recency — trading-based ranking available after Phase 5.
+
+ )}
+
+
+ {storylines.map((s) => (
+
+ ))}
+ {storylines.length === 0 && (
+
+ No stories found.
+
+ )}
+
+
+ );
+}
diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx
new file mode 100644
index 00000000..ef18a37f
--- /dev/null
+++ b/src/components/StoryCard.tsx
@@ -0,0 +1,47 @@
+import Link from "next/link";
+import type { Storyline } from "../../lib/supabase";
+
+function truncateAddress(address: string): string {
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
+export function StoryCard({
+ storyline,
+ genre,
+}: {
+ storyline: Storyline;
+ genre?: string;
+}) {
+ return (
+
+
+
+ {storyline.title}
+
+ {storyline.sunset && (
+ complete
+ )}
+
+
+ {truncateAddress(storyline.writer_address)}
+
+ {storyline.plot_count}{" "}
+ {storyline.plot_count === 1 ? "plot" : "plots"}
+
+ {genre && (
+
+ {genre}
+
+ )}
+ {storyline.writer_type === 1 && (
+
+ agent
+
+ )}
+
+
+ );
+}
diff --git a/src/components/TabNav.tsx b/src/components/TabNav.tsx
new file mode 100644
index 00000000..caf3f55b
--- /dev/null
+++ b/src/components/TabNav.tsx
@@ -0,0 +1,29 @@
+import Link from "next/link";
+
+export function TabNav({
+ tabs,
+ active,
+ className,
+}: {
+ tabs: readonly string[];
+ active: string;
+ className?: string;
+}) {
+ return (
+
+ );
+}