diff --git a/lib/ranking.ts b/lib/ranking.ts index 4816a5cf..2c550be6 100644 --- a/lib/ranking.ts +++ b/lib/ranking.ts @@ -1,7 +1,7 @@ import { type Address, formatUnits } from "viem"; import { get24hPriceChange, getTokenTVL } from "./price"; import { STORY_FACTORY } from "./contracts/constants"; -import type { Storyline } from "./supabase"; +import type { Database, Storyline } from "./supabase"; import type { SupabaseClient } from "@supabase/supabase-js"; interface RankedStoryline extends Storyline { @@ -57,9 +57,8 @@ function computeTrendScore( } /** Shared: fetch storyline candidates + batch ratings */ -async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: number) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let q = (supabase.from("storylines") as any) +async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: number) { + let q = supabase.from("storylines") .select("*") .eq("hidden", false) .eq("sunset", false) @@ -75,8 +74,7 @@ async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: // Batch: fetch all ratings for candidate storyline IDs in one query const storylineIds = storylines.map((sl) => sl.storyline_id); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: allRatings } = await (supabase.from("ratings") as any) + const { data: allRatings } = await supabase.from("ratings") .select("storyline_id, rating") .in("storyline_id", storylineIds) .eq("contract_address", STORY_FACTORY.toLowerCase()); @@ -84,7 +82,7 @@ async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: const ratingMap = new Map(); if (allRatings) { const grouped = new Map(); - for (const r of allRatings as { storyline_id: number; rating: number }[]) { + for (const r of allRatings) { const arr = grouped.get(r.storyline_id) ?? []; arr.push(r.rating); grouped.set(r.storyline_id, arr); @@ -118,7 +116,7 @@ async function enrichWithOnChain( * Fetch trending storylines ranked by composite score. */ export async function getTrendingStorylines( - supabase: SupabaseClient, + supabase: SupabaseClient, limit = 20, writerType?: number, offset = 0, @@ -158,7 +156,7 @@ export async function getTrendingStorylines( * baseline denominator — acceleration comes from rating + plot signals. */ export async function getRisingStorylines( - supabase: SupabaseClient, + supabase: SupabaseClient, limit = 20, writerType?: number, offset = 0, @@ -181,15 +179,13 @@ export async function getRisingStorylines( const storylineIds = eligible.map((sl) => sl.storyline_id); // Batch: windowed ratings - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: recentRatings } = await (supabase.from("ratings") as any) + const { data: recentRatings } = await supabase.from("ratings") .select("storyline_id, rating") .in("storyline_id", storylineIds) .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("updated_at", threeDaysAgo); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: priorRatings } = await (supabase.from("ratings") as any) + const { data: priorRatings } = await supabase.from("ratings") .select("storyline_id, rating") .in("storyline_id", storylineIds) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -197,15 +193,13 @@ export async function getRisingStorylines( .lt("updated_at", threeDaysAgo); // Batch: windowed plot counts - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: recentPlots } = await (supabase.from("plots") as any) + const { data: recentPlots } = await supabase.from("plots") .select("storyline_id") .in("storyline_id", storylineIds) .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("block_timestamp", threeDaysAgo); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: priorPlots } = await (supabase.from("plots") as any) + const { data: priorPlots } = await supabase.from("plots") .select("storyline_id") .in("storyline_id", storylineIds) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -214,14 +208,14 @@ export async function getRisingStorylines( function avgFromRows(rows: { storyline_id: number; rating: number }[] | null, slId: number): number { if (!rows) return 0; - const filtered = rows.filter((r) => r.storyline_id === slId); + const filtered = rows.filter((r: { storyline_id: number }) => r.storyline_id === slId); if (filtered.length === 0) return 0; - return filtered.reduce((s, r) => s + r.rating, 0) / filtered.length; + return filtered.reduce((s: number, r: { rating: number }) => s + r.rating, 0) / filtered.length; } function countFromRows(rows: { storyline_id: number }[] | null, slId: number): number { if (!rows) return 0; - return rows.filter((r) => r.storyline_id === slId).length; + return rows.filter((r: { storyline_id: number }) => r.storyline_id === slId).length; } // Single parallel batch for all on-chain reads @@ -233,8 +227,8 @@ export async function getRisingStorylines( const { priceChange, tvlRaw, tvlDecimals } = onChainResults[i]; // Recent window composite (all 4 signals) - const recentAvgRating = avgFromRows(recentRatings as { storyline_id: number; rating: number }[] | null, sl.storyline_id); - const recentPlotCount = countFromRows(recentPlots as { storyline_id: number }[] | null, sl.storyline_id); + const recentAvgRating = avgFromRows(recentRatings, sl.storyline_id); + const recentPlotCount = countFromRows(recentPlots, sl.storyline_id); const recentScore = computeTrendScore( recentAvgRating, priceChange, @@ -245,8 +239,8 @@ export async function getRisingStorylines( ); // Prior window composite (same 4 signals, same TVL + price as baseline) - const priorAvgRating = avgFromRows(priorRatings as { storyline_id: number; rating: number }[] | null, sl.storyline_id); - const priorPlotCount = countFromRows(priorPlots as { storyline_id: number }[] | null, sl.storyline_id); + const priorAvgRating = avgFromRows(priorRatings, sl.storyline_id); + const priorPlotCount = countFromRows(priorPlots, sl.storyline_id); const priorScore = computeTrendScore( priorAvgRating, priceChange, // same baseline — acceleration from rating/plot signals diff --git a/lib/supabase.ts b/lib/supabase.ts index 9662f48b..aa1650d0 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -101,6 +101,7 @@ export interface Database { view_count?: number; contract_address?: string; }; + Relationships: []; }; page_views: { Row: { @@ -130,6 +131,7 @@ export interface Database { viewed_at?: string; contract_address?: string; }; + Relationships: []; }; plots: { Row: { @@ -180,6 +182,7 @@ export interface Database { indexed_at?: string; contract_address?: string; }; + Relationships: []; }; comments: { Row: { @@ -212,6 +215,7 @@ export interface Database { hidden?: boolean; contract_address?: string; }; + Relationships: []; }; donations: { Row: { @@ -247,6 +251,7 @@ export interface Database { indexed_at?: string; contract_address?: string; }; + Relationships: []; }; ratings: { Row: { @@ -279,7 +284,41 @@ export interface Database { updated_at?: string; contract_address?: string; }; + Relationships: []; }; + backfill_cursor: { + Row: { + id: number; + last_block: number; + updated_at: string; + }; + Insert: { + id?: number; + last_block: number; + updated_at?: string; + }; + Update: { + id?: number; + last_block?: number; + updated_at?: string; + }; + Relationships: []; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + increment_view_count: { + Args: { sid: number; caddr: string }; + Returns: void; + }; + }; + Enums: { + [_ in never]: never; + }; + CompositeTypes: { + [_ in never]: never; }; }; } diff --git a/src/app/api/admin/auth.ts b/src/app/api/admin/auth.ts index 0835d9d7..9815d9a5 100644 --- a/src/app/api/admin/auth.ts +++ b/src/app/api/admin/auth.ts @@ -68,14 +68,11 @@ export async function handleModeration( ); } - const table = type === "storyline" ? "storylines" : "plots"; - const idColumn = type === "storyline" ? "storyline_id" : "id"; const hidden = action === "hide"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: dbError } = await (supabase.from(table) as any) - .update({ hidden }) - .eq(idColumn, id); + const { error: dbError } = type === "storyline" + ? await supabase.from("storylines").update({ hidden }).eq("storyline_id", id) + : await supabase.from("plots").update({ hidden }).eq("id", id); if (dbError) { return NextResponse.json( diff --git a/src/app/api/comments/route.ts b/src/app/api/comments/route.ts index 29a3b40a..cd36d19c 100644 --- a/src/app/api/comments/route.ts +++ b/src/app/api/comments/route.ts @@ -33,8 +33,7 @@ export async function GET(req: NextRequest) { const page = Math.max(Number(req.nextUrl.searchParams.get("page") ?? 1), 1); const offset = (page - 1) * limit; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data, error: dbError } = await (db.from("comments") as any) + const { data, error: dbError } = await db.from("comments") .select("*") .eq("storyline_id", sid) .eq("plot_index", pidx) @@ -46,8 +45,7 @@ export async function GET(req: NextRequest) { if (dbError) return error(`Database error: ${dbError.message}`, 500); // Get total count for pagination - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { count } = await (db.from("comments") as any) + const { count } = await db.from("comments") .select("id", { count: "exact", head: true }) .eq("storyline_id", sid) .eq("plot_index", pidx) @@ -117,8 +115,7 @@ export async function POST(req: NextRequest) { // Rate limit: max 1 comment per address per plot per minute const oneMinuteAgo = new Date(Date.now() - 60 * 1000).toISOString(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: recent } = await (serverClient.from("comments") as any) + const { data: recent } = await serverClient.from("comments") .select("id") .eq("storyline_id", storylineId) .eq("plot_index", plotIndex) @@ -135,8 +132,7 @@ export async function POST(req: NextRequest) { } // Insert comment - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: insertError } = await (serverClient.from("comments") as any).insert({ + const { error: insertError } = await serverClient.from("comments").insert({ storyline_id: storylineId, plot_index: plotIndex, commenter_address: commenterAddress.toLowerCase(), diff --git a/src/app/api/cron/backfill/route.ts b/src/app/api/cron/backfill/route.ts index 89a89bc9..d767eaf7 100644 --- a/src/app/api/cron/backfill/route.ts +++ b/src/app/api/cron/backfill/route.ts @@ -70,8 +70,7 @@ export async function GET(req: Request) { const currentBlock = await publicClient.getBlockNumber(); // Read last processed block from persistent cursor - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: cursor } = await (supabase.from("backfill_cursor") as any) + const { data: cursor } = await supabase.from("backfill_cursor") .select("last_block") .eq("id", 1) .single(); @@ -162,8 +161,7 @@ export async function GET(req: Request) { } // Persist cursor — advance to highest block actually scanned - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: cursorError } = await (supabase.from("backfill_cursor") as any) + const { error: cursorError } = await supabase.from("backfill_cursor") .update({ last_block: Number(toBlock), updated_at: new Date().toISOString() }) .eq("id", 1); if (cursorError) { @@ -181,17 +179,15 @@ export async function GET(req: Request) { }); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type DecodedEvent = any; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type SupabaseClient = any; +type DecodedEvent = ReturnType>; +type BackfillSupabaseClient = NonNullable>; async function processStorylineCreated( decoded: DecodedEvent, log: Log, txHash: string, logIndex: number, - supabase: SupabaseClient, + supabase: BackfillSupabaseClient, getTimestamp: (blockNumber: bigint) => Promise ) { const { @@ -202,7 +198,7 @@ async function processStorylineCreated( hasDeadline, openingCID, openingHash, - } = decoded.args; + } = decoded.args as { storylineId: bigint; writer: `0x${string}`; tokenAddress: `0x${string}`; title: string; hasDeadline: boolean; openingCID: string; openingHash: `0x${string}` }; const timestampISO = await getTimestamp(log.blockNumber!); const writerType = await detectWriterType(writer); @@ -258,11 +254,11 @@ async function processPlotChained( log: Log, txHash: string, logIndex: number, - supabase: SupabaseClient, + supabase: BackfillSupabaseClient, getTimestamp: (blockNumber: bigint) => Promise ) { const { storylineId, plotIndex, writer, contentCID, contentHash } = - decoded.args; + decoded.args as { storylineId: bigint; plotIndex: bigint; writer: `0x${string}`; title: string; contentCID: string; contentHash: `0x${string}` }; const content = await fetchIPFSContent(contentCID); if (content === null) return; // skip if content unavailable @@ -296,10 +292,10 @@ async function processDonation( log: Log, txHash: string, logIndex: number, - supabase: SupabaseClient, + supabase: BackfillSupabaseClient, getTimestamp: (blockNumber: bigint) => Promise ) { - const { storylineId, donor, amount } = decoded.args; + const { storylineId, donor, amount } = decoded.args as { storylineId: bigint; donor: `0x${string}`; amount: bigint }; const timestampISO = await getTimestamp(log.blockNumber!); const row: DonationInsert = { diff --git a/src/app/api/index/donation/route.ts b/src/app/api/index/donation/route.ts index 49ba03ee..e31b3c57 100644 --- a/src/app/api/index/donation/route.ts +++ b/src/app/api/index/donation/route.ts @@ -93,8 +93,7 @@ export async function POST(req: Request) { contract_address: STORY_FACTORY.toLowerCase(), }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: dbError } = await (supabase.from("donations") as any).upsert( + const { error: dbError } = await supabase.from("donations").upsert( row, { onConflict: "tx_hash,log_index" } ); diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index 74f86d1e..14c3b579 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -124,8 +124,7 @@ export async function POST(req: Request) { contract_address: STORY_FACTORY.toLowerCase(), }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: dbError } = await (supabase.from("plots") as any).upsert( + const { error: dbError } = await supabase.from("plots").upsert( row, { onConflict: "tx_hash,log_index" } ); diff --git a/src/app/api/index/storyline/route.ts b/src/app/api/index/storyline/route.ts index e1de2a72..a14eb3cd 100644 --- a/src/app/api/index/storyline/route.ts +++ b/src/app/api/index/storyline/route.ts @@ -141,8 +141,7 @@ export async function POST(req: Request) { contract_address: STORY_FACTORY.toLowerCase(), }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: dbError } = await (supabase.from("storylines") as any).upsert( + const { error: dbError } = await supabase.from("storylines").upsert( storylineRow, { onConflict: "tx_hash,log_index" } ); @@ -165,8 +164,7 @@ export async function POST(req: Request) { contract_address: STORY_FACTORY.toLowerCase(), }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: plotDbError } = await (supabase.from("plots") as any).upsert( + const { error: plotDbError } = await supabase.from("plots").upsert( plotRow, { onConflict: "tx_hash,log_index" } ); diff --git a/src/app/api/ratings/route.ts b/src/app/api/ratings/route.ts index 5f55a73c..2545966c 100644 --- a/src/app/api/ratings/route.ts +++ b/src/app/api/ratings/route.ts @@ -31,8 +31,7 @@ export async function GET(req: NextRequest) { const sid = Number(storylineId); // Fetch all ratings for global average/count, then slice for pagination - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: allData, error: allError } = await (db.from("ratings") as any) + const { data: allData, error: allError } = await db.from("ratings") .select("rating") .eq("storyline_id", sid) .eq("contract_address", STORY_FACTORY.toLowerCase()); @@ -49,8 +48,7 @@ export async function GET(req: NextRequest) { : 0; // Paginated query for full rating objects - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data, error: dbError } = await (db.from("ratings") as any) + const { data, error: dbError } = await db.from("ratings") .select("*") .eq("storyline_id", sid) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -67,8 +65,7 @@ export async function GET(req: NextRequest) { const raterAddress = req.nextUrl.searchParams.get("raterAddress"); let myRating: unknown = null; if (raterAddress) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: mine } = await (db.from("ratings") as any) + const { data: mine } = await db.from("ratings") .select("*") .eq("storyline_id", sid) .eq("rater_address", raterAddress.toLowerCase()) @@ -147,8 +144,7 @@ export async function POST(req: NextRequest) { return error("Supabase not configured", 500); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: storyline, error: slError } = await (serverClient.from("storylines") as any) + const { data: storyline, error: slError } = await serverClient.from("storylines") .select("token_address") .eq("storyline_id", storylineId) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -177,8 +173,7 @@ export async function POST(req: NextRequest) { } // 4. Upsert rating via service role client - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: upsertError } = await (serverClient.from("ratings") as any).upsert( + const { error: upsertError } = await serverClient.from("ratings").upsert( { storyline_id: storylineId, rater_address: raterAddress.toLowerCase(), diff --git a/src/app/api/views/route.ts b/src/app/api/views/route.ts index 7413f0f5..7d1895f7 100644 --- a/src/app/api/views/route.ts +++ b/src/app/api/views/route.ts @@ -56,8 +56,7 @@ export async function GET(req: NextRequest) { const sid = Number(storylineId); if (isNaN(sid) || sid <= 0) return error("Invalid storylineId"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data, error: dbError } = await (db.from("storylines") as any) + const { data, error: dbError } = await db.from("storylines") .select("view_count") .eq("storyline_id", sid) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -116,8 +115,7 @@ export async function POST(req: NextRequest) { const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // Dedup: check if this session already viewed this page in the last hour - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let dedupQuery = (serverClient.from("page_views") as any) + let dedupQuery = serverClient.from("page_views") .select("id") .eq("storyline_id", storylineId) .eq("contract_address", STORY_FACTORY.toLowerCase()) @@ -138,8 +136,7 @@ export async function POST(req: NextRequest) { } // Insert page view record - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: insertError } = await (serverClient.from("page_views") as any).insert({ + const { error: insertError } = await serverClient.from("page_views").insert({ storyline_id: storylineId, plot_index: plotVal, viewer_address: viewerAddress?.toLowerCase() ?? null, @@ -151,13 +148,12 @@ export async function POST(req: NextRequest) { // Increment denormalized counter (storyline-level views only) if (plotVal === null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (serverClient.rpc as any)("increment_view_count", { + // Ignore errors — counter will be slightly behind but page_views table is authoritative + const { error: rpcError } = await serverClient.rpc("increment_view_count", { sid: storylineId, caddr: STORY_FACTORY.toLowerCase(), - }).catch(() => { - // Ignore — counter will be slightly behind but page_views table is authoritative }); + if (rpcError) console.warn("increment_view_count failed:", rpcError.message); } return NextResponse.json({ success: true, deduplicated: false }); diff --git a/src/app/dashboard/writer/page.tsx b/src/app/dashboard/writer/page.tsx index 4f88bae3..1cbdf2ec 100644 --- a/src/app/dashboard/writer/page.tsx +++ b/src/app/dashboard/writer/page.tsx @@ -173,8 +173,7 @@ function DonationCount({ storylineId, tokenAddress }: { storylineId: number; tok const [tvlData, rows] = await Promise.all([ getTokenTVL(tokenAddress as Address), supabase - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - (supabase.from("donations") as any) + ? supabase.from("donations") .select("amount") .eq("storyline_id", storylineId) .eq("contract_address", STORY_FACTORY.toLowerCase()) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4443e55b..147f0464 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -39,8 +39,7 @@ export default async function Home({ storylines = await queryTab(supabase, tab, writer, page); // Fetch genesis plot previews if (storylines.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: plots } = await (supabase.from("plots") as any) + const { data: plots } = await supabase.from("plots") .select("storyline_id, content") .in("storyline_id", storylines.map((s) => s.storyline_id)) .eq("plot_index", 0)