From 046627b1dddf30a783c5097847dcb3f7bce182ce Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sat, 26 Sep 2026 17:19:55 +0700 Subject: [PATCH] feat(home): 'Top Zapped' section linking to the leaderboard Shows the four most-zapped spots (last 30 days) on the home page, with their sats totals, linking through to /leaderboards. Renders nothing when there is no zap activity or no linkable spot, so the section never appears empty. --- src/pages/Index.tsx | 83 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 178d8e6..de59467 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; @@ -9,6 +10,8 @@ import { LoginArea } from '@/components/auth/LoginArea'; import { useSkateSpots } from '@/hooks/useSkateSpots'; import { useUpcomingSkateEvents } from '@/hooks/useSkateEvents'; import { useLabProfiles } from '@/hooks/useLabProfiles'; +import { useZapLeaderboard } from '@/hooks/useZapLeaderboard'; +import { SKATE_SPOT_KIND, type SkateSpot } from '@/lib/types'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useTranslation } from '@/contexts/LanguageContext'; @@ -25,6 +28,7 @@ import { GraduationCap, Video, Crown, + Zap, } from 'lucide-react'; import { FounderFollow } from '@/components/FounderFollow'; import { SponsorSlot } from '@/components/SponsorSlot'; @@ -238,6 +242,84 @@ function UpcomingEvents() { ); } +function TopZappedSection() { + // Last 30 days of zap activity, aggregated once. + const since = useMemo(() => Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60, []); + const { data } = useZapLeaderboard(since); + const { data: spots } = useSkateSpots(200); + + const spotByCoord = useMemo(() => { + const map = new Map(); + for (const s of spots ?? []) { + map.set(`${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`, s); + } + return map; + }, [spots]); + + // Only spots we can actually link to, so every row lands somewhere real. + const top = useMemo( + () => + (data?.targets ?? []) + .filter((t) => t.key.startsWith(`${SKATE_SPOT_KIND}:`) && spotByCoord.has(t.key)) + .slice(0, 4), + [data, spotByCoord], + ); + + // Never render an empty section on the home page. + if (top.length === 0) return null; + + return ( +
+
+
+
+

+ TOP ZAPPED +

+

+ Most supported spots this month +

+
+ + + +
+ +
+ {top.map((t, i) => { + const spot = spotByCoord.get(t.key)!; + const isShop = spot.types.includes('shop'); + return ( + +
+ + {i === 0 ? : `#${i + 1}`} + +
+

{spot.title}

+ {spot.location &&

{spot.location}

} +
+
+ + {t.sats.toLocaleString()} +
+
+ + ); + })} +
+
+
+ ); +} + function TheLabSection() { const { data: profiles, isLoading } = useLabProfiles(100); const { t } = useTranslation(); @@ -552,6 +634,7 @@ export default function Index() { +