SkateSpots/src/components/HostingDashboard.tsx
SkateSpots 9357386bbd i18n(es): batch 3 part 2 — locale-aware dates & relative times
- New src/lib/dateFnsI18n.ts re-exports date-fns with formatDistanceToNow /
  formatDistanceToNowStrict / formatDistance / format defaulting to the active
  locale, plus dateLocaleCode() for native toLocale{Date,Time}String calls.
- LanguageProvider keeps the locale in sync during render, so the first paint is
  already in-language.
- Codemod: 11 date-fns imports now use the shim; ~39 native date/time calls pass
  the app locale code instead of undefined/[]/hardcoded 'en-US'. Number
  .toLocaleString() calls are deliberately untouched.
2026-10-02 15:19:38 +07:00

172 lines
6.2 KiB
TypeScript

import { dateLocaleCode } from '@/lib/dateFnsI18n';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, Users, Pencil, Megaphone, Plus, CalendarClock } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useSkateEvents } from '@/hooks/useSkateEvents';
import { useEventRsvps, eventCoord } from '@/hooks/useEventRsvps';
import { cn } from '@/lib/utils';
import type { SkateEvent } from '@/lib/types';
function formatWhen(ts: number): string {
return new Date(ts * 1000).toLocaleDateString(dateLocaleCode(), {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}
/** A single hosted-event row with live RSVP counts + quick actions. */
function HostedEventRow({ event, isPast }: { event: SkateEvent; isPast: boolean }) {
const { goingCount, tentativeCount } = useEventRsvps(eventCoord(event));
const to = `/event/${event.pubkey}/${event.identifier}`;
return (
<Card className={cn('border-white/10 transition-colors hover:border-[#ccff00]/40', isPast && 'opacity-70')}>
<CardContent className="flex items-center gap-4 p-4">
{/* Date chip */}
<div className="flex-shrink-0 w-14 text-center">
<div className="rounded-lg border-2 border-[#ccff00]/40 bg-[#ccff00]/10 p-1.5">
<div className="text-[10px] font-black uppercase text-[#ccff00]">
{new Date(event.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short' })}
</div>
<div className="text-xl font-black text-white leading-none">
{new Date(event.start * 1000).getDate()}
</div>
</div>
</div>
{/* Info */}
<Link to={to} className="min-w-0 flex-1">
<h3 className="truncate font-bold uppercase tracking-wide text-white hover:text-[#ccff00]">
{event.title}
</h3>
<p className="flex items-center gap-1 text-xs text-gray-500">
<CalendarClock className="h-3 w-3" />
{formatWhen(event.start)}
</p>
<div className="mt-1 flex items-center gap-3 text-xs">
<span className="flex items-center gap-1 font-bold text-green-400">
<Users className="h-3 w-3" />
{goingCount} going
</span>
{tentativeCount > 0 && (
<span className="text-[#ccff00]">{tentativeCount} interested</span>
)}
</div>
</Link>
{/* Quick actions */}
<div className="flex flex-shrink-0 items-center gap-1.5">
<Link to={to} title="Post update">
<Button size="icon" variant="ghost" className="h-8 w-8 text-gray-400 hover:text-[#ccff00]">
<Megaphone className="h-4 w-4" />
</Button>
</Link>
<Link to={`/edit-event/${event.pubkey}/${event.identifier}`} title="Edit">
<Button size="icon" variant="ghost" className="h-8 w-8 text-gray-400 hover:text-white">
<Pencil className="h-4 w-4" />
</Button>
</Link>
</div>
</CardContent>
</Card>
);
}
/**
* "Hosting" dashboard — the events the logged-in user created, split into
* upcoming and past, each with live RSVP counts and quick edit/update actions.
* The host's home base.
*/
export function HostingDashboard() {
const { user } = useCurrentUser();
const { data: events, isLoading } = useSkateEvents(200);
const now = Math.floor(Date.now() / 1000);
const mine = useMemo(() => {
if (!user || !events) return { upcoming: [], past: [] };
const owned = events.filter((e) => e.pubkey === user.pubkey);
return {
upcoming: owned.filter((e) => e.start >= now).sort((a, b) => a.start - b.start),
past: owned.filter((e) => e.start < now).sort((a, b) => b.start - a.start),
};
}, [user, events, now]);
if (!user) {
return (
<Card className="border-dashed">
<CardContent className="py-12 text-center">
<Calendar className="mx-auto mb-3 h-8 w-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">Log in to see the events you're hosting.</p>
</CardContent>
</Card>
);
}
if (isLoading) {
return (
<div className="space-y-3">
{[0, 1, 2].map((i) => (
<Skeleton key={i} className="h-20 w-full rounded-xl" />
))}
</div>
);
}
const total = mine.upcoming.length + mine.past.length;
if (total === 0) {
return (
<Card className="border-dashed">
<CardContent className="py-12 text-center">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-[#ccff00]/10">
<Calendar className="h-7 w-7 text-[#ccff00]" />
</div>
<h3 className="mb-1 font-black uppercase tracking-wide text-white">You're not hosting anything yet</h3>
<p className="mx-auto mb-5 max-w-xs text-sm text-muted-foreground">
Run a jam, comp or session. Create an event and skaters can RSVP, get updates, and roll through.
</p>
<Link to="/create-event">
<Button className="gap-2 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90">
<Plus className="h-4 w-4" />
Create an event
</Button>
</Link>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{mine.upcoming.length > 0 && (
<div className="space-y-2.5">
<h2 className="text-xs font-black uppercase tracking-widest text-[#ccff00]">
Upcoming · {mine.upcoming.length}
</h2>
{mine.upcoming.map((e) => (
<HostedEventRow key={e.id} event={e} isPast={false} />
))}
</div>
)}
{mine.past.length > 0 && (
<div className="space-y-2.5">
<h2 className="text-xs font-black uppercase tracking-widest text-gray-500">
Past · {mine.past.length}
</h2>
{mine.past.map((e) => (
<HostedEventRow key={e.id} event={e} isPast />
))}
</div>
)}
</div>
);
}