Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 9 additions & 114 deletions src/app/discover/page.tsx
Original file line number Diff line number Diff line change
@@ -1,122 +1,17 @@
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";
import { redirect } from "next/navigation";

export const revalidate = 120; // ISR: regenerate at most every 2 minutes
type SearchParams = Promise<Record<string, string | string[] | undefined>>;

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({
export default async function DiscoverRedirect({
searchParams,
}: {
searchParams: 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) {
return (
<div className="flex min-h-[calc(100vh-2.75rem)] items-center justify-center">
<p className="text-muted text-sm">Database unavailable</p>
</div>
);
}

const storylines = await queryTab(supabase, tab, writer);

const extraParams =
writer !== "all" ? { writer } : undefined;

return (
<div className="mx-auto max-w-2xl px-6 py-12">
<h1 className="text-accent text-2xl font-bold tracking-tight">
Discover
</h1>
<p className="text-muted mt-2 text-sm">Browse stories on PlotLink</p>

<TabNav
tabs={TABS}
active={tab}
className="mt-6"
extraParams={extraParams}
/>

<WriterFilter active={writer} tab={tab} className="mt-4" />

<div className="mt-6 space-y-3">
{storylines.map((s) => (
<StoryCard key={s.id} storyline={s} />
))}
{storylines.length === 0 && (
<p className="text-muted py-8 text-center text-sm">
No stories found.
</p>
)}
</div>
</div>
);
}

async function queryTab(
supabase: ReturnType<typeof createServerClient> & object,
tab: Tab,
writer: WriterFilterValue,
): Promise<Storyline[]> {
switch (tab) {
case "new": {
let q = supabase
.from("storylines")
.select("*")
.eq("hidden", 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<Storyline[]>();
return data ?? [];
}

case "completed": {
let q = supabase
.from("storylines")
.select("*")
.eq("hidden", false)
.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<Storyline[]>();
return data ?? [];
}

case "trending": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getTrendingStorylines(supabase, 20, wt);
}

case "rising": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getRisingStorylines(supabase, 20, wt);
}
const params = await searchParams;
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (typeof v === "string") qs.set(k, v);
}
const query = qs.toString();
redirect(query ? `/?${query}` : "/");
}
147 changes: 88 additions & 59 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { createServerClient, type Storyline } from "../../lib/supabase";
import { getTrendingStorylines } from "../../lib/ranking";
import { getTrendingStorylines, getRisingStorylines } from "../../lib/ranking";
import { StoryCard } from "../components/StoryCard";
import { TabNav } from "../components/TabNav";
import { WriterFilter, type WriterFilterValue } from "../components/WriterFilter";
import Link from "next/link";

export const revalidate = 120;

export default async function Home() {
const supabase = createServerClient();
const TABS = ["new", "trending", "rising", "completed"] as const;
type Tab = (typeof TABS)[number];

const WRITER_VALUES: WriterFilterValue[] = ["all", "human", "agent"];

let recent: Storyline[] = [];
let trending: Storyline[] = [];
type SearchParams = Promise<{ tab?: string; writer?: string }>;

export default async function Home({
searchParams,
}: {
searchParams: 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();

let storylines: Storyline[] = [];
if (supabase) {
const { data } = await supabase
.from("storylines")
.select("*")
.eq("hidden", false)
.eq("sunset", false)
.order("block_timestamp", { ascending: false })
.limit(8)
.returns<Storyline[]>();

recent = data ?? [];
trending = await getTrendingStorylines(supabase, 4).catch(() => []);
storylines = await queryTab(supabase, tab, writer);
}

const hasContent = recent.length > 0;
const extraParams = writer !== "all" ? { writer } : undefined;

return (
<div className="mx-auto max-w-2xl px-6 py-10">
<div className="mx-auto max-w-5xl px-6 py-10">
{/* Compact hero */}
<header className="mb-8">
<h1 className="text-accent text-xl font-bold tracking-tight">
Expand All @@ -39,48 +48,20 @@ export default async function Home() {
</p>
</header>

{hasContent ? (
<>
{/* Trending section */}
{trending.length > 0 && (
<section className="mb-8">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-foreground text-sm font-medium">
<span className="text-accent-dim mr-1">#</span>trending
</h2>
</div>
<div className="space-y-2">
{trending.map((s) => (
<StoryCard key={s.id} storyline={s} />
))}
</div>
</section>
)}

{/* Recent feed */}
<section>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-foreground text-sm font-medium">
<span className="text-accent-dim mr-1">#</span>recent
</h2>
<Link
href="/discover"
className="text-muted hover:text-accent text-xs transition-colors"
>
view all →
</Link>
</div>
<div className="space-y-2">
{recent.map((s) => (
<StoryCard key={s.id} storyline={s} />
))}
</div>
</section>
</>
) : (
/* Empty state */
{/* Filter bar */}
<TabNav tabs={TABS} active={tab} basePath="/" extraParams={extraParams} />
<WriterFilter active={writer} tab={tab} basePath="/" className="mt-4" />

{/* Story grid */}
<div className="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{storylines.map((s) => (
<StoryCard key={s.id} storyline={s} />
))}
</div>

{storylines.length === 0 && (
<section className="flex flex-col items-center gap-4 py-16 text-center">
<div className="border-border rounded border px-4 py-3 text-xs text-muted">
<div className="border-border text-muted rounded border px-4 py-3 text-xs">
<span className="text-accent-dim">$</span> no storylines found
</div>
<p className="text-muted text-sm">
Expand All @@ -97,3 +78,51 @@ export default async function Home() {
</div>
);
}

async function queryTab(
supabase: ReturnType<typeof createServerClient> & object,
tab: Tab,
writer: WriterFilterValue,
): Promise<Storyline[]> {
switch (tab) {
case "new": {
let q = supabase
.from("storylines")
.select("*")
.eq("hidden", 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<Storyline[]>();
return data ?? [];
}

case "completed": {
let q = supabase
.from("storylines")
.select("*")
.eq("hidden", false)
.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<Storyline[]>();
return data ?? [];
}

case "trending": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getTrendingStorylines(supabase, 20, wt);
}

case "rising": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getRisingStorylines(supabase, 20, wt);
}
}
}
1 change: 0 additions & 1 deletion src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { usePathname } from "next/navigation";
import { ConnectWallet } from "./ConnectWallet";

const NAV_LINKS = [
{ href: "/discover", label: "discover" },
{ href: "/create", label: "create" },
{ href: "/dashboard/writer", label: "writer" },
{ href: "/dashboard/reader", label: "reader" },
Expand Down
4 changes: 3 additions & 1 deletion src/components/TabNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export function TabNav({
tabs,
active,
className,
basePath = "/",
extraParams,
}: {
tabs: readonly string[];
active: string;
className?: string;
basePath?: string;
extraParams?: Record<string, string>;
}) {
function buildHref(tab: string) {
Expand All @@ -18,7 +20,7 @@ export function TabNav({
params.set(k, v);
}
}
return `/discover?${params.toString()}`;
return `${basePath}?${params.toString()}`;
}

return (
Expand Down
4 changes: 3 additions & 1 deletion src/components/WriterFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ export function WriterFilter({
active,
tab,
className,
basePath = "/",
}: {
active: WriterFilterValue;
tab: string;
className?: string;
basePath?: string;
}) {
return (
<div className={`flex items-center gap-1 ${className ?? ""}`}>
<span className="text-muted mr-1 text-xs">Writer:</span>
{WRITER_OPTIONS.map(({ value, label }) => (
<Link
key={value}
href={`/discover?tab=${tab}&writer=${value}`}
href={`${basePath}?tab=${tab}&writer=${value}`}
className={`rounded px-2 py-1 text-xs transition-colors ${
value === active
? "bg-accent/10 text-accent font-medium"
Expand Down
Loading