- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust" signal (reuses batch trust scores); replaces the plain avatar stack - Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only composer with update/reschedule/relocation/cancelled categories that p-tags every RSVPer so they get notified; public feed on the event page - EditEventPage: auto-post a reschedule/relocation update to notify attendees when the host changes time or location - Activity bell: new 'event-update' notification type for RSVPers - EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the user's upcoming/past events, live RSVP counts, and quick edit/update actions - Document Event Update kind 7103 in NIP.md Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
171 lines
6.2 KiB
TypeScript
171 lines
6.2 KiB
TypeScript
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(undefined, {
|
|
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(undefined, { 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>
|
|
);
|
|
}
|