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
10 changes: 8 additions & 2 deletions lib/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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";
import { getStoryStatus } from "./story-status";

interface RankedStoryline extends Storyline {
trendScore: number;
Expand Down Expand Up @@ -130,7 +131,6 @@ async function fetchCandidatesAndRatings(
function applyBase(q: ReturnType<typeof supabase.from>) {
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);
Expand Down Expand Up @@ -272,7 +272,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 = getStoryStatus(a) === "active" ? 0 : 1;
const bActive = getStoryStatus(b) === "active" ? 0 : 1;
if (aActive !== bActive) return aActive - bActive;
return b.trendScore - a.trendScore;
});
return enriched.slice(offset, offset + limit);
}

17 changes: 17 additions & 0 deletions lib/story-status.ts
Original file line number Diff line number Diff line change
@@ -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<Storyline, "sunset" | "has_deadline" | "last_plot_time">): 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";
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plotlink",
"version": "0.1.47",
"version": "0.1.48",
"private": true,
"workspaces": [
"packages/*"
Expand Down
4 changes: 2 additions & 2 deletions src/components/DeadlineCountdown.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
25 changes: 17 additions & 8 deletions src/components/StoryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AgentBadge } from "./AgentBadge";
import { WriterIdentityClient } from "./WriterIdentityClient";
import { RatingSummary } from "./RatingSummary";
import { StoryCardTVL } from "./StoryCardStats";
import { getStoryStatus } from "../../lib/story-status";

const DAY_MS = 24 * 60 * 60 * 1000;
function isWithin24h(timestamp: string): boolean {
Expand All @@ -21,9 +22,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 (
<div className="flex flex-col">
<div className={`flex flex-col${!isActive ? " opacity-60" : ""}`}>
<Link
href={`/story/${storyline.storyline_id}`}
className="moleskine-notebook group relative block"
Expand Down Expand Up @@ -55,7 +58,7 @@ export function StoryCard({

{/* Cover — opens on hover (desktop) */}
<div
className="notebook-cover relative z-10 flex aspect-[2/3] flex-col overflow-hidden border border-[var(--border)]"
className={`notebook-cover relative z-10 flex aspect-[2/3] flex-col overflow-hidden ${isActive ? "border-2 border-[var(--accent)]" : "border border-[var(--border)]"}`}
style={{
borderRadius: "5px 15px 15px 5px",
backgroundColor: "#F5EFE6",
Expand All @@ -77,11 +80,6 @@ export function StoryCard({
<span className="rounded-sm bg-[var(--accent)]/10 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-widest text-[var(--accent)]">
{displayGenre || "Uncategorized"}
</span>
{storyline.sunset && (
<span className="rounded-sm border border-[var(--border)] px-1.5 py-0.5 text-[9px] text-[var(--text-muted)]">
complete
</span>
)}
</div>
</div>

Expand All @@ -97,9 +95,20 @@ export function StoryCard({
)}
</div>

{/* Bottom: plot count + NEW badges */}
{/* Bottom: status + plot count + NEW badges */}
<div className="relative z-10 px-4 py-3">
<div className="flex flex-wrap items-center gap-1.5">
{isActive ? (
<span className="rounded-sm bg-[var(--accent)]/10 px-1.5 py-0.5 text-[9px] font-semibold text-[var(--accent)]">
Active
</span>
) : (
<span className="rounded-sm border border-[var(--border)] px-1.5 py-0.5 text-[9px] text-[var(--text-muted)]">
{status === "expired" ? "Expired" : "Completed"}
</span>
)}
</div>
<div className="mt-1 flex flex-wrap items-center gap-1.5">
<span className="rounded-sm border border-[var(--border)] px-1.5 py-0.5 text-[9px] text-[var(--text-muted)]">
{storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"}
</span>
Expand Down
Loading