Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { CalendarDays, ChevronLeft, ChevronRight, MapPin, Plus, Users } from 'lucide-react';
|
||||
|
||||
import {
|
||||
CalendarDays,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MapPin,
|
||||
Plus,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createEventManagementApi } from '@kit/event-management/api';
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createEventManagementApi } from '@kit/event-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { StatsCard } from '~/components/stats-card';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { EVENT_STATUS_VARIANT, EVENT_STATUS_LABEL } from '~/lib/status-badges';
|
||||
|
||||
interface PageProps {
|
||||
@@ -40,14 +47,14 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
const events = await api.listEvents(acct.id, { page });
|
||||
|
||||
// Fetch registration counts for all events on this page
|
||||
const eventIds = events.data.map((e: Record<string, unknown>) => String(e.id));
|
||||
const eventIds = events.data.map((e: Record<string, unknown>) =>
|
||||
String(e.id),
|
||||
);
|
||||
const registrationCounts = await api.getRegistrationCounts(eventIds);
|
||||
|
||||
// Pre-compute stats before rendering
|
||||
const uniqueLocationCount = new Set(
|
||||
events.data
|
||||
.map((e: Record<string, unknown>) => e.location)
|
||||
.filter(Boolean),
|
||||
events.data.map((e: Record<string, unknown>) => e.location).filter(Boolean),
|
||||
).size;
|
||||
|
||||
const totalCapacity = events.data.reduce(
|
||||
@@ -63,9 +70,7 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{t('title')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-muted-foreground">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<Link href={`/home/${account}/events/new`}>
|
||||
@@ -107,19 +112,31 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('allEvents')} ({events.total})</CardTitle>
|
||||
<CardTitle>
|
||||
{t('allEvents')} ({events.total})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">{t('name')}</th>
|
||||
<th className="p-3 text-left font-medium">{t('eventDate')}</th>
|
||||
<th className="p-3 text-left font-medium">{t('eventLocation')}</th>
|
||||
<th className="p-3 text-right font-medium">{t('capacity')}</th>
|
||||
<th className="p-3 text-left font-medium">{t('status')}</th>
|
||||
<th className="p-3 text-right font-medium">{t('registrations')}</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
{t('eventDate')}
|
||||
</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
{t('eventLocation')}
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
{t('capacity')}
|
||||
</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
{t('status')}
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
{t('registrations')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -130,7 +147,7 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
return (
|
||||
<tr
|
||||
key={eventId}
|
||||
className="border-b hover:bg-muted/30"
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">
|
||||
<Link
|
||||
@@ -141,9 +158,7 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
</Link>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{event.event_date
|
||||
? new Date(String(event.event_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(event.event_date as string)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{String(event.location ?? '—')}
|
||||
@@ -156,10 +171,12 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
<td className="p-3">
|
||||
<Badge
|
||||
variant={
|
||||
EVENT_STATUS_VARIANT[String(event.status)] ?? 'secondary'
|
||||
EVENT_STATUS_VARIANT[String(event.status)] ??
|
||||
'secondary'
|
||||
}
|
||||
>
|
||||
{EVENT_STATUS_LABEL[String(event.status)] ?? String(event.status)}
|
||||
{EVENT_STATUS_LABEL[String(event.status)] ??
|
||||
String(event.status)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-right font-medium">
|
||||
@@ -175,12 +192,17 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
{/* Pagination */}
|
||||
{events.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('paginationPage', { page: events.page, totalPages: events.totalPages })}
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t('paginationPage', {
|
||||
page: events.page,
|
||||
totalPages: events.totalPages,
|
||||
})}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
{events.page > 1 && (
|
||||
<Link href={`/home/${account}/events?page=${events.page - 1}`}>
|
||||
<Link
|
||||
href={`/home/${account}/events?page=${events.page - 1}`}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
{t('paginationPrevious')}
|
||||
@@ -188,7 +210,9 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
||||
</Link>
|
||||
)}
|
||||
{events.page < events.totalPages && (
|
||||
<Link href={`/home/${account}/events?page=${events.page + 1}`}>
|
||||
<Link
|
||||
href={`/home/${account}/events?page=${events.page + 1}`}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
{t('paginationNext')}
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user