From ed4bc8886ee91c837ab1ad6c85b6949d204c08b1 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 13:08:42 +0000 Subject: [PATCH 1/2] [#33] Add writer type filter and agent badge Fixes #33 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/discover/page.tsx | 65 +++++++++++++++++++++++----- src/app/story/[storylineId]/page.tsx | 7 +-- src/components/AgentBadge.tsx | 9 ++++ src/components/StoryCard.tsx | 7 +-- src/components/TabNav.tsx | 14 +++++- src/components/WriterFilter.tsx | 38 ++++++++++++++++ 6 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 src/components/AgentBadge.tsx create mode 100644 src/components/WriterFilter.tsx diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index aa7696fb..d52190e2 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -2,21 +2,32 @@ import { createServerClient, type Storyline } from "../../../lib/supabase"; import { getTrendingStorylines, getRisingStorylines } from "../../../lib/ranking"; import { StoryCard } from "../../components/StoryCard"; import { TabNav } from "../../components/TabNav"; +import { + WriterFilter, + type WriterFilterValue, +} from "../../components/WriterFilter"; export const revalidate = 120; // ISR: regenerate at most every 2 minutes -type SearchParams = Promise<{ tab?: string }>; +type SearchParams = Promise<{ tab?: string; writer?: string }>; const TABS = ["new", "trending", "rising", "completed"] as const; type Tab = (typeof TABS)[number]; +const WRITER_VALUES: WriterFilterValue[] = ["all", "human", "agent"]; + export default async function DiscoverPage({ searchParams, }: { searchParams: SearchParams; }) { - const { tab: rawTab } = await searchParams; + const { tab: rawTab, writer: rawWriter } = await searchParams; const tab: Tab = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : "new"; + const writer: WriterFilterValue = WRITER_VALUES.includes( + rawWriter as WriterFilterValue, + ) + ? (rawWriter as WriterFilterValue) + : "all"; const supabase = createServerClient(); if (!supabase) { @@ -27,7 +38,10 @@ export default async function DiscoverPage({ ); } - const storylines = await queryTab(supabase, tab); + const storylines = await queryTab(supabase, tab, writer); + + const extraParams = + writer !== "all" ? { writer } : undefined; return (
@@ -36,7 +50,14 @@ export default async function DiscoverPage({

Browse stories on PlotLink

- + + +
{storylines.map((s) => ( @@ -55,14 +76,18 @@ export default async function DiscoverPage({ async function queryTab( supabase: ReturnType & object, tab: Tab, + writer: WriterFilterValue, ): Promise { switch (tab) { case "new": { - const { data } = await supabase + let q = supabase .from("storylines") .select("*") .eq("hidden", false) - .eq("sunset", false) + .eq("sunset", false); + if (writer === "human") q = q.eq("writer_type", 0); + if (writer === "agent") q = q.eq("writer_type", 1); + const { data } = await q .order("block_timestamp", { ascending: false }) .limit(50) .returns(); @@ -70,11 +95,14 @@ async function queryTab( } case "completed": { - const { data } = await supabase + let q = supabase .from("storylines") .select("*") .eq("hidden", false) - .eq("sunset", true) + .eq("sunset", true); + if (writer === "human") q = q.eq("writer_type", 0); + if (writer === "agent") q = q.eq("writer_type", 1); + const { data } = await q .order("plot_count", { ascending: false }) .limit(50) .returns(); @@ -82,11 +110,28 @@ async function queryTab( } case "trending": { - return getTrendingStorylines(supabase, 20); + const all = await getTrendingStorylines(supabase, 50); + return filterByWriter(all, writer).slice(0, 20); } case "rising": { - return getRisingStorylines(supabase, 20); + const all = await getRisingStorylines(supabase, 50); + return filterByWriter(all, writer).slice(0, 20); } } } + +/** Client-side filter for pre-ranked results (trending/rising) */ +function filterByWriter( + storylines: Storyline[], + writer: WriterFilterValue, +): Storyline[] { + switch (writer) { + case "human": + return storylines.filter((s) => s.writer_type === 0); + case "agent": + return storylines.filter((s) => s.writer_type === 1); + default: + return storylines; + } +} diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index af0165cf..18ae918f 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -9,6 +9,7 @@ import { getTokenPrice, type TokenPriceInfo } from "../../../../lib/price"; import { IS_TESTNET } from "../../../../lib/contracts/constants"; import { type Address } from "viem"; import { truncateAddress } from "../../../../lib/utils"; +import { AgentBadge } from "../../../components/AgentBadge"; type Params = Promise<{ storylineId: string }>; @@ -115,11 +116,7 @@ function StoryHeader({ {storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"} - {storyline.writer_type === 1 && ( - - agent - - )} + {storyline.writer_type === 1 && }
diff --git a/src/components/AgentBadge.tsx b/src/components/AgentBadge.tsx new file mode 100644 index 00000000..3f8135b0 --- /dev/null +++ b/src/components/AgentBadge.tsx @@ -0,0 +1,9 @@ +export function AgentBadge({ className }: { className?: string }) { + return ( + + agent + + ); +} diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index 3566e354..0c6857c4 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import type { Storyline } from "../../lib/supabase"; import { truncateAddress } from "../../lib/utils"; +import { AgentBadge } from "./AgentBadge"; export function StoryCard({ storyline, @@ -33,11 +34,7 @@ export function StoryCard({ {genre} )} - {storyline.writer_type === 1 && ( - - agent - - )} + {storyline.writer_type === 1 && }
); diff --git a/src/components/TabNav.tsx b/src/components/TabNav.tsx index caf3f55b..e9883577 100644 --- a/src/components/TabNav.tsx +++ b/src/components/TabNav.tsx @@ -4,17 +4,29 @@ export function TabNav({ tabs, active, className, + extraParams, }: { tabs: readonly string[]; active: string; className?: string; + extraParams?: Record; }) { + function buildHref(tab: string) { + const params = new URLSearchParams({ tab }); + if (extraParams) { + for (const [k, v] of Object.entries(extraParams)) { + params.set(k, v); + } + } + return `/discover?${params.toString()}`; + } + return (