Removes the last react-refresh disables by splitting exports instead: - contexts/language.ts: Language/Translations types + LanguageContext + useTranslation/useT (LanguageProvider stays a component-only file); 12 importers repointed. - lib/sortOptions.ts: calculatePopularity + calculateDistance (out of SortDropdown) - lib/trustFilter.ts: TrustFilterLevel + filterByTrustLevel (out of TrustFilter) - EventCountdown: deleted the unused useEventStatus hook - NewsletterPage: dropped the NEWSLETTER_ISSUES re-export shim - AGENTS.md updated (lint stays clean with no blanket disables) Gate: 101 tests, 0 errors/warnings, boot smoke green.
650 lines
27 KiB
TypeScript
650 lines
27 KiB
TypeScript
import { useMemo } from 'react';
|
||
import { useSeoMeta } from '@unhead/react';
|
||
import { Link } from 'react-router-dom';
|
||
import { Layout } from '@/components/Layout';
|
||
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
||
import { EventCard, EventCardSkeleton } from '@/components/EventCard';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { LoginArea } from '@/components/auth/LoginArea';
|
||
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
||
import { useUpcomingSkateEvents } from '@/hooks/useSkateEvents';
|
||
import { useLabProfiles, type LabProfile } 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/language';
|
||
import { genUserName } from '@/lib/genUserName';
|
||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import {
|
||
MapPin,
|
||
Calendar,
|
||
Plus,
|
||
ChevronRight,
|
||
Sparkles,
|
||
CheckCircle2,
|
||
GraduationCap,
|
||
Video,
|
||
Crown,
|
||
Zap,
|
||
} from 'lucide-react';
|
||
import { FounderFollow } from '@/components/FounderFollow';
|
||
import { SponsorSlot } from '@/components/SponsorSlot';
|
||
|
||
function HeroSection() {
|
||
const { user } = useCurrentUser();
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<section className="relative overflow-hidden bg-black">
|
||
{/* Neon background glows */}
|
||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-[#ff0040] opacity-20 rounded-full blur-[150px]" />
|
||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-[#ccff00] opacity-15 rounded-full blur-[120px]" />
|
||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-[#ff6600] opacity-10 rounded-full blur-[100px]" />
|
||
|
||
<div className="relative container px-3 xs:px-4 sm:px-6 md:px-8 py-10 xs:py-14 sm:py-20 md:py-28">
|
||
<div className="max-w-4xl mx-auto text-center">
|
||
<div className="mb-6 xs:mb-8">
|
||
<div className="inline-flex items-center px-3 xs:px-6 py-1.5 xs:py-2 rounded-full border-2 border-[#ff0040]/50 bg-black/50 backdrop-blur-sm mb-4 xs:mb-6">
|
||
<span className="font-condensed text-[#ff0040] uppercase tracking-[0.2em] text-sm xs:text-base" style={{ fontWeight: 600 }}>
|
||
<Sparkles className="inline h-3 w-3 xs:h-4 xs:w-4 mr-1 xs:mr-2" />
|
||
{t('home.tagline')}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Main headline — Bebas Neue split into two lines for visual impact */}
|
||
<h1 className="font-display mb-4 xs:mb-5 leading-[0.88] tracking-wide">
|
||
<span className="block text-[clamp(4rem,18vw,10rem)] gradient-text drop-shadow-[0_2px_24px_rgba(255,0,64,0.4)]">
|
||
SKATE
|
||
</span>
|
||
<span className="block text-[clamp(4rem,18vw,10rem)] text-white drop-shadow-[0_2px_24px_rgba(204,255,0,0.25)]">
|
||
SPOTS
|
||
</span>
|
||
</h1>
|
||
|
||
{/* Subtitle — Barlow Condensed, bold and spaced */}
|
||
<p className="font-condensed font-700 text-[clamp(0.85rem,3.5vw,1.35rem)] text-gray-100 mb-2 md:mb-3 max-w-2xl mx-auto uppercase tracking-[0.18em] px-2 xs:px-4"
|
||
style={{ fontWeight: 700 }}>
|
||
{t('home.subtitle')}
|
||
</p>
|
||
<p className="font-condensed text-[clamp(0.75rem,2.5vw,1rem)] text-gray-400 max-w-lg mx-auto px-2 xs:px-4 mb-6 xs:mb-8 tracking-wide italic">
|
||
{t('home.description')}
|
||
</p>
|
||
|
||
<div className="flex flex-wrap items-center justify-center gap-2 mb-6 xs:mb-8 px-2">
|
||
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-yellow-500/20 border border-yellow-500/50">
|
||
<span className="text-yellow-400 font-bold text-xs uppercase">
|
||
⚠️ {t('home.betaWarning')}
|
||
</span>
|
||
</div>
|
||
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-blue-500/20 border border-blue-500/50">
|
||
<span className="text-blue-400 font-bold text-xs uppercase">
|
||
📱 {t('home.pwaHint')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col xs:flex-row items-stretch xs:items-center justify-center gap-2 xs:gap-3 sm:gap-4 mb-6 px-2 xs:px-4">
|
||
<Link to="/map" className="w-full xs:w-auto">
|
||
<Button
|
||
size="lg"
|
||
className="w-full xs:w-auto gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white border-2 border-[#ff0040] shadow-[0_0_20px_rgba(255,0,64,0.5)] font-bold uppercase px-5 sm:px-8 py-3 xs:py-4 sm:py-6 text-sm sm:text-base h-auto"
|
||
>
|
||
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
||
{t('home.exploreMap')}
|
||
</Button>
|
||
</Link>
|
||
{user && (
|
||
<Link to="/create-spot" className="w-full xs:w-auto">
|
||
<Button
|
||
size="lg"
|
||
variant="outline"
|
||
className="w-full xs:w-auto gap-2 border-2 border-[#ccff00] bg-transparent hover:bg-[#ccff00]/10 text-[#ccff00] shadow-[0_0_15px_rgba(204,255,0,0.3)] font-bold uppercase px-5 sm:px-8 py-3 xs:py-4 sm:py-6 text-sm sm:text-base h-auto"
|
||
>
|
||
<Plus className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
||
{t('home.addSpot')}
|
||
</Button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
|
||
{/* Login/Signup Area - Prominent for logged out users (desktop only, mobile has it in header) */}
|
||
{!user && (
|
||
<div className="px-4 mt-6 hidden md:block">
|
||
<div className="max-w-md mx-auto p-5 sm:p-6 rounded-2xl bg-black/80 backdrop-blur-lg border-2 border-[#ccff00]/50 shadow-[0_0_30px_rgba(204,255,0,0.15)]">
|
||
<p className="text-center text-gray-200 mb-5 text-sm sm:text-base font-medium">
|
||
Join the community to add spots, create events & more
|
||
</p>
|
||
<LoginArea className="flex flex-row gap-3 w-full justify-center" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function FeaturedSpots() {
|
||
const { data: spots, isLoading } = useSkateSpots(8);
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<section className="bg-black py-16 border-t border-[#ff0040]/30">
|
||
<div className="container px-4">
|
||
<div className="flex items-center justify-between mb-10">
|
||
<div>
|
||
<h2 className="font-display text-4xl md:text-5xl neon-red mb-1 tracking-wide">
|
||
{t('home.featuredSpots')}
|
||
</h2>
|
||
<p className="font-condensed text-gray-400 uppercase text-sm tracking-[0.15em]" style={{ fontWeight: 600 }}>
|
||
{t('home.featuredSpotsDesc')}
|
||
</p>
|
||
</div>
|
||
<Link to="/map">
|
||
<Button variant="ghost" className="gap-2 text-[#ff0040] hover:text-[#ff0040]/80 hover:bg-[#ff0040]/10">
|
||
{t('common.viewAll')}
|
||
<ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 stagger-children">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<SpotCardSkeleton key={i} />
|
||
))}
|
||
</div>
|
||
) : spots && spots.length > 0 ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 stagger-children">
|
||
{spots.slice(0, 4).map((spot) => (
|
||
<SpotCard key={spot.id} spot={spot} />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<Card className="border-2 border-dashed border-[#ff0040]/30 bg-black/50">
|
||
<CardContent className="py-12 text-center">
|
||
<MapPin className="h-12 w-12 mx-auto mb-4 text-[#ff0040]/50" />
|
||
<p className="text-gray-400 mb-4 uppercase font-bold tracking-wide">
|
||
{t('home.noSpotsYet')}
|
||
</p>
|
||
<Link to="/create-spot">
|
||
<Button className="gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90 font-bold uppercase">
|
||
<Plus className="h-4 w-4" />
|
||
{t('home.addSpot')}
|
||
</Button>
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function UpcomingEvents() {
|
||
const { data: events, isLoading } = useUpcomingSkateEvents(4);
|
||
const { t } = useTranslation();
|
||
|
||
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-10">
|
||
<div>
|
||
<h2 className="font-display text-4xl md:text-5xl neon-lime mb-1 tracking-wide">
|
||
{t('home.upcomingEvents')}
|
||
</h2>
|
||
<p className="font-condensed text-gray-400 uppercase text-sm tracking-[0.15em]" style={{ fontWeight: 600 }}>
|
||
{t('home.upcomingEventsDesc')}
|
||
</p>
|
||
</div>
|
||
<Link to="/events">
|
||
<Button variant="ghost" className="gap-2 text-[#ccff00] hover:text-[#ccff00]/80 hover:bg-[#ccff00]/10">
|
||
{t('common.viewAll')}
|
||
<ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<EventCardSkeleton key={i} compact />
|
||
))}
|
||
</div>
|
||
) : events && events.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||
{events.map((event) => (
|
||
<EventCard key={event.id} event={event} compact />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<Card className="border-2 border-dashed border-[#ccff00]/30 bg-black/50">
|
||
<CardContent className="py-12 text-center">
|
||
<Calendar className="h-12 w-12 mx-auto mb-4 text-[#ccff00]/50" />
|
||
<p className="text-gray-400 mb-4 uppercase font-bold tracking-wide">
|
||
{t('home.noEventsYet')}
|
||
</p>
|
||
<Link to="/create-event">
|
||
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase">
|
||
<Plus className="h-4 w-4" />
|
||
{t('home.createEvent')}
|
||
</Button>
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
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() {
|
||
const { data: profiles, isLoading } = useLabProfiles(100);
|
||
const { t } = useTranslation();
|
||
|
||
// Get 2 coaches and 2 filmers
|
||
const coaches = profiles?.filter(p => p.profileType === 'coach').slice(0, 2) || [];
|
||
const filmers = profiles?.filter(p => p.profileType === 'filmer').slice(0, 2) || [];
|
||
const displayProfiles = [...coaches, ...filmers];
|
||
|
||
return (
|
||
<section className="bg-black py-16 border-t border-[#ff6600]/30">
|
||
<div className="container px-4">
|
||
<div className="flex items-center justify-between mb-10">
|
||
<div>
|
||
<h2 className="font-display text-4xl md:text-5xl mb-1 tracking-wide">
|
||
<span className="neon-red">THE</span> <span className="neon-orange">LAB</span>
|
||
</h2>
|
||
<p className="font-condensed text-gray-400 uppercase text-sm tracking-[0.15em]" style={{ fontWeight: 600 }}>
|
||
{t('home.theLabDesc')}
|
||
</p>
|
||
</div>
|
||
<Link to="/lab">
|
||
<Button variant="ghost" className="gap-2 text-[#ff6600] hover:text-[#ff6600]/80 hover:bg-[#ff6600]/10">
|
||
{t('common.viewAll')}
|
||
<ChevronRight className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<Card key={i} className="bg-black/90 border-2 border-[#ff6600]/20">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-start gap-4">
|
||
{/* Avatar skeleton */}
|
||
<div className="h-16 w-16 rounded-full bg-gray-800 animate-pulse flex-shrink-0" />
|
||
<div className="flex-1 min-w-0 space-y-3">
|
||
{/* Name skeleton */}
|
||
<div className="flex items-center gap-2">
|
||
<div className="h-5 w-28 bg-gray-800 animate-pulse rounded" />
|
||
<div className="h-5 w-5 bg-gray-800 animate-pulse rounded" />
|
||
</div>
|
||
{/* Location skeleton */}
|
||
<div className="h-3 w-24 bg-gray-800 animate-pulse rounded" />
|
||
{/* Bio skeleton */}
|
||
<div className="space-y-1.5">
|
||
<div className="h-3 w-full bg-gray-800 animate-pulse rounded" />
|
||
<div className="h-3 w-3/4 bg-gray-800 animate-pulse rounded" />
|
||
</div>
|
||
{/* Skills skeleton */}
|
||
<div className="flex gap-1.5">
|
||
<div className="h-5 w-14 bg-gray-800 animate-pulse rounded-full" />
|
||
<div className="h-5 w-16 bg-gray-800 animate-pulse rounded-full" />
|
||
<div className="h-5 w-12 bg-gray-800 animate-pulse rounded-full" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
) : displayProfiles.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||
{displayProfiles.map((profile) => (
|
||
<LabProfileCard key={profile.id} profile={profile} />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<Card className="border-2 border-dashed border-[#ff6600]/30 bg-black/50">
|
||
<CardContent className="py-12 text-center">
|
||
<GraduationCap className="h-12 w-12 mx-auto mb-4 text-[#ff6600]/50" />
|
||
<p className="text-gray-400 mb-4 uppercase font-bold tracking-wide">
|
||
{t('lab.noCoaches')}
|
||
</p>
|
||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||
<Link to="/become/coach">
|
||
<Button className="gap-2 bg-red-500 hover:bg-red-600 font-bold uppercase">
|
||
<GraduationCap className="h-4 w-4" />
|
||
{t('home.becomeCoach')}
|
||
</Button>
|
||
</Link>
|
||
<Link to="/become/filmer">
|
||
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 font-bold uppercase">
|
||
<Video className="h-4 w-4" />
|
||
{t('home.becomeFilmer')}
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function LabProfileCard({ profile }: { profile: LabProfile }) {
|
||
const author = useAuthor(profile.pubkey);
|
||
const metadata = author.data?.metadata;
|
||
const displayName = metadata?.name ?? genUserName(profile.pubkey);
|
||
const avatar = profile.image || metadata?.picture;
|
||
|
||
const isCoach = profile.profileType === 'coach';
|
||
const accentColor = isCoach ? '#ff0040' : '#ff6600';
|
||
|
||
return (
|
||
<Link to={`/coach/${profile.pubkey}/${profile.identifier}`}>
|
||
<Card className="bg-black/90 border-2 hover:shadow-2xl transition-all h-full backdrop-blur" style={{
|
||
borderColor: `${accentColor}40`,
|
||
}}>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-start gap-4">
|
||
<Avatar className="h-16 w-16 border-2" style={{ borderColor: `${accentColor}50` }}>
|
||
<AvatarImage src={avatar} alt={displayName} />
|
||
<AvatarFallback className="text-lg font-bold" style={{
|
||
backgroundColor: `${accentColor}20`,
|
||
color: accentColor
|
||
}}>
|
||
{displayName.slice(0, 2).toUpperCase()}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<h3 className="font-black text-lg uppercase truncate" style={{ color: accentColor }}>
|
||
{displayName}
|
||
</h3>
|
||
{isCoach ? (
|
||
<GraduationCap className="h-5 w-5 flex-shrink-0" style={{ color: accentColor }} />
|
||
) : (
|
||
<Video className="h-5 w-5 flex-shrink-0" style={{ color: accentColor }} />
|
||
)}
|
||
</div>
|
||
|
||
{profile.location && (
|
||
<p className="text-sm text-gray-400 flex items-center gap-1 mb-2">
|
||
<MapPin className="h-3 w-3" style={{ color: accentColor }} />
|
||
{profile.location}
|
||
</p>
|
||
)}
|
||
|
||
{profile.bio && (
|
||
<p className="text-sm text-gray-300 line-clamp-2 mb-3">
|
||
{profile.bio}
|
||
</p>
|
||
)}
|
||
|
||
{profile.skills.length > 0 && (
|
||
<div className="flex flex-wrap gap-1">
|
||
{profile.skills.slice(0, 3).map((skill: string) => (
|
||
<Badge key={skill} variant="secondary" className="text-xs bg-gray-800 border-gray-700">
|
||
{skill}
|
||
</Badge>
|
||
))}
|
||
{profile.skills.length > 3 && (
|
||
<Badge variant="secondary" className="text-xs bg-gray-800 border-gray-700">
|
||
+{profile.skills.length - 3}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function NewToNostrSection() {
|
||
const { user } = useCurrentUser();
|
||
|
||
// Don't show this section if user is already logged in
|
||
if (user) return null;
|
||
|
||
return (
|
||
<section className="bg-black py-12 md:py-20 border-t border-[#ccff00]/30">
|
||
<div className="container px-4">
|
||
<div className="relative overflow-hidden rounded-2xl border-2 border-[#ccff00]/50 bg-black/50 backdrop-blur">
|
||
{/* Neon glow */}
|
||
<div className="absolute inset-0 bg-[#ccff00]/5 blur-xl" />
|
||
|
||
<div className="relative py-12 md:py-16 px-4 md:px-12 text-center">
|
||
<h2 className="font-display text-5xl md:text-7xl neon-lime mb-4 tracking-wide">
|
||
NEW HERE?
|
||
</h2>
|
||
<p className="text-gray-300 max-w-2xl mx-auto mb-6 md:mb-8 text-base md:text-lg px-4">
|
||
Create a free account to add spots, join events, connect with skaters, and become part of the community. No email required.
|
||
</p>
|
||
|
||
{/* Founder Follow - prominent placement */}
|
||
<div className="flex items-center justify-center gap-6 mb-8">
|
||
<div className="flex flex-col items-center">
|
||
<FounderFollow size="lg" showBadge={true} />
|
||
</div>
|
||
<div className="text-left max-w-xs hidden sm:block">
|
||
<div className="flex items-center gap-2 text-[#ff6600] font-bold text-sm mb-1">
|
||
<Crown className="h-4 w-4" />
|
||
Follow the Founder
|
||
</div>
|
||
<p className="text-gray-400 text-xs">
|
||
Hold the avatar for 3 seconds to follow and stay updated on new features and events!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<Link to="/onboarding">
|
||
<Button size="lg" className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 border-2 border-[#ccff00] shadow-[0_0_20px_rgba(204,255,0,0.5)] font-bold uppercase px-10 py-6 text-lg">
|
||
<CheckCircle2 className="h-6 w-6" />
|
||
Create Account
|
||
</Button>
|
||
</Link>
|
||
<p className="text-gray-500 text-xs mt-4">
|
||
Your account works across all apps in the network
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function CTASection() {
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<section className="bg-black py-20 border-t border-[#ff6600]/30">
|
||
<div className="container px-4">
|
||
<div className="relative overflow-hidden rounded-2xl border-2 border-[#ff6600]/50">
|
||
{/* Neon glows */}
|
||
<div className="absolute top-0 right-0 w-96 h-96 bg-[#ff0040] opacity-20 rounded-full blur-[120px]" />
|
||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-[#ccff00] opacity-15 rounded-full blur-[100px]" />
|
||
|
||
<div className="relative py-16 px-6 md:px-12 text-center bg-black/50 backdrop-blur">
|
||
<h2 className="font-display text-5xl md:text-7xl neon-orange mb-4 tracking-wide">
|
||
{t('home.readyToShare')}
|
||
</h2>
|
||
<p className="text-gray-300 max-w-2xl mx-auto mb-8 text-lg">
|
||
{t('home.readyToShareDesc')}
|
||
</p>
|
||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||
<Link to="/create-spot">
|
||
<Button size="lg" className="gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white border-2 border-[#ff0040] shadow-[0_0_20px_rgba(255,0,64,0.5)] font-bold uppercase px-8">
|
||
<Plus className="h-5 w-5" />
|
||
{t('home.addSpot')}
|
||
</Button>
|
||
</Link>
|
||
<Link to="/create-event">
|
||
<Button size="lg" variant="outline" className="gap-2 border-2 border-[#ccff00] bg-transparent hover:bg-[#ccff00]/10 text-[#ccff00] shadow-[0_0_15px_rgba(204,255,0,0.3)] font-bold uppercase px-8">
|
||
<Calendar className="h-5 w-5" />
|
||
{t('home.createEvent')}
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function Footer() {
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<footer className="border-t border-[#ff0040]/30 bg-black">
|
||
<div className="container px-4 py-10">
|
||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||
<div className="flex items-center gap-3">
|
||
<img
|
||
src="/images/icon-1024.png"
|
||
alt="SkateSpots"
|
||
className="w-10 h-10 rounded-lg shadow-[0_0_20px_rgba(255,0,64,0.5)]"
|
||
/>
|
||
<span className="font-display text-2xl neon-red tracking-wide" style={{
|
||
textShadow: '0 0 10px #ff0040'
|
||
}}>SkateSpots</span>
|
||
</div>
|
||
|
||
<p className="text-sm text-gray-400 text-center uppercase tracking-wide">
|
||
Open Source • Community Owned • No Ads
|
||
</p>
|
||
|
||
<div className="flex items-center gap-6 text-sm">
|
||
<Link to="/faq" className="text-gray-400 hover:text-[#ff0040] transition-colors uppercase font-bold tracking-wide">
|
||
FAQ
|
||
</Link>
|
||
<Link to="/about" className="text-gray-400 hover:text-[#ff0040] transition-colors uppercase font-bold tracking-wide">
|
||
{t('nav.about')}
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
);
|
||
}
|
||
|
||
export default function Index() {
|
||
useSeoMeta({
|
||
title: 'SkateSpots - Discover Skateboarding Locations Worldwide',
|
||
description:
|
||
'Find the best skateboarding spots, connect with fellow skaters, and discover events near you. Built on Nostr for true decentralization.',
|
||
});
|
||
|
||
// Platform sponsors - currently empty (invisible placeholder)
|
||
// When sponsors are added, pass them here: sponsors={[{name: "Vans", logo: "...", tier: "title"}]}
|
||
const platformSponsors = undefined;
|
||
|
||
return (
|
||
<Layout>
|
||
<HeroSection />
|
||
<FeaturedSpots />
|
||
<TopZappedSection />
|
||
<UpcomingEvents />
|
||
<TheLabSection />
|
||
<NewToNostrSection />
|
||
<CTASection />
|
||
{/* Sponsor slot - invisible until sponsors are added */}
|
||
<SponsorSlot
|
||
size="banner"
|
||
sponsors={platformSponsors}
|
||
title="Platform Partners"
|
||
/>
|
||
<Footer />
|
||
</Layout>
|
||
);
|
||
}
|