From 0bba2b15637bb2b1e30a41571eaa6a7e976a0629 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 08:19:38 +0100 Subject: [PATCH 1/3] [#738] Writer tab v9: move status/created to deadline section, add rating box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Move active/complete tag from stat boxes to Deadline divider section 2. Move Created date to Deadline section with full year (e.g. Mar 26, 2026) 3. Replace empty 4th stat box with Rating (average or "—") Fixes #738 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 44 ++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index 19f371a0..b304c9b4 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -812,6 +812,17 @@ function StoryRow({ enabled: !!storyline.token_address, }); + // Average rating for this storyline + const { data: ratingData } = useQuery<{ average: number; count: number }>({ + queryKey: ["ratings", storyline.storyline_id], + queryFn: async () => { + const res = await fetch(`/api/ratings?storylineId=${storyline.storyline_id}`); + if (!res.ok) throw new Error("Failed to fetch ratings"); + return res.json(); + }, + staleTime: 60000, + }); + return ( <>
@@ -869,15 +880,10 @@ function StoryRow({
Views
-
{storyline.block_timestamp ? new Date(storyline.block_timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric" }) : "—"}
-
Created
+
{ratingData && ratingData.count > 0 ? ratingData.average.toFixed(1) : "—"}
+
Rating
- {storyline.sunset ? ( - complete - ) : ( - active - )} {/* TVL + Donations (inline in info area) */} {storyline.token_address && ( <> @@ -891,12 +897,26 @@ function StoryRow({ - {/* Deadline */} - {!storyline.sunset && storyline.last_plot_time && ( -
- + {/* Status + Created + Deadline */} +
+
+ {storyline.sunset ? ( + complete + ) : ( + active + )} + {!storyline.sunset && storyline.last_plot_time && ( + <> + · + + + )}
- )} +
+ Created:{" "} + {storyline.block_timestamp ? new Date(storyline.block_timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "—"} +
+
{/* Royalties — own profile */} {isOwnProfile && storyline.token_address && ( From 0f87ccdc06b15470726fd0070f74e3c517bcbc4e Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 08:24:49 +0100 Subject: [PATCH 2/3] [#738] Batch-fetch ratings at StoriesTab level, remove N+1 per-card fetch Addresses T2a review: replace per-card /api/ratings fetch with a single supabase query in StoriesTab that batch-fetches all ratings for the writer's storylines and passes them down as a prop. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 40 ++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index b304c9b4..5dc78635 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -567,6 +567,32 @@ function StoriesTab({ enabled: storylineIds.length > 0, }); + // Batch-fetch average ratings for all storylines + const { data: ratingsMap = new Map() } = useQuery({ + queryKey: ["profile-ratings-batch", storylineIds], + queryFn: async () => { + if (!supabase || storylineIds.length === 0) return new Map(); + const { data } = await supabase + .from("ratings") + .select("storyline_id, rating") + .in("storyline_id", storylineIds); + const map = new Map(); + if (!data) return map; + const grouped = new Map(); + for (const r of data as { storyline_id: number; rating: number }[]) { + const arr = grouped.get(r.storyline_id) ?? []; + arr.push(r.rating); + grouped.set(r.storyline_id, arr); + } + for (const [sid, ratings] of grouped) { + const avg = ratings.reduce((a, b) => a + b, 0) / ratings.length; + map.set(sid, { average: avg, count: ratings.length }); + } + return map; + }, + enabled: storylineIds.length > 0, + }); + // Total token holders across all writer's storylines (on-chain balanceOf) const { data: totalHolders } = useQuery({ queryKey: ["profile-total-holders", address, storylineIds], @@ -748,6 +774,7 @@ function StoriesTab({ isOwnProfile={isOwnProfile} writerAddress={connectedAddress as Address} plotUsd={plotUsd} + ratingData={ratingsMap.get(s.storyline_id)} /> ))}
@@ -760,11 +787,13 @@ function StoryRow({ isOwnProfile, writerAddress, plotUsd, + ratingData, }: { storyline: Storyline; isOwnProfile: boolean; writerAddress: Address; plotUsd?: number | null; + ratingData?: { average: number; count: number }; }) { const tokenAddr = storyline.token_address as Address; @@ -812,17 +841,6 @@ function StoryRow({ enabled: !!storyline.token_address, }); - // Average rating for this storyline - const { data: ratingData } = useQuery<{ average: number; count: number }>({ - queryKey: ["ratings", storyline.storyline_id], - queryFn: async () => { - const res = await fetch(`/api/ratings?storylineId=${storyline.storyline_id}`); - if (!res.ok) throw new Error("Failed to fetch ratings"); - return res.json(); - }, - staleTime: 60000, - }); - return ( <>
From 9590458f1987b3677fe6b4c19b250af5619309d0 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 08:27:39 +0100 Subject: [PATCH 3/3] [#738] Add contract_address filter to batch ratings query Scopes the ratings query to STORY_FACTORY, matching the existing ratings API pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index 5dc78635..9a49a2ff 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -575,7 +575,8 @@ function StoriesTab({ const { data } = await supabase .from("ratings") .select("storyline_id, rating") - .in("storyline_id", storylineIds); + .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()); const map = new Map(); if (!data) return map; const grouped = new Map();