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
40 changes: 40 additions & 0 deletions lib/genres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const GENRES = [
"Romance",
"Fantasy",
"Science Fiction",
"Mystery",
"Thriller",
"Horror",
"Adventure",
"Historical Fiction",
"Contemporary Lit",
"Humor",
"Poetry",
"Non-Fiction",
"Fanfiction",
"Short Story",
"Paranormal",
"Werewolf",
"LGBTQ+",
"New Adult",
"Teen Fiction",
"Diverse Lit",
"Others",
] as const;

export const LANGUAGES = [
"English",
"Chinese",
"Korean",
"Japanese",
"Spanish",
"French",
"Hindi",
"Arabic",
"Portuguese",
"Russian",
"Others",
] as const;

export type Genre = (typeof GENRES)[number];
export type Language = (typeof LANGUAGES)[number];
17 changes: 14 additions & 3 deletions lib/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,21 @@ function computeTrendScore(
}

/** Shared: fetch storyline candidates + batch ratings */
async function fetchCandidatesAndRatings(supabase: SupabaseClient<Database>, writerType?: number) {
async function fetchCandidatesAndRatings(
supabase: SupabaseClient<Database>,
writerType?: number,
genre?: string,
lang?: string,
) {
let q = supabase.from("storylines")
.select("*")
.eq("hidden", false)
.eq("sunset", false)
.neq("token_address", "")
.eq("contract_address", STORY_FACTORY.toLowerCase());
if (writerType !== undefined) q = q.eq("writer_type", writerType);
if (genre) q = q.eq("genre", genre);
if (lang) q = q.eq("language", lang);
const { data } = await q
.order("block_timestamp", { ascending: false })
.limit(50);
Expand Down Expand Up @@ -120,8 +127,10 @@ export async function getTrendingStorylines(
limit = 20,
writerType?: number,
offset = 0,
genre?: string,
lang?: string,
): Promise<RankedStoryline[]> {
const { storylines, ratingMap } = await fetchCandidatesAndRatings(supabase, writerType);
const { storylines, ratingMap } = await fetchCandidatesAndRatings(supabase, writerType, genre, lang);
if (storylines.length === 0) return [];

const enriched = await Promise.all(
Expand Down Expand Up @@ -160,8 +169,10 @@ export async function getRisingStorylines(
limit = 20,
writerType?: number,
offset = 0,
genre?: string,
lang?: string,
): Promise<RankedStoryline[]> {
const { storylines } = await fetchCandidatesAndRatings(supabase, writerType);
const { storylines } = await fetchCandidatesAndRatings(supabase, writerType, genre, lang);
if (storylines.length === 0) return [];

const now = new Date();
Expand Down
6 changes: 6 additions & 0 deletions lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface Database {
indexed_at: string;
view_count: number;
contract_address: string;
genre: string | null;
language: string;
};
Insert: {
id?: never;
Expand All @@ -81,6 +83,8 @@ export interface Database {
indexed_at?: string;
view_count?: number;
contract_address: string;
genre?: string | null;
language?: string;
};
Update: {
id?: never;
Expand All @@ -100,6 +104,8 @@ export interface Database {
indexed_at?: string;
view_count?: number;
contract_address?: string;
genre?: string | null;
language?: string;
};
Relationships: [];
};
Expand Down
7 changes: 7 additions & 0 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { STORY_FACTORY } from "../../../../../lib/contracts/constants";
import { detectWriterType } from "../../../../../lib/contracts/erc8004";
import { hashContent } from "../../../../../lib/content";
import { GENRES, LANGUAGES } from "../../../../../lib/genres";
import type { Database } from "../../../../../lib/supabase";

const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/";
Expand All @@ -28,6 +29,10 @@ export async function POST(req: Request) {
const body = await req.json();
const txHash = body.txHash as Hex | undefined;
const fallbackContent = body.content as string | undefined;
const rawGenre = body.genre as string | undefined;
const rawLanguage = body.language as string | undefined;
const genre = rawGenre && (GENRES as readonly string[]).includes(rawGenre) ? rawGenre : null;
const language = rawLanguage && (LANGUAGES as readonly string[]).includes(rawLanguage) ? rawLanguage : "English";

if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
return error("Missing or invalid txHash");
Expand Down Expand Up @@ -139,6 +144,8 @@ export async function POST(req: Request) {
tx_hash: txHash.toLowerCase(),
log_index: storylineLog.logIndex!,
contract_address: STORY_FACTORY.toLowerCase(),
genre,
language,
};

const { error: dbError } = await supabase.from("storylines").upsert(
Expand Down
38 changes: 37 additions & 1 deletion src/app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { decodeEventLog, encodeEventTopics } from "viem";
import Link from "next/link";
import { ConnectWallet } from "../../components/ConnectWallet";
import { GENRES, LANGUAGES } from "../../../lib/genres";

const STORYLINE_CREATED_TOPIC = encodeEventTopics({
abi: [storylineCreatedEvent],
Expand All @@ -32,15 +33,18 @@
export default function CreateStorylinePage() {
const { isConnected } = useAccount();
const [title, setTitle] = useState("");
const [genre, setGenre] = useState("");
const [language, setLanguage] = useState("English");
const [content, setContent] = useState("");
const hasDeadline = true; // mandatory 7-day deadline for all storylines

const { state, error, receipt, execute, reset } = usePublish();

Check warning on line 41 in src/app/create/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'reset' is assigned a value but never used
const { valid, charCount } = validateContentLength(content);
const titleValid = title.trim().length > 0;
const genreValid = genre.length > 0;
const canSubmit =
state === "idle" || state === "error"
? titleValid && valid
? titleValid && genreValid && valid
: false;

if (!isConnected) {
Expand Down Expand Up @@ -119,6 +123,7 @@
args: [title.trim(), cid, contentHash, hasDeadline],
gas: BigInt(16_000_000),
}),
metadata: { genre, language },
});
}}
className="mt-8 space-y-6"
Expand All @@ -136,6 +141,37 @@
/>
</div>

{/* Genre + Language */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-foreground mb-2 block text-sm">Genre</label>
<select
value={genre}
onChange={(e) => setGenre(e.target.value)}
disabled={busy}
className="border-border bg-surface text-foreground w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50"
>
<option value="">Select genre...</option>
{GENRES.map((g) => (
<option key={g} value={g}>{g}</option>
))}
</select>
</div>
<div>
<label className="text-foreground mb-2 block text-sm">Language</label>
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
disabled={busy}
className="border-border bg-surface text-foreground w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50"
>
{LANGUAGES.map((l) => (
<option key={l} value={l}>{l}</option>
))}
</select>
</div>
</div>

{/* Content */}
<div>
<label className="text-foreground mb-2 block text-sm">
Expand Down
41 changes: 31 additions & 10 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { StoryCard } from "../components/StoryCard";
import { SortDropdown } from "../components/SortDropdown";
import { WriterFilter, type WriterFilterValue } from "../components/WriterFilter";
import { GenreFilter, LanguageFilter } from "../components/GenreLanguageFilter";
import { GENRES, LANGUAGES } from "../../lib/genres";
import Link from "next/link";

export const revalidate = 120;
Expand All @@ -15,28 +17,30 @@

const PAGE_SIZE = 24;

type SearchParams = Promise<{ tab?: string; writer?: string; page?: string }>;
type SearchParams = Promise<{ tab?: string; writer?: string; page?: string; genre?: string; lang?: string }>;

export default async function Home({
searchParams,
}: {
searchParams: SearchParams;
}) {
const { tab: rawTab, writer: rawWriter, page: rawPage } = await searchParams;
const { tab: rawTab, writer: rawWriter, page: rawPage, genre: rawGenre, lang: rawLang } = 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 page = Math.max(1, parseInt(rawPage ?? "1", 10) || 1);
const genre = rawGenre && (GENRES as readonly string[]).includes(rawGenre) ? rawGenre : "all";
const lang = rawLang && (LANGUAGES as readonly string[]).includes(rawLang) ? rawLang : "all";

const supabase = createServerClient();

let storylines: Storyline[] = [];
const previews: Record<number, string> = {};
if (supabase) {
storylines = await queryTab(supabase, tab, writer, page);
storylines = await queryTab(supabase, tab, writer, page, genre, lang);
// Fetch genesis plot previews
if (storylines.length > 0) {
const { data: plots } = await supabase.from("plots")
Expand All @@ -52,7 +56,7 @@
}
}

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

Check warning on line 59 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'extraParams' is assigned a value but never used

return (
<div className="mx-auto max-w-5xl px-6 py-10">
Expand All @@ -68,7 +72,11 @@

{/* Filter bar */}
<div className="flex flex-wrap items-center justify-between gap-3">
<WriterFilter active={writer} tab={tab} basePath="/" />
<div className="flex flex-wrap items-center gap-3">
<WriterFilter active={writer} tab={tab} basePath="/" />
<GenreFilter active={genre} tab={tab} writer={writer} lang={lang} />
<LanguageFilter active={lang} tab={tab} writer={writer} genre={genre} />
</div>
<SortDropdown active={tab} writer={writer} basePath="/" />
</div>

Expand Down Expand Up @@ -134,10 +142,21 @@
tab: Tab,
writer: WriterFilterValue,
page: number,
genre: string,
lang: string,
): Promise<Storyline[]> {
const from = (page - 1) * PAGE_SIZE;
const to = from + PAGE_SIZE - 1;

function applyFilters(q: ReturnType<typeof supabase.from>) {
let filtered = q;
if (writer === "human") filtered = filtered.eq("writer_type", 0);
if (writer === "agent") filtered = filtered.eq("writer_type", 1);
if (genre !== "all") filtered = filtered.eq("genre", genre);
if (lang !== "all") filtered = filtered.eq("language", lang);
return filtered;
}

switch (tab) {
case "new": {
let q = supabase
Expand All @@ -146,8 +165,7 @@
.eq("hidden", false)
.eq("sunset", false)
.eq("contract_address", STORY_FACTORY.toLowerCase());
if (writer === "human") q = q.eq("writer_type", 0);
if (writer === "agent") q = q.eq("writer_type", 1);
q = applyFilters(q);
const { data } = await q
.order("block_timestamp", { ascending: false })
.range(from, to)
Expand All @@ -162,8 +180,7 @@
.eq("hidden", false)
.eq("sunset", true)
.eq("contract_address", STORY_FACTORY.toLowerCase());
if (writer === "human") q = q.eq("writer_type", 0);
if (writer === "agent") q = q.eq("writer_type", 1);
q = applyFilters(q);
const { data } = await q
.order("plot_count", { ascending: false })
.range(from, to)
Expand All @@ -173,12 +190,16 @@

case "trending": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getTrendingStorylines(supabase, PAGE_SIZE, wt, from);
const g = genre !== "all" ? genre : undefined;
const l = lang !== "all" ? lang : undefined;
return getTrendingStorylines(supabase, PAGE_SIZE, wt, from, g, l);
}

case "rising": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
return getRisingStorylines(supabase, PAGE_SIZE, wt, from);
const g = genre !== "all" ? genre : undefined;
const l = lang !== "all" ? lang : undefined;
return getRisingStorylines(supabase, PAGE_SIZE, wt, from, g, l);
}
}
}
10 changes: 10 additions & 0 deletions src/app/story/[storylineId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ function StoryHeader({
{storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"}
</span>
<ViewCount storylineId={storyline.storyline_id} initialCount={storyline.view_count} />
{storyline.genre && (
<span className="border-border rounded border px-1.5 py-0.5 text-[10px]">
{storyline.genre}
</span>
)}
{storyline.language && storyline.language !== "English" && (
<span className="border-border rounded border px-1.5 py-0.5 text-[10px]">
{storyline.language}
</span>
)}
{storyline.writer_type === 1 && <AgentBadge />}
<RatingSummary storylineId={storyline.storyline_id} />
</div>
Expand Down
45 changes: 45 additions & 0 deletions src/components/GenreLanguageFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { GENRES, LANGUAGES } from "../../lib/genres";

export function GenreFilter({ active, tab, writer, lang }: { active: string; tab: string; writer: string; lang: string }) {
return (
<select
defaultValue={active}
onChange={(e) => {
const params = new URLSearchParams({ tab });
if (writer !== "all") params.set("writer", writer);
if (e.target.value !== "all") params.set("genre", e.target.value);
if (lang !== "all") params.set("lang", lang);
window.location.href = `/?${params.toString()}`;
}}
className="border-border bg-surface text-muted rounded border px-2 py-1 text-xs"
>
<option value="all">All genres</option>
{GENRES.map((g) => (
<option key={g} value={g}>{g}</option>
))}
</select>
);
}

export function LanguageFilter({ active, tab, writer, genre }: { active: string; tab: string; writer: string; genre: string }) {
return (
<select
defaultValue={active}
onChange={(e) => {
const params = new URLSearchParams({ tab });
if (writer !== "all") params.set("writer", writer);
if (genre !== "all") params.set("genre", genre);
if (e.target.value !== "all") params.set("lang", e.target.value);
window.location.href = `/?${params.toString()}`;
}}
className="border-border bg-surface text-muted rounded border px-2 py-1 text-xs"
>
<option value="all">All languages</option>
{LANGUAGES.map((l) => (
<option key={l} value={l}>{l}</option>
))}
</select>
);
}
Loading
Loading