From ce00c6f7ac105c8f62e9b6018f2ef47452b859d5 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 14 Mar 2026 10:53:23 +0000 Subject: [PATCH 1/3] [#201] Build discovery page with tabs and reusable story card - Create /discover route with Trending, New, Rising, Completed tabs - New tab: active storylines sorted by creation time (default) - Completed tab: sunset storylines sorted by plot count - Trending/Rising: fall back to recency until trading data (Phase 5) - Create StoryCard component: title, truncated writer address, plot count, agent badge, sunset indicator, links to /story/[id] - Create TabNav component: reusable tab navigation with active state styling - Terminal aesthetic throughout Fixes #201 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/discover/page.tsx | 82 ++++++++++++++++++++++++++++++++++++ src/components/StoryCard.tsx | 36 ++++++++++++++++ src/components/TabNav.tsx | 29 +++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/app/discover/page.tsx create mode 100644 src/components/StoryCard.tsx create mode 100644 src/components/TabNav.tsx diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx new file mode 100644 index 00000000..2e743372 --- /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 ( +
+

Database unavailable

+
+ ); + } + + 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..b752662b --- /dev/null +++ b/src/components/StoryCard.tsx @@ -0,0 +1,36 @@ +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 }: { storyline: Storyline }) { + return ( + +
+

+ {storyline.title} +

+ {storyline.sunset && ( + complete + )} +
+
+ {truncateAddress(storyline.writer_address)} + + {storyline.plot_count}{" "} + {storyline.plot_count === 1 ? "plot" : "plots"} + + {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 ( + + ); +} From 245d54f0cccca00c1005164fcca1c54468773424 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 14 Mar 2026 10:54:44 +0000 Subject: [PATCH 2/3] [#201] Add genre tag prop to StoryCard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Genre tag is optional — renders when provided, ready to wire once the genre field is added to the storylines schema (writer-assigned at genesis). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/StoryCard.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index b752662b..ef18a37f 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -5,7 +5,13 @@ function truncateAddress(address: string): string { return `${address.slice(0, 6)}...${address.slice(-4)}`; } -export function StoryCard({ storyline }: { storyline: Storyline }) { +export function StoryCard({ + storyline, + genre, +}: { + storyline: Storyline; + genre?: string; +}) { return ( + {genre && ( + + {genre} + + )} {storyline.writer_type === 1 && ( agent From 9230dc419c95a4cd35846d8099458240b4448a8e Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 14 Mar 2026 10:55:51 +0000 Subject: [PATCH 3/3] [#201] Pass default genre tag to StoryCard in discovery page Default to "fiction" until genre field is added to storylines schema and writer-assigned at genesis. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/discover/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index 2e743372..8a5a1d02 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -69,7 +69,7 @@ export default async function DiscoverPage({
{storylines.map((s) => ( - + ))} {storylines.length === 0 && (