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.
This commit is contained in:
parent
6ae4e674dc
commit
046627b1dd
1 changed files with 83 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
|
|
@ -9,6 +10,8 @@ import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
||||||
import { useUpcomingSkateEvents } from '@/hooks/useSkateEvents';
|
import { useUpcomingSkateEvents } from '@/hooks/useSkateEvents';
|
||||||
import { useLabProfiles } from '@/hooks/useLabProfiles';
|
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 { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useTranslation } from '@/contexts/LanguageContext';
|
import { useTranslation } from '@/contexts/LanguageContext';
|
||||||
|
|
@ -25,6 +28,7 @@ import {
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
Video,
|
Video,
|
||||||
Crown,
|
Crown,
|
||||||
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { FounderFollow } from '@/components/FounderFollow';
|
import { FounderFollow } from '@/components/FounderFollow';
|
||||||
import { SponsorSlot } from '@/components/SponsorSlot';
|
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<string, SkateSpot>();
|
||||||
|
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 (
|
||||||
|
<section className="bg-black py-16 border-t border-[#ccff00]/30">
|
||||||
|
<div className="container px-4">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-display text-4xl md:text-5xl neon-lime mb-1 tracking-wide">
|
||||||
|
TOP ZAPPED
|
||||||
|
</h2>
|
||||||
|
<p className="font-condensed text-gray-400 uppercase text-sm tracking-[0.15em]" style={{ fontWeight: 600 }}>
|
||||||
|
Most supported spots this month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link to="/leaderboards">
|
||||||
|
<Button variant="ghost" className="gap-2 text-[#ccff00] hover:text-[#ccff00]/80 hover:bg-[#ccff00]/10">
|
||||||
|
Leaderboard
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{top.map((t, i) => {
|
||||||
|
const spot = spotByCoord.get(t.key)!;
|
||||||
|
const isShop = spot.types.includes('shop');
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={t.key}
|
||||||
|
to={isShop ? `/shop/${spot.pubkey}/${spot.identifier}` : `/spot/${spot.pubkey}/${spot.identifier}`}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 rounded-xl border-2 border-white/10 bg-black/60 px-3 py-2.5 transition-colors hover:border-[#ccff00]/60">
|
||||||
|
<span className="flex w-6 items-center justify-center text-sm font-bold text-gray-500">
|
||||||
|
{i === 0 ? <Crown className="h-5 w-5 text-yellow-400" /> : `#${i + 1}`}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate font-bold uppercase tracking-wide text-white">{spot.title}</p>
|
||||||
|
{spot.location && <p className="truncate text-xs text-gray-500">{spot.location}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 font-bold text-[#ccff00]">
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
{t.sats.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TheLabSection() {
|
function TheLabSection() {
|
||||||
const { data: profiles, isLoading } = useLabProfiles(100);
|
const { data: profiles, isLoading } = useLabProfiles(100);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -552,6 +634,7 @@ export default function Index() {
|
||||||
<Layout>
|
<Layout>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<FeaturedSpots />
|
<FeaturedSpots />
|
||||||
|
<TopZappedSection />
|
||||||
<UpcomingEvents />
|
<UpcomingEvents />
|
||||||
<TheLabSection />
|
<TheLabSection />
|
||||||
<NewToNostrSection />
|
<NewToNostrSection />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue