From f564646854640fecee590c7215adc4dddaf5c655 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 07:17:20 +0900 Subject: [PATCH 1/4] [#958] Add active story badges, accent borders, and active-first trending sort Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/ranking.ts | 20 ++++++++++++++++++-- package-lock.json | 4 ++-- package.json | 2 +- src/components/StoryCard.tsx | 26 ++++++++++++++++++++++---- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/ranking.ts b/lib/ranking.ts index f2cc4aac..d06abcdc 100644 --- a/lib/ranking.ts +++ b/lib/ranking.ts @@ -4,6 +4,17 @@ import { STORY_FACTORY } from "./contracts/constants"; import type { Database, Storyline, User } from "./supabase"; import type { SupabaseClient } from "@supabase/supabase-js"; +const DEADLINE_MS = 168 * 60 * 60 * 1000; + +/** True when a story is still accepting new plots. */ +function isStoryActive(sl: Storyline): boolean { + if (sl.sunset) return false; + if (sl.has_deadline && sl.last_plot_time) { + return new Date(sl.last_plot_time).getTime() + DEADLINE_MS > Date.now(); + } + return true; +} + interface RankedStoryline extends Storyline { trendScore: number; } @@ -130,7 +141,6 @@ async function fetchCandidatesAndRatings( function applyBase(q: ReturnType) { let filtered = q .eq("hidden", false) - .eq("sunset", false) .neq("token_address", "") .eq("contract_address", STORY_FACTORY.toLowerCase()); if (writerType !== undefined) filtered = filtered.eq("writer_type", writerType); @@ -272,7 +282,13 @@ export async function getTrendingStorylines( }), ); - enriched.sort((a, b) => b.trendScore - a.trendScore); + // Active-first: active stories rank above completed/expired, then by trendScore + enriched.sort((a, b) => { + const aActive = isStoryActive(a) ? 0 : 1; + const bActive = isStoryActive(b) ? 0 : 1; + if (aActive !== bActive) return aActive - bActive; + return b.trendScore - a.trendScore; + }); return enriched.slice(offset, offset + limit); } diff --git a/package-lock.json b/package-lock.json index 9a15534c..5def62a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "0.1.47", + "version": "0.1.48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "0.1.47", + "version": "0.1.48", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index b01272bc..24c88500 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.47", + "version": "0.1.48", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index e9ae5f99..c7bda221 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -4,12 +4,24 @@ import { AgentBadge } from "./AgentBadge"; import { WriterIdentityClient } from "./WriterIdentityClient"; import { RatingSummary } from "./RatingSummary"; import { StoryCardTVL } from "./StoryCardStats"; +import { DEADLINE_MS } from "./DeadlineCountdown"; const DAY_MS = 24 * 60 * 60 * 1000; function isWithin24h(timestamp: string): boolean { return Date.now() - new Date(timestamp).getTime() < DAY_MS; } +type StoryStatus = "active" | "completed" | "expired"; + +function getStoryStatus(storyline: Storyline): StoryStatus { + if (storyline.sunset) return "completed"; + if (storyline.has_deadline && storyline.last_plot_time) { + const deadline = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS; + if (Date.now() > deadline) return "expired"; + } + return "active"; +} + export function StoryCard({ storyline, genre, @@ -21,9 +33,11 @@ export function StoryCard({ const isNew = storyline.last_plot_time ? isWithin24h(storyline.last_plot_time) : false; + const status = getStoryStatus(storyline); + const isActive = status === "active"; return ( -
+
{displayGenre || "Uncategorized"} - {storyline.sunset && ( + {isActive ? ( + + Active + + ) : ( - complete + {status === "expired" ? "Expired" : "Completed"} )}
From 92c7ba698b2f1fd156c90d5aed1395ee10d9747a Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 07:19:28 +0900 Subject: [PATCH 2/4] [#958] Extract shared getStoryStatus + DEADLINE_MS to lib/story-status.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/ranking.ts | 16 +++------------- lib/story-status.ts | 17 +++++++++++++++++ src/components/StoryCard.tsx | 13 +------------ 3 files changed, 21 insertions(+), 25 deletions(-) create mode 100644 lib/story-status.ts diff --git a/lib/ranking.ts b/lib/ranking.ts index d06abcdc..f2a9f434 100644 --- a/lib/ranking.ts +++ b/lib/ranking.ts @@ -3,17 +3,7 @@ import { get24hPriceChange, getTokenTVL } from "./price"; import { STORY_FACTORY } from "./contracts/constants"; import type { Database, Storyline, User } from "./supabase"; import type { SupabaseClient } from "@supabase/supabase-js"; - -const DEADLINE_MS = 168 * 60 * 60 * 1000; - -/** True when a story is still accepting new plots. */ -function isStoryActive(sl: Storyline): boolean { - if (sl.sunset) return false; - if (sl.has_deadline && sl.last_plot_time) { - return new Date(sl.last_plot_time).getTime() + DEADLINE_MS > Date.now(); - } - return true; -} +import { getStoryStatus } from "./story-status"; interface RankedStoryline extends Storyline { trendScore: number; @@ -284,8 +274,8 @@ export async function getTrendingStorylines( // Active-first: active stories rank above completed/expired, then by trendScore enriched.sort((a, b) => { - const aActive = isStoryActive(a) ? 0 : 1; - const bActive = isStoryActive(b) ? 0 : 1; + const aActive = getStoryStatus(a) === "active" ? 0 : 1; + const bActive = getStoryStatus(b) === "active" ? 0 : 1; if (aActive !== bActive) return aActive - bActive; return b.trendScore - a.trendScore; }); diff --git a/lib/story-status.ts b/lib/story-status.ts new file mode 100644 index 00000000..8cf6af81 --- /dev/null +++ b/lib/story-status.ts @@ -0,0 +1,17 @@ +import type { Storyline } from "./supabase"; + +/** Deadline window in hours — stories expire this long after their last plot. */ +export const DEADLINE_HOURS = 168; +export const DEADLINE_MS = DEADLINE_HOURS * 60 * 60 * 1000; + +export type StoryStatus = "active" | "completed" | "expired"; + +/** Determine whether a story is active, completed, or expired. */ +export function getStoryStatus(storyline: Pick): StoryStatus { + if (storyline.sunset) return "completed"; + if (storyline.has_deadline && storyline.last_plot_time) { + const deadline = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS; + if (Date.now() > deadline) return "expired"; + } + return "active"; +} diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index c7bda221..aa9246be 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -4,24 +4,13 @@ import { AgentBadge } from "./AgentBadge"; import { WriterIdentityClient } from "./WriterIdentityClient"; import { RatingSummary } from "./RatingSummary"; import { StoryCardTVL } from "./StoryCardStats"; -import { DEADLINE_MS } from "./DeadlineCountdown"; +import { getStoryStatus } from "../../lib/story-status"; const DAY_MS = 24 * 60 * 60 * 1000; function isWithin24h(timestamp: string): boolean { return Date.now() - new Date(timestamp).getTime() < DAY_MS; } -type StoryStatus = "active" | "completed" | "expired"; - -function getStoryStatus(storyline: Storyline): StoryStatus { - if (storyline.sunset) return "completed"; - if (storyline.has_deadline && storyline.last_plot_time) { - const deadline = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS; - if (Date.now() > deadline) return "expired"; - } - return "active"; -} - export function StoryCard({ storyline, genre, From fb2e68e09583306ded16b744852e910ad795a0bb Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 07:20:41 +0900 Subject: [PATCH 3/4] [#958] Move status badge from top to bottom of card (above plot count) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/StoryCard.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index aa9246be..8e5fdf85 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -80,15 +80,6 @@ export function StoryCard({ {displayGenre || "Uncategorized"} - {isActive ? ( - - Active - - ) : ( - - {status === "expired" ? "Expired" : "Completed"} - - )}
@@ -104,9 +95,20 @@ export function StoryCard({ )} - {/* Bottom: plot count + NEW badges */} + {/* Bottom: status + plot count + NEW badges */}
+ {isActive ? ( + + Active + + ) : ( + + {status === "expired" ? "Expired" : "Completed"} + + )} +
+
{storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"} From bfdd93a6baf3947b4124c4b4d4f2f8f6638fb521 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 07:21:31 +0900 Subject: [PATCH 4/4] [#958] DeadlineCountdown imports DEADLINE_HOURS/MS from shared lib/story-status Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DeadlineCountdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DeadlineCountdown.tsx b/src/components/DeadlineCountdown.tsx index dcd93a18..8b10cdf2 100644 --- a/src/components/DeadlineCountdown.tsx +++ b/src/components/DeadlineCountdown.tsx @@ -1,9 +1,9 @@ "use client"; import { useState, useEffect } from "react"; +import { DEADLINE_HOURS, DEADLINE_MS } from "../../lib/story-status"; -export const DEADLINE_HOURS = 168; -export const DEADLINE_MS = DEADLINE_HOURS * 60 * 60 * 1000; +export { DEADLINE_HOURS, DEADLINE_MS }; export function DeadlineCountdown({ lastPlotTime,