@@ -1317,6 +1317,67 @@ export function updateCampaignMetadata(campaignId: string, newMetadata: string):
13171317 ) ;
13181318}
13191319
1320+ export interface TrendingCampaignEntry {
1321+ campaign : CampaignRecord ;
1322+ progress : CampaignProgress ;
1323+ pledgeVelocity : number ; // pledges in last 24h / hours the campaign has been open
1324+ recentPledgeCount : number ; // raw count of non-refunded pledges in last 24h
1325+ }
1326+
1327+ /**
1328+ * Returns the top 10 open campaigns ranked by pledge velocity.
1329+ *
1330+ * Velocity = (non-refunded pledges in last 24h) / hours the campaign has been open.
1331+ * Only campaigns that are currently open (not funded, claimed, or failed) are included.
1332+ *
1333+ * @param limit - Maximum number of trending campaigns to return (default: 10).
1334+ * @returns An array of {@link TrendingCampaignEntry} objects sorted by velocity descending.
1335+ */
1336+ export function getTrendingCampaigns ( limit = 10 ) : TrendingCampaignEntry [ ] {
1337+ const db = getDb ( ) ;
1338+ const now = nowInSeconds ( ) ;
1339+ const window24h = now - 86400 ; // 24 hours ago in unix seconds
1340+
1341+ // Fetch open campaigns with their 24h pledge counts in one query.
1342+ // "Open" = deadline in the future, not claimed, not yet at target (funded).
1343+ // deleted_at IS NULL is always enforced.
1344+ const rows = db
1345+ . prepare (
1346+ `SELECT
1347+ c.*,
1348+ COUNT(p.id) AS recent_pledge_count
1349+ FROM campaigns c
1350+ LEFT JOIN pledges p
1351+ ON p.campaign_id = c.id
1352+ AND p.refunded_at IS NULL
1353+ AND p.created_at >= ?
1354+ WHERE c.deleted_at IS NULL
1355+ AND c.claimed_at IS NULL
1356+ AND c.pledged_amount < c.target_amount
1357+ AND c.deadline > ?
1358+ GROUP BY c.id
1359+ ORDER BY recent_pledge_count DESC, c.pledged_amount DESC
1360+ LIMIT ?` ,
1361+ )
1362+ . all ( window24h , now , limit ) as Array < CampaignRow & { recent_pledge_count : number } > ;
1363+
1364+ return rows . map ( ( row ) => {
1365+ const { recent_pledge_count, ...campaignRow } = row ;
1366+ const campaign = rowToCampaign ( campaignRow as CampaignRow ) ;
1367+
1368+ // hoursOpen: at least 1 hour floor to avoid division-by-zero on brand-new campaigns
1369+ const hoursOpen = Math . max ( 1 , ( now - campaign . createdAt ) / 3600 ) ;
1370+ const pledgeVelocity = Number ( ( recent_pledge_count / hoursOpen ) . toFixed ( 4 ) ) ;
1371+
1372+ return {
1373+ campaign,
1374+ progress : calculateProgress ( campaign , now ) ,
1375+ pledgeVelocity,
1376+ recentPledgeCount : recent_pledge_count ,
1377+ } ;
1378+ } ) ;
1379+ }
1380+
13201381export function getTopContributors ( limit : number = 10 ) : LeaderboardEntry [ ] {
13211382 const db = getDb ( ) ;
13221383 const rows = db
0 commit comments