feat(client): event list

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-01 14:26:35 +08:00
committed by Asai Neko
parent cee71097af
commit b7ac942807
20 changed files with 600 additions and 49 deletions

View File

@@ -0,0 +1,40 @@
import { Calendar } from 'lucide-react';
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '../ui/badge';
import { Skeleton } from '../ui/skeleton';
export function EventCardSkeleton() {
return (
<Card className="relative mx-auto w-full max-w-sm pt-0">
<div className="absolute inset-0 z-30 aspect-video bg-black/10" />
<Skeleton
className="relative z-20 aspect-video w-full object-cover rounded-t-xl"
/>
<CardHeader>
<CardAction>
<Badge variant="secondary" className="bg-accent animate-pulse text-accent select-none">Official</Badge>
</CardAction>
<CardTitle>
<Skeleton className="h-4 max-w-48" />
</CardTitle>
<CardDescription className="flex flex-row items-center text-xs">
<Calendar className="size-4 mr-2" />
<Skeleton className="h-4 w-24" />
</CardDescription>
<CardDescription className="mt-1">
<Skeleton className="h-5 max-w-64" />
</CardDescription>
</CardHeader>
<CardFooter>
<Skeleton className="h-9 px-4 py-2 w-full"></Skeleton>
</CardFooter>
</Card>
);
}

View File

@@ -1,3 +1,4 @@
import type { EventInfo } from './types';
import dayjs from 'dayjs';
import { Calendar } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
@@ -10,16 +11,9 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Skeleton } from '../ui/skeleton';
export function EventCardView({ type, coverImage, eventName, description, startTime, endTime }:
{
type: 'official' | 'party';
coverImage: string;
eventName: string;
description: string;
startTime: Date;
endTime: Date;
}) {
export function EventCardView({ type, coverImage, eventName, description, startTime, endTime, onJoinEvent }: EventInfo) {
const startDayJs = dayjs(startTime);
const endDayJs = dayjs(endTime);
return (
@@ -30,6 +24,9 @@ export function EventCardView({ type, coverImage, eventName, description, startT
alt="Event cover"
className="relative z-20 aspect-video w-full object-cover rounded-t-xl"
/>
<Skeleton
className="absolute z-15 aspect-video w-full object-cover rounded-t-xl"
/>
<CardHeader>
<CardAction>
{type === 'official' ? <Badge variant="secondary">Official</Badge> : <Badge variant="destructive">Party</Badge>}
@@ -42,10 +39,9 @@ export function EventCardView({ type, coverImage, eventName, description, startT
<CardDescription className="mt-1">
{description}
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full"></Button>
<Button className="w-full" onClick={onJoinEvent}></Button>
</CardFooter>
</Card>
);

View File

@@ -0,0 +1,23 @@
import type { EventInfo } from './types';
import PlaceholderImage from '@/assets/event-placeholder.png';
import { useGetEvents } from '@/hooks/data/useGetEvents';
import { useJoinEvent } from '@/hooks/data/useJoinEvent';
import { EventGridView } from './event-grid.view';
export function EventGridContainer() {
const { data, isLoading } = useGetEvents();
const { mutate } = useJoinEvent();
const allEvents: EventInfo[] = isLoading
? []
: data.pages.flatMap(page => page.data!).map(it => ({
type: it.type! as EventInfo['type'],
coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!,
description: it.description!,
startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!),
onJoinEvent: () => mutate({ body: { event_id: it.event_id } }),
} satisfies EventInfo));
return <EventGridView events={allEvents} />;
}

View File

@@ -0,0 +1,12 @@
import { EventCardSkeleton } from './event-card.skeleton';
export function EventGridSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<EventCardSkeleton key={i} />
))}
</div>
);
}

View File

@@ -0,0 +1,12 @@
import type { EventInfo } from './types';
import { EventCardView } from './event-card.view';
export function EventGridView({ events }: { events: EventInfo[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{events.map(event => (
<EventCardView key={event.eventName} {...event} />
))}
</div>
);
}

View File

@@ -0,0 +1,9 @@
export interface EventInfo {
type: 'official' | 'party';
coverImage: string;
eventName: string;
description: string;
startTime: Date;
endTime: Date;
onJoinEvent: () => void;
}

View File

@@ -1,9 +0,0 @@
import { Skeleton } from '../ui/skeleton';
export function CardSkeleton() {
return (
<Skeleton
className="gap-6 rounded-xl py-6 h-full"
/>
);
}