diff --git a/lib/ranking.ts b/lib/ranking.ts index f2a9f434..8657d85f 100644 --- a/lib/ranking.ts +++ b/lib/ranking.ts @@ -1,5 +1,6 @@ import { type Address, formatUnits } from "viem"; -import { get24hPriceChange, getTokenTVL } from "./price"; +import { get24hPriceChange, getTokenTVL, getBatchTokenData } from "./price"; +import { getPlotUsdPrice } from "./usd-price"; import { STORY_FACTORY } from "./contracts/constants"; import type { Database, Storyline, User } from "./supabase"; import type { SupabaseClient } from "@supabase/supabase-js"; @@ -282,3 +283,62 @@ export async function getTrendingStorylines( return enriched.slice(offset, offset + limit); } +/** + * Fetch storylines sorted by market cap (descending), active-first. + * MCap = totalSupply × pricePerToken × PLOT/USD rate. + */ +export async function getMcapStorylines( + supabase: SupabaseClient, + limit = 20, + writerType?: number, + offset = 0, + genre?: string, + lang?: string, +): Promise { + // Fetch all eligible stories — MCap needs the full set, not a recency-biased subset + let q = supabase + .from("storylines") + .select("*") + .eq("hidden", 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.returns(); + const storylines = data ?? []; + if (storylines.length === 0) return []; + + const tokenAddresses = storylines + .map((sl) => sl.token_address) + .filter((addr): addr is string => !!addr) as Address[]; + + const [batchData, plotUsd] = await Promise.all([ + getBatchTokenData(tokenAddresses), + getPlotUsdPrice(), + ]); + + const usdRate = plotUsd ?? 0; + + const withMcap = storylines.map((sl) => { + const data = batchData.get(sl.token_address?.toLowerCase() ?? ""); + let mcap = 0; + if (data?.price) { + const supply = Number(data.price.totalSupply); + const price = Number(data.price.pricePerToken); + mcap = supply * price * usdRate; + } + return { ...sl, mcap }; + }); + + // Active-first, then by MCap descending + withMcap.sort((a, b) => { + const aActive = getStoryStatus(a) === "active" ? 0 : 1; + const bActive = getStoryStatus(b) === "active" ? 0 : 1; + if (aActive !== bActive) return aActive - bActive; + return b.mcap - a.mcap; + }); + + return withMcap.slice(offset, offset + limit); +} + diff --git a/package-lock.json b/package-lock.json index 5def62a9..1c62f448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "0.1.48", + "version": "0.1.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "0.1.48", + "version": "0.1.49", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index 24c88500..f5e679d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.48", + "version": "0.1.49", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/page.tsx b/src/app/page.tsx index 12f5197a..393d5438 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,12 @@ import { createServerClient, type Storyline } from "../../lib/supabase"; import { STORY_FACTORY } from "../../lib/contracts/constants"; -import { getTrendingStorylines } from "../../lib/ranking"; +import { getTrendingStorylines, getMcapStorylines } from "../../lib/ranking"; import { StoryGrid } from "../components/StoryGrid"; import { FilterBar, type WriterFilterValue } from "../components/FilterBar"; import { GENRES, LANGUAGES } from "../../lib/genres"; import Link from "next/link"; -const TABS = ["new", "trending"] as const; +const TABS = ["new", "trending", "mcap"] as const; type Tab = (typeof TABS)[number]; const WRITER_VALUES: WriterFilterValue[] = ["all", "human", "agent"]; @@ -148,5 +148,11 @@ async function queryTab( return getTrendingStorylines(supabase, PAGE_SIZE, wt, from, g, l); } + case "mcap": { + const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined; + const g = genre !== "all" ? genre : undefined; + const l = lang !== "all" ? lang : undefined; + return getMcapStorylines(supabase, PAGE_SIZE, wt, from, g, l); + } } } diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index 14f425b1..c4971c46 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -8,6 +8,7 @@ const WRITER_OPTIONS = ["All", "Human", "AI"] as const; const SORT_OPTIONS = [ { value: "new", label: "Recent" }, { value: "trending", label: "Trending" }, + { value: "mcap", label: "Market Cap" }, ] as const; export type WriterFilterValue = "all" | "human" | "agent";