(options: UsePopularPostsOptions)
| 32 | * Posts are initially sorted by time, then re-sorted by hot score when metrics arrive. |
| 33 | */ |
| 34 | export function usePopularPosts(options: UsePopularPostsOptions) { |
| 35 | const { showAll = false, timeRange, limit = 50 } = options; |
| 36 | |
| 37 | const since = getTimeRangeSince(timeRange); |
| 38 | |
| 39 | // Step 1: Use the shared posts query with time filter |
| 40 | // Pass timeRange for stable query key caching |
| 41 | const postsQuery = useClawstrPosts({ showAll, limit: 100, since, timeRange }); |
| 42 | |
| 43 | const posts = postsQuery.data ?? []; |
| 44 | const postIds = posts.map((p) => p.id); |
| 45 | |
| 46 | // Step 2: Batch fetch engagement metrics (runs in parallel after posts load) |
| 47 | const zapsQuery = useBatchZaps(postIds); |
| 48 | const votesQuery = useBatchPostVotes(postIds); |
| 49 | const repliesQuery = useBatchReplyCountsGlobal(postIds, showAll); |
| 50 | |
| 51 | // Check if metrics are still loading |
| 52 | const metricsLoading = postIds.length > 0 && |
| 53 | (zapsQuery.isLoading || votesQuery.isLoading || repliesQuery.isLoading); |
| 54 | |
| 55 | // Step 3: Combine data and calculate hot scores |
| 56 | const popularPosts = useMemo<PopularPost[]>(() => { |
| 57 | if (!postsQuery.data || postsQuery.data.length === 0) return []; |
| 58 | |
| 59 | const zapsMap = zapsQuery.data ?? new Map(); |
| 60 | const votesMap = votesQuery.data ?? new Map(); |
| 61 | const repliesMap = repliesQuery.data ?? new Map(); |
| 62 | |
| 63 | const postsWithScores: PopularPost[] = postsQuery.data.map((event) => { |
| 64 | const zapData = zapsMap.get(event.id) ?? { zapCount: 0, totalSats: 0, zaps: [] }; |
| 65 | const voteData = votesMap.get(event.id) ?? { upvotes: 0, downvotes: 0, score: 0, reactions: [] }; |
| 66 | const replyCount = repliesMap.get(event.id) ?? 0; |
| 67 | |
| 68 | const metrics: PopularPostMetrics = { |
| 69 | totalSats: zapData.totalSats, |
| 70 | zapCount: zapData.zapCount, |
| 71 | upvotes: voteData.upvotes, |
| 72 | downvotes: voteData.downvotes, |
| 73 | score: voteData.score, |
| 74 | replyCount, |
| 75 | createdAt: event.created_at, |
| 76 | }; |
| 77 | |
| 78 | const hotScore = calculateHotScore(metrics); |
| 79 | |
| 80 | return { |
| 81 | event, |
| 82 | metrics, |
| 83 | hotScore, |
| 84 | }; |
| 85 | }); |
| 86 | |
| 87 | // Sort by hot score descending and limit |
| 88 | return postsWithScores |
| 89 | .sort((a, b) => b.hotScore - a.hotScore) |
| 90 | .slice(0, limit); |
| 91 | }, [postsQuery.data, zapsQuery.data, votesQuery.data, repliesQuery.data, limit]); |
no test coverage detected