fix: add missing newlines at the end of JSON files; clean up formatting in page components
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 17m4s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-02 11:02:58 +02:00
parent b26e5aaafa
commit c6d564836f
56 changed files with 471 additions and 381 deletions

View File

@@ -54,10 +54,7 @@ async function AuditPage(props: AdminAuditPageProps) {
return ( return (
<PageBody> <PageBody>
<PageHeader <PageHeader title={t('title')} description={t('description')} />
title={t('title')}
description={t('description')}
/>
<div className="space-y-4"> <div className="space-y-4">
{/* Filters */} {/* Filters */}

View File

@@ -75,7 +75,9 @@ export default async function PortalInvitePage({
<Shield className="mx-auto mb-4 h-10 w-10 text-amber-500" /> <Shield className="mx-auto mb-4 h-10 w-10 text-amber-500" />
<h2 className="text-lg font-bold">{t('invite.expiredTitle')}</h2> <h2 className="text-lg font-bold">{t('invite.expiredTitle')}</h2>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
{t('invite.expiredDesc', { date: formatDate(invitation.expires_at) })} {t('invite.expiredDesc', {
date: formatDate(invitation.expires_at),
})}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -179,13 +179,17 @@ export function PortalLinkedAccounts({ slug }: { slug: string }) {
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('linkedAccounts.title')}</AlertDialogTitle> <AlertDialogTitle>
{t('linkedAccounts.title')}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('linkedAccounts.disconnectDesc')} {t('linkedAccounts.disconnectDesc')}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t('linkedAccounts.cancel')}</AlertDialogCancel> <AlertDialogCancel>
{t('linkedAccounts.cancel')}
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => handleUnlink(identity)} onClick={() => handleUnlink(identity)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"

View File

@@ -3,13 +3,7 @@ import { redirect } from 'next/navigation';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { import { UserCircle, Mail, MapPin, Shield, Link2 } from 'lucide-react';
UserCircle,
Mail,
MapPin,
Shield,
Link2,
} from 'lucide-react';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';

View File

@@ -99,7 +99,12 @@ export default async function BookingDetailPage({ params }: PageProps) {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('detail.backToBookings')}> <Button
variant="ghost"
size="icon"
asChild
aria-label={t('detail.backToBookings')}
>
<Link href={`/home/${account}/bookings`}> <Link href={`/home/${account}/bookings`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" /> <ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link> </Link>

View File

@@ -1,13 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react'; import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createBookingManagementApi } from '@kit/booking-management/api'; import { createBookingManagementApi } from '@kit/booking-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
@@ -139,14 +139,17 @@ export default async function BookingCalendarPage({ params }: PageProps) {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.backToBookings')}> <Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.backToBookings')}
>
<Link href={`/home/${account}/bookings`}> <Link href={`/home/${account}/bookings`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" /> <ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
</Button> </Button>
<p className="text-muted-foreground"> <p className="text-muted-foreground">{t('calendar.subtitle')}</p>
{t('calendar.subtitle')}
</p>
</div> </div>
</div> </div>
@@ -154,13 +157,23 @@ export default async function BookingCalendarPage({ params }: PageProps) {
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" size="icon" disabled aria-label={t('calendar.previousMonth')}> <Button
variant="ghost"
size="icon"
disabled
aria-label={t('calendar.previousMonth')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" /> <ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
<CardTitle> <CardTitle>
{MONTH_NAMES[month]} {year} {MONTH_NAMES[month]} {year}
</CardTitle> </CardTitle>
<Button variant="ghost" size="icon" disabled aria-label={t('calendar.nextMonth')}> <Button
variant="ghost"
size="icon"
disabled
aria-label={t('calendar.nextMonth')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" /> <ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
</div> </div>
@@ -232,7 +245,10 @@ export default async function BookingCalendarPage({ params }: PageProps) {
<p className="text-2xl font-bold">{bookings.data.length}</p> <p className="text-2xl font-bold">{bookings.data.length}</p>
</div> </div>
<Badge variant="outline"> <Badge variant="outline">
{t('calendar.daysOccupied', { occupied: occupiedDates.size, total: daysInMonth })} {t('calendar.daysOccupied', {
occupied: occupiedDates.size,
total: daysInMonth,
})}
</Badge> </Badge>
</div> </div>
</CardContent> </CardContent>

View File

@@ -250,7 +250,10 @@ export default async function BookingsPage({
'secondary' 'secondary'
} }
> >
{t(STATUS_LABEL_KEYS[String(booking.status)] ?? String(booking.status))} {t(
STATUS_LABEL_KEYS[String(booking.status)] ??
String(booking.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { CreateCourseForm } from '@kit/course-management/components'; import { CreateCourseForm } from '@kit/course-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -88,8 +88,10 @@ export default async function CourseDetailPage({ params }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ?? {t(
String(courseData.status))} COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
String(courseData.status),
)}
</Badge> </Badge>
</div> </div>
</CardContent> </CardContent>

View File

@@ -129,7 +129,12 @@ export default async function CourseCalendarPage({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.backToCourses')}> <Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.backToCourses')}
>
<Link href={`/home/${account}/courses`}> <Link href={`/home/${account}/courses`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" /> <ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
@@ -142,7 +147,12 @@ export default async function CourseCalendarPage({
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.previousMonth')}> <Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.previousMonth')}
>
<Link <Link
href={`/home/${account}/courses/calendar?month=${ href={`/home/${account}/courses/calendar?month=${
month === 0 month === 0
@@ -156,7 +166,12 @@ export default async function CourseCalendarPage({
<CardTitle> <CardTitle>
{MONTH_NAMES[month]} {year} {MONTH_NAMES[month]} {year}
</CardTitle> </CardTitle>
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.nextMonth')}> <Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.nextMonth')}
>
<Link <Link
href={`/home/${account}/courses/calendar?month=${ href={`/home/${account}/courses/calendar?month=${
month === 11 month === 11

View File

@@ -196,7 +196,10 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(COURSE_STATUS_LABEL_KEYS[String(course.status)] ?? String(course.status))} {t(
COURSE_STATUS_LABEL_KEYS[String(course.status)] ??
String(course.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">

View File

@@ -18,7 +18,10 @@ import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
import { EVENT_STATUS_LABEL_KEYS, EVENT_STATUS_VARIANT } from '~/lib/status-badges'; import {
EVENT_STATUS_LABEL_KEYS,
EVENT_STATUS_VARIANT,
} from '~/lib/status-badges';
import { DeleteEventButton } from './delete-event-button'; import { DeleteEventButton } from './delete-event-button';
@@ -64,8 +67,10 @@ export default async function EventDetailPage({ params }: PageProps) {
} }
className="mt-1" className="mt-1"
> >
{t(EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ?? {t(
String(eventData.status))} EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ??
String(eventData.status),
)}
</Badge> </Badge>
</div> </div>
<Button> <Button>

View File

@@ -21,7 +21,10 @@ import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state'; import { EmptyState } from '~/components/empty-state';
import { StatsCard } from '~/components/stats-card'; import { StatsCard } from '~/components/stats-card';
import { EVENT_STATUS_VARIANT, EVENT_STATUS_LABEL_KEYS } from '~/lib/status-badges'; import {
EVENT_STATUS_VARIANT,
EVENT_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps { interface PageProps {
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -178,7 +181,10 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(EVENT_STATUS_LABEL_KEYS[String(event.status)] ?? String(event.status))} {t(
EVENT_STATUS_LABEL_KEYS[String(event.status)] ??
String(event.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3 text-right font-medium"> <td className="p-3 text-right font-medium">

View File

@@ -13,7 +13,10 @@ import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state'; import { EmptyState } from '~/components/empty-state';
import { StatsCard } from '~/components/stats-card'; import { StatsCard } from '~/components/stats-card';
import { EVENT_STATUS_VARIANT, EVENT_STATUS_LABEL_KEYS } from '~/lib/status-badges'; import {
EVENT_STATUS_VARIANT,
EVENT_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps { interface PageProps {
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -156,7 +159,10 @@ export default async function EventRegistrationsPage({ params }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(EVENT_STATUS_LABEL_KEYS[event.status] ?? event.status)} {t(
EVENT_STATUS_LABEL_KEYS[event.status] ??
event.status,
)}
</Badge> </Badge>
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">

View File

@@ -1,13 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/api'; import { createFinanceApi } from '@kit/finance/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
@@ -66,7 +66,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle> <CardTitle>
{t('invoices.invoiceLabel', { number: String(invoice.invoice_number ?? '') })} {t('invoices.invoiceLabel', {
number: String(invoice.invoice_number ?? ''),
})}
</CardTitle> </CardTitle>
<Badge variant={INVOICE_STATUS_VARIANT[status] ?? 'secondary'}> <Badge variant={INVOICE_STATUS_VARIANT[status] ?? 'secondary'}>
{t(INVOICE_STATUS_LABEL_KEYS[status] ?? status)} {t(INVOICE_STATUS_LABEL_KEYS[status] ?? status)}
@@ -125,7 +127,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
{/* Line Items */} {/* Line Items */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{t('invoiceForm.lineItems')} ({items.length})</CardTitle> <CardTitle>
{t('invoiceForm.lineItems')} ({items.length})
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{items.length === 0 ? ( {items.length === 0 ? (
@@ -140,11 +144,15 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
<th scope="col" className="p-3 text-left font-medium"> <th scope="col" className="p-3 text-left font-medium">
{t('invoiceForm.itemDescription')} {t('invoiceForm.itemDescription')}
</th> </th>
<th scope="col" className="p-3 text-right font-medium">{t('invoiceForm.quantity')}</th> <th scope="col" className="p-3 text-right font-medium">
{t('invoiceForm.quantity')}
</th>
<th scope="col" className="p-3 text-right font-medium"> <th scope="col" className="p-3 text-right font-medium">
{t('invoices.unitPriceCol')} {t('invoices.unitPriceCol')}
</th> </th>
<th scope="col" className="p-3 text-right font-medium">{t('invoices.totalCol')}</th> <th scope="col" className="p-3 text-right font-medium">
{t('invoices.totalCol')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -183,7 +191,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
</tr> </tr>
<tr> <tr>
<td colSpan={3} className="p-3 text-right font-medium"> <td colSpan={3} className="p-3 text-right font-medium">
{t('invoiceForm.tax', { rate: Number(invoice.tax_rate ?? 19) })} {t('invoiceForm.tax', {
rate: Number(invoice.tax_rate ?? 19),
})}
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">
{formatCurrency(invoice.tax_amount ?? 0)} {formatCurrency(invoice.tax_amount ?? 0)}

View File

@@ -216,8 +216,10 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ?? {t(
String(batch.status))} BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
String(batch.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3"> <td className="p-3">
@@ -318,7 +320,11 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(INVOICE_STATUS_LABEL_KEYS[String(invoice.status)] ?? String(invoice.status))} {t(
INVOICE_STATUS_LABEL_KEYS[
String(invoice.status)
] ?? String(invoice.status),
)}
</Badge> </Badge>
</td> </td>
</tr> </tr>
@@ -347,7 +353,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
</Link> </Link>
</Button> </Button>
) : ( ) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.previous')}> <Button
variant="outline"
size="sm"
disabled
aria-label={t('common.previous')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" /> <ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
)} )}
@@ -366,7 +377,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
</Link> </Link>
</Button> </Button>
) : ( ) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.next')}> <Button
variant="outline"
size="sm"
disabled
aria-label={t('common.next')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" /> <ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
)} )}

View File

@@ -1,13 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
import { Euro, CreditCard, TrendingUp, ArrowRight } from 'lucide-react'; import { Euro, CreditCard, TrendingUp, ArrowRight } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/api'; import { createFinanceApi } from '@kit/finance/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
@@ -84,9 +84,7 @@ export default async function PaymentsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Header */} {/* Header */}
<div> <div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">{t('payments.subtitle')}</p>
{t('payments.subtitle')}
</p>
</div> </div>
{/* Stats */} {/* Stats */}
@@ -121,7 +119,9 @@ export default async function PaymentsPage({ params }: PageProps) {
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">{t('payments.openInvoices')}</CardTitle> <CardTitle className="text-base">
{t('payments.openInvoices')}
</CardTitle>
<Badge <Badge
variant={openInvoices.length > 0 ? 'default' : 'secondary'} variant={openInvoices.length > 0 ? 'default' : 'secondary'}
> >
@@ -131,7 +131,10 @@ export default async function PaymentsPage({ params }: PageProps) {
<CardContent> <CardContent>
<p className="text-muted-foreground mb-4 text-sm"> <p className="text-muted-foreground mb-4 text-sm">
{openInvoices.length > 0 {openInvoices.length > 0
? t('payments.invoicesOpenSummary', { count: openInvoices.length, total: formatCurrency(openTotal) }) ? t('payments.invoicesOpenSummary', {
count: openInvoices.length,
total: formatCurrency(openTotal),
})
: t('payments.noOpenInvoices')} : t('payments.noOpenInvoices')}
</p> </p>
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>
@@ -145,7 +148,9 @@ export default async function PaymentsPage({ params }: PageProps) {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">{t('payments.sepaBatches')}</CardTitle> <CardTitle className="text-base">
{t('payments.sepaBatches')}
</CardTitle>
<Badge variant={batches.length > 0 ? 'default' : 'secondary'}> <Badge variant={batches.length > 0 ? 'default' : 'secondary'}>
{batches.length} {batches.length}
</Badge> </Badge>
@@ -153,7 +158,10 @@ export default async function PaymentsPage({ params }: PageProps) {
<CardContent> <CardContent>
<p className="text-muted-foreground mb-4 text-sm"> <p className="text-muted-foreground mb-4 text-sm">
{batches.length > 0 {batches.length > 0
? t('payments.batchSummary', { count: batches.length, total: formatCurrency(sepaTotal) }) ? t('payments.batchSummary', {
count: batches.length,
total: formatCurrency(sepaTotal),
})
: t('payments.noBatchesFound')} : t('payments.noBatchesFound')}
</p> </p>
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { ArrowLeft, Download } from 'lucide-react'; import { ArrowLeft, Download } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/api'; import { createFinanceApi } from '@kit/finance/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';
@@ -8,7 +9,6 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
@@ -76,7 +76,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
{/* Summary Card */} {/* Summary Card */}
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle>{String(batch.description ?? t('sepa.batchFallbackName'))}</CardTitle> <CardTitle>
{String(batch.description ?? t('sepa.batchFallbackName'))}
</CardTitle>
<Badge variant={BATCH_STATUS_VARIANT[status] ?? 'secondary'}> <Badge variant={BATCH_STATUS_VARIANT[status] ?? 'secondary'}>
{t(BATCH_STATUS_LABEL_KEYS[status] ?? status)} {t(BATCH_STATUS_LABEL_KEYS[status] ?? status)}
</Badge> </Badge>
@@ -133,7 +135,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
{/* Items Table */} {/* Items Table */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{t('sepa.itemCount')} ({items.length})</CardTitle> <CardTitle>
{t('sepa.itemCount')} ({items.length})
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{items.length === 0 ? ( {items.length === 0 ? (
@@ -145,10 +149,18 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
<table className="w-full min-w-[640px] text-sm"> <table className="w-full min-w-[640px] text-sm">
<thead> <thead>
<tr className="bg-muted/50 border-b"> <tr className="bg-muted/50 border-b">
<th scope="col" className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th scope="col" className="p-3 text-left font-medium">IBAN</th> Name
<th scope="col" className="p-3 text-right font-medium">{t('common.amount')}</th> </th>
<th scope="col" className="p-3 text-left font-medium">{t('common.status')}</th> <th scope="col" className="p-3 text-left font-medium">
IBAN
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('common.amount')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.status')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -176,7 +188,11 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
ITEM_STATUS_VARIANT[itemStatus] ?? 'secondary' ITEM_STATUS_VARIANT[itemStatus] ?? 'secondary'
} }
> >
{t(`sepaItemStatus.${itemStatus}` as Parameters<typeof t>[0]) ?? itemStatus} {t(
`sepaItemStatus.${itemStatus}` as Parameters<
typeof t
>[0],
) ?? itemStatus}
</Badge> </Badge>
</td> </td>
</tr> </tr>

View File

@@ -13,7 +13,10 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state'; import { EmptyState } from '~/components/empty-state';
import { BATCH_STATUS_VARIANT, BATCH_STATUS_LABEL_KEYS } from '~/lib/status-badges'; import {
BATCH_STATUS_VARIANT,
BATCH_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps { interface PageProps {
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -114,7 +117,10 @@ export default async function SepaPage({ params }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ?? String(batch.status))} {t(
BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
String(batch.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3"> <td className="p-3">

View File

@@ -4,7 +4,24 @@ import { use } from 'react';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { Fish, FileSignature, Building2 } from 'lucide-react'; import {
Fish,
Waves,
Anchor,
BookOpen,
ShieldCheck,
Trophy,
FileSignature,
ScrollText,
ListChecks,
BookMarked,
Building2,
Network,
SearchCheck,
Share2,
PieChart,
LayoutTemplate,
} from 'lucide-react';
import * as z from 'zod'; import * as z from 'zod';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -61,65 +78,132 @@ const getAccountFeatures = cache(async (accountSlug: string) => {
return (settings?.features as Record<string, boolean>) ?? {}; return (settings?.features as Record<string, boolean>) ?? {};
}); });
const iconClasses = 'w-4';
/** /**
* Inject per-account feature routes (e.g. Fischerei) into the parsed * Inject per-account feature module route groups (Fischerei, Meetings, Verband)
* navigation config. The entry is inserted right after "Veranstaltungen". * into the navigation config. These are added as separate collapsible sections
* before the Administration section (last group).
*
* Only modules enabled in the account's settings are shown.
*/ */
function injectAccountFeatureRoutes( function injectAccountFeatureRoutes(
config: z.output<typeof NavigationConfigSchema>, config: z.output<typeof NavigationConfigSchema>,
account: string, account: string,
features: Record<string, boolean>, features: Record<string, boolean>,
): z.output<typeof NavigationConfigSchema> { ): z.output<typeof NavigationConfigSchema> {
if (!features.fischerei && !features.meetings && !features.verband) const featureGroups: z.output<typeof NavigationConfigSchema>['routes'] = [];
return config;
const featureEntries: Array<{
label: string;
path: string;
Icon: React.ReactNode;
}> = [];
if (features.fischerei) { if (features.fischerei) {
featureEntries.push({ featureGroups.push({
label: 'common.routes.fischerei', label: 'common:routes.fisheriesManagement',
path: `/home/${account}/fischerei`, collapsible: true,
Icon: <Fish className="w-4" />, children: [
{
label: 'common:routes.fisheriesOverview',
path: `/home/${account}/fischerei`,
Icon: <Fish className={iconClasses} />,
},
{
label: 'common:routes.fisheriesWaters',
path: `/home/${account}/fischerei/waters`,
Icon: <Waves className={iconClasses} />,
},
{
label: 'common:routes.fisheriesLeases',
path: `/home/${account}/fischerei/leases`,
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCatchBooks',
path: `/home/${account}/fischerei/catch-books`,
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common:routes.fisheriesPermits',
path: `/home/${account}/fischerei/permits`,
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCompetitions',
path: `/home/${account}/fischerei/competitions`,
Icon: <Trophy className={iconClasses} />,
},
],
}); });
} }
if (features.meetings) { if (features.meetings) {
featureEntries.push({ featureGroups.push({
label: 'common.routes.meetings', label: 'common:routes.meetingProtocols',
path: `/home/${account}/meetings`, collapsible: true,
Icon: <FileSignature className="w-4" />, children: [
{
label: 'common:routes.meetingsOverview',
path: `/home/${account}/meetings`,
Icon: <BookMarked className={iconClasses} />,
},
{
label: 'common:routes.meetingsProtocols',
path: `/home/${account}/meetings/protocols`,
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common:routes.meetingsTasks',
path: `/home/${account}/meetings/tasks`,
Icon: <ListChecks className={iconClasses} />,
},
],
}); });
} }
if (features.verband) { if (features.verband) {
featureEntries.push({ featureGroups.push({
label: 'common.routes.verband', label: 'common:routes.associationManagement',
path: `/home/${account}/verband`, collapsible: true,
Icon: <Building2 className="w-4" />, children: [
{
label: 'common:routes.associationOverview',
path: `/home/${account}/verband`,
Icon: <Building2 className={iconClasses} />,
},
{
label: 'common:routes.associationHierarchy',
path: `/home/${account}/verband/hierarchy`,
Icon: <Network className={iconClasses} />,
},
{
label: 'common:routes.associationMemberSearch',
path: `/home/${account}/verband/members`,
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common:routes.associationEvents',
path: `/home/${account}/verband/events`,
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common:routes.associationReporting',
path: `/home/${account}/verband/reporting`,
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common:routes.associationTemplates',
path: `/home/${account}/verband/templates`,
Icon: <LayoutTemplate className={iconClasses} />,
},
],
}); });
} }
return { if (featureGroups.length === 0) return config;
...config,
routes: config.routes.map((group) => {
if (!('children' in group)) return group;
const eventsIndex = group.children.findIndex( // Insert before the last group (Administration)
(child) => child.label === 'common.routes.events', const routes = [...config.routes];
); const adminIndex = routes.length - 1;
routes.splice(adminIndex, 0, ...featureGroups);
if (eventsIndex === -1) return group; return { ...config, routes };
const newChildren = [...group.children];
newChildren.splice(eventsIndex + 1, 0, ...featureEntries);
return { ...group, children: newChildren };
}),
};
} }
async function SidebarLayout({ async function SidebarLayout({

View File

@@ -3,16 +3,16 @@ import { PageBody } from '@kit/ui/page';
export default function AccountLoading() { export default function AccountLoading() {
return ( return (
<PageBody> <PageBody>
<div className="flex flex-col gap-6 animate-pulse"> <div className="flex animate-pulse flex-col gap-6">
<div className="h-8 w-48 rounded-md bg-muted" /> <div className="bg-muted h-8 w-48 rounded-md" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => ( {[1, 2, 3, 4].map((i) => (
<div key={i} className="h-24 rounded-lg bg-muted" /> <div key={i} className="bg-muted h-24 rounded-lg" />
))} ))}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => ( {[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-12 w-full rounded bg-muted" /> <div key={i} className="bg-muted h-12 w-full rounded" />
))} ))}
</div> </div>
</div> </div>

View File

@@ -86,7 +86,9 @@ export default async function ProtocolDetailPage({ params }: PageProps) {
{MEETING_TYPE_LABELS[protocol.status] ?? protocol.status} {MEETING_TYPE_LABELS[protocol.status] ?? protocol.status}
</Badge> </Badge>
{protocol.status === 'final' ? ( {protocol.status === 'final' ? (
<Badge variant="default">{t('pages.statusPublished')}</Badge> <Badge variant="default">
{t('pages.statusPublished')}
</Badge>
) : ( ) : (
<Badge variant="outline">{t('pages.statusDraft')}</Badge> <Badge variant="outline">{t('pages.statusDraft')}</Badge>
)} )}

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { EditMemberForm } from '@kit/member-management/components'; import { EditMemberForm } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api'; import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components'; import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api'; import { createModuleBuilderApi } from '@kit/module-builder/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api'; import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components'; import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -171,7 +171,11 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
'secondary' 'secondary'
} }
> >
{t(NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[rStatus] ?? rStatus)} {t(
NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[
rStatus
] ?? rStatus,
)}
</Badge> </Badge>
</td> </td>
</tr> </tr>

View File

@@ -204,7 +204,10 @@ export default async function NewsletterPage({
'secondary' 'secondary'
} }
> >
{t(NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ?? String(nl.status))} {t(
NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ??
String(nl.status),
)}
</Badge> </Badge>
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">
@@ -241,7 +244,12 @@ export default async function NewsletterPage({
</Link> </Link>
</Button> </Button>
) : ( ) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.previous')}> <Button
variant="outline"
size="sm"
disabled
aria-label={t('common.previous')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" /> <ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
)} )}
@@ -256,11 +264,19 @@ export default async function NewsletterPage({
href={`/home/${account}/newsletter${buildQuery(queryBase, { page: safePage + 1 })}`} href={`/home/${account}/newsletter${buildQuery(queryBase, { page: safePage + 1 })}`}
aria-label={t('common.next')} aria-label={t('common.next')}
> >
<ChevronRight className="h-4 w-4" aria-hidden="true" /> <ChevronRight
className="h-4 w-4"
aria-hidden="true"
/>
</Link> </Link>
</Button> </Button>
) : ( ) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.next')}> <Button
variant="outline"
size="sm"
disabled
aria-label={t('common.next')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" /> <ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button> </Button>
)} )}

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { CreatePageForm } from '@kit/site-builder/components'; import { CreatePageForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -166,10 +166,7 @@ export default async function SiteBuilderDashboard({ params }: Props) {
</thead> </thead>
<tbody> <tbody>
{(pages as SitePage[]).map((page) => ( {(pages as SitePage[]).map((page) => (
<tr <tr key={page.id} className="hover:bg-muted/30 border-b">
key={page.id}
className="hover:bg-muted/30 border-b"
>
<td className="p-3 font-medium">{page.title}</td> <td className="p-3 font-medium">{page.title}</td>
<td className="text-muted-foreground p-3 font-mono text-xs"> <td className="text-muted-foreground p-3 font-mono text-xs">
/{page.slug} /{page.slug}

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { CreatePostForm } from '@kit/site-builder/components'; import { CreatePostForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found'; import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell'; import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -77,10 +77,7 @@ export default async function PostsManagerPage({ params }: Props) {
</thead> </thead>
<tbody> <tbody>
{(posts as SitePost[]).map((post) => ( {(posts as SitePost[]).map((post) => (
<tr <tr key={post.id} className="hover:bg-muted/30 border-b">
key={post.id}
className="hover:bg-muted/30 border-b"
>
<td className="p-3 font-medium">{post.title}</td> <td className="p-3 font-medium">{post.title}</td>
<td className="p-3"> <td className="p-3">
<Badge <Badge

View File

@@ -5,7 +5,6 @@ import { useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { toast } from '@kit/ui/sonner';
import { import {
AlertDialog, AlertDialog,
@@ -19,6 +18,7 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from '@kit/ui/alert-dialog'; } from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { toast } from '@kit/ui/sonner';
interface PublishToggleButtonProps { interface PublishToggleButtonProps {
pageId: string; pageId: string;
@@ -38,11 +38,14 @@ export function PublishToggleButton({
const handleToggle = () => { const handleToggle = () => {
startTransition(async () => { startTransition(async () => {
try { try {
const response = await fetch(`/api/site-builder/pages/${pageId}/publish`, { const response = await fetch(
method: 'PATCH', `/api/site-builder/pages/${pageId}/publish`,
headers: { 'Content-Type': 'application/json' }, {
body: JSON.stringify({ accountId, isPublished: !isPublished }), method: 'PATCH',
}); headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId, isPublished: !isPublished }),
},
);
if (!response.ok) { if (!response.ok) {
toast.error(t('pages.toggleError')); toast.error(t('pages.toggleError'));

View File

@@ -230,9 +230,7 @@ export default function SettingsContent({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">{t('settings.subtitle')}</p>
{t('settings.subtitle')}
</p>
</div> </div>
<SettingsSection <SettingsSection

View File

@@ -19,7 +19,8 @@ export async function AccountNotFound({
const t = await getTranslations('common'); const t = await getTranslations('common');
const resolvedTitle = title ?? t('accountNotFoundCard.title'); const resolvedTitle = title ?? t('accountNotFoundCard.title');
const resolvedDescription = description ?? t('accountNotFoundCard.description'); const resolvedDescription =
description ?? t('accountNotFoundCard.description');
const resolvedButtonLabel = buttonLabel ?? t('accountNotFoundCard.action'); const resolvedButtonLabel = buttonLabel ?? t('accountNotFoundCard.action');
return ( return (

View File

@@ -27,7 +27,9 @@ export function CmsPageShell({
<TeamAccountLayoutPageHeader <TeamAccountLayoutPageHeader
account={account} account={account}
title={title} title={title}
description={description !== undefined ? description : <AppBreadcrumbs />} description={
description !== undefined ? description : <AppBreadcrumbs />
}
/> />
<PageBody>{children}</PageBody> <PageBody>{children}</PageBody>

View File

@@ -8,7 +8,6 @@ import {
UserPlus, UserPlus,
IdCard, IdCard,
ClipboardList, ClipboardList,
KeyRound,
// Courses // Courses
GraduationCap, GraduationCap,
CalendarDays, CalendarDays,
@@ -41,24 +40,6 @@ import {
PanelTop, PanelTop,
Newspaper, Newspaper,
Palette, Palette,
// Fisheries
Fish,
Waves,
Anchor,
BookOpen,
ShieldCheck,
Trophy,
// Meetings
BookMarked,
ListChecks,
ScrollText,
// Association (Verband)
Building2,
Network,
SearchCheck,
Share2,
PieChart,
LayoutTemplate,
// Modules // Modules
Database, Database,
} from 'lucide-react'; } from 'lucide-react';
@@ -396,176 +377,51 @@ const getRoutes = (account: string) => {
}); });
} }
// ── Custom Modules ── // Note: Fischerei, Meetings, and Verband sections are injected at runtime
if (featureFlagsConfig.enableModuleBuilder) { // via injectAccountFeatureRoutes() in the layout, based on per-account
routes.push({ // settings (account_settings.features). They are NOT added here to avoid
label: 'common:routes.customModules', // duplicate entries when both the global feature flag and per-account
collapsible: true, // setting are enabled.
collapsed: true,
children: [
{
label: 'common:routes.moduleList',
path: createPath(pathsConfig.app.accountModules, account),
Icon: <Database className={iconClasses} />,
},
],
});
}
// ── Fisheries ──
if (featureFlagsConfig.enableFischerei) {
routes.push({
label: 'common:routes.fisheriesManagement',
collapsible: true,
children: [
{
label: 'common:routes.fisheriesOverview',
path: createPath(pathsConfig.app.accountFischerei, account),
Icon: <Fish className={iconClasses} />,
},
{
label: 'common:routes.fisheriesWaters',
path: createPath(
pathsConfig.app.accountFischerei + '/waters',
account,
),
Icon: <Waves className={iconClasses} />,
},
{
label: 'common:routes.fisheriesLeases',
path: createPath(
pathsConfig.app.accountFischerei + '/leases',
account,
),
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCatchBooks',
path: createPath(
pathsConfig.app.accountFischerei + '/catch-books',
account,
),
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common:routes.fisheriesPermits',
path: createPath(
pathsConfig.app.accountFischerei + '/permits',
account,
),
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCompetitions',
path: createPath(
pathsConfig.app.accountFischerei + '/competitions',
account,
),
Icon: <Trophy className={iconClasses} />,
},
],
});
}
// ── Meeting Protocols ──
if (featureFlagsConfig.enableMeetingProtocols) {
routes.push({
label: 'common:routes.meetingProtocols',
collapsible: true,
children: [
{
label: 'common:routes.meetingsOverview',
path: createPath(pathsConfig.app.accountMeetings, account),
Icon: <BookMarked className={iconClasses} />,
},
{
label: 'common:routes.meetingsProtocols',
path: createPath(
pathsConfig.app.accountMeetings + '/protocols',
account,
),
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common:routes.meetingsTasks',
path: createPath(pathsConfig.app.accountMeetings + '/tasks', account),
Icon: <ListChecks className={iconClasses} />,
},
],
});
}
// ── Association Management (Verband) ──
if (featureFlagsConfig.enableVerbandsverwaltung) {
routes.push({
label: 'common:routes.associationManagement',
collapsible: true,
children: [
{
label: 'common:routes.associationOverview',
path: createPath(pathsConfig.app.accountVerband, account),
Icon: <Building2 className={iconClasses} />,
},
{
label: 'common:routes.associationHierarchy',
path: createPath(
pathsConfig.app.accountVerband + '/hierarchy',
account,
),
Icon: <Network className={iconClasses} />,
},
{
label: 'common:routes.associationMemberSearch',
path: createPath(
pathsConfig.app.accountVerband + '/members',
account,
),
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common:routes.associationEvents',
path: createPath(pathsConfig.app.accountVerband + '/events', account),
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common:routes.associationReporting',
path: createPath(
pathsConfig.app.accountVerband + '/reporting',
account,
),
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common:routes.associationTemplates',
path: createPath(
pathsConfig.app.accountVerband + '/templates',
account,
),
Icon: <LayoutTemplate className={iconClasses} />,
},
],
});
}
// ── Administration ── // ── Administration ──
routes.push({ {
label: 'common:routes.administration', const adminChildren: Array<
collapsible: false, | {
children: [ label: string;
path: string;
Icon: React.ReactNode;
}
| undefined
> = [
{ {
label: 'common:routes.accountSettings', label: 'common:routes.accountSettings',
path: createPath(pathsConfig.app.accountSettings, account), path: createPath(pathsConfig.app.accountSettings, account),
Icon: <Settings className={iconClasses} />, Icon: <Settings className={iconClasses} />,
}, },
featureFlagsConfig.enableTeamAccountBilling ];
? {
label: 'common:routes.billing', if (featureFlagsConfig.enableModuleBuilder) {
path: createPath(pathsConfig.app.accountBilling, account), adminChildren.push({
Icon: <CreditCard className={iconClasses} />, label: 'common:routes.moduleList',
} path: createPath(pathsConfig.app.accountModules, account),
: undefined, Icon: <Database className={iconClasses} />,
], });
}); }
if (featureFlagsConfig.enableTeamAccountBilling) {
adminChildren.push({
label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />,
});
}
routes.push({
label: 'common:routes.administration',
collapsible: false,
children: adminChildren,
});
}
return routes; return routes;
}; };

View File

@@ -150,15 +150,7 @@
"overview": "Kurstermine im Überblick", "overview": "Kurstermine im Überblick",
"activeCourses": "Aktive Kurse ({count})", "activeCourses": "Aktive Kurse ({count})",
"noActiveCourses": "Keine aktiven Kurse in diesem Monat.", "noActiveCourses": "Keine aktiven Kurse in diesem Monat.",
"weekdays": [ "weekdays": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa",
"So"
],
"months": [ "months": [
"Januar", "Januar",
"Februar", "Februar",

View File

@@ -150,15 +150,7 @@
"overview": "Overview of course dates", "overview": "Overview of course dates",
"activeCourses": "Active Courses ({count})", "activeCourses": "Active Courses ({count})",
"noActiveCourses": "No active courses this month.", "noActiveCourses": "No active courses this month.",
"weekdays": [ "weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
],
"months": [ "months": [
"January", "January",
"February", "February",

View File

@@ -223,7 +223,8 @@ export const COURSE_STATUS_LABEL = COURSE_STATUS_LABEL_KEYS;
/** @deprecated Use APPLICATION_STATUS_LABEL_KEYS + t() */ /** @deprecated Use APPLICATION_STATUS_LABEL_KEYS + t() */
export const APPLICATION_STATUS_LABEL = APPLICATION_STATUS_LABEL_KEYS; export const APPLICATION_STATUS_LABEL = APPLICATION_STATUS_LABEL_KEYS;
/** @deprecated Use NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS + t() */ /** @deprecated Use NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS + t() */
export const NEWSLETTER_RECIPIENT_STATUS_LABEL = NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS; export const NEWSLETTER_RECIPIENT_STATUS_LABEL =
NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS;
/** @deprecated Use BOOKING_STATUS_LABEL_KEYS + t() */ /** @deprecated Use BOOKING_STATUS_LABEL_KEYS + t() */
export const BOOKING_STATUS_LABEL = BOOKING_STATUS_LABEL_KEYS; export const BOOKING_STATUS_LABEL = BOOKING_STATUS_LABEL_KEYS;
/** @deprecated Use MODULE_STATUS_LABEL_KEYS + t() */ /** @deprecated Use MODULE_STATUS_LABEL_KEYS + t() */

View File

@@ -40,7 +40,7 @@ const RouteGroup = z.object({
}); });
export const NavigationConfigSchema = z.object({ export const NavigationConfigSchema = z.object({
sidebarCollapsed: z.stringbool().optional().default(false), sidebarCollapsed: z.coerce.boolean().optional().default(false),
sidebarCollapsedStyle: z.enum(['icon', 'offcanvas', 'none']).default('icon'), sidebarCollapsedStyle: z.enum(['icon', 'offcanvas', 'none']).default('icon'),
routes: z.array(z.union([RouteGroup, Divider])), routes: z.array(z.union([RouteGroup, Divider])),
style: z.enum(['sidebar', 'header', 'custom']).default('sidebar'), style: z.enum(['sidebar', 'header', 'custom']).default('sidebar'),

View File

@@ -3,7 +3,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { ChevronDown } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl'; import { useLocale, useTranslations } from 'next-intl';
import * as z from 'zod'; import * as z from 'zod';
@@ -68,7 +68,7 @@ function MaybeCollapsible({
} }
return ( return (
<Collapsible defaultOpen={defaultOpen} className={'group/collapsible'}> <Collapsible defaultOpen={defaultOpen} className="group/collapsible">
{children} {children}
</Collapsible> </Collapsible>
); );
@@ -129,23 +129,23 @@ function SidebarNavigationRouteGroupLabel({
}) { }) {
const className = cn({ hidden: !open }); const className = cn({ hidden: !open });
return ( if (!collapsible) {
<If return (
condition={collapsible}
fallback={
<SidebarGroupLabel className={className}>
<Trans i18nKey={label} defaults={label} />
</SidebarGroupLabel>
}
>
<SidebarGroupLabel className={className}> <SidebarGroupLabel className={className}>
<CollapsibleTrigger> <Trans i18nKey={label} defaults={label} />
<Trans i18nKey={label} defaults={label} />
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</CollapsibleTrigger>
</SidebarGroupLabel> </SidebarGroupLabel>
</If> );
}
return (
<SidebarGroupLabel className={cn(className, 'cursor-pointer')}>
<CollapsibleTrigger className="flex w-full items-center gap-1">
<ChevronRight className="size-3.5 shrink-0 transition-transform duration-200 data-[panel-open]:rotate-90" />
<span className="truncate">
<Trans i18nKey={label} defaults={label} />
</span>
</CollapsibleTrigger>
</SidebarGroupLabel>
); );
} }
@@ -165,8 +165,8 @@ function SidebarNavigationSubItems({
{(items) => {(items) =>
items.length > 0 && ( items.length > 0 && (
<SidebarMenuSub <SidebarMenuSub
className={cn({ className={cn('border-sidebar-border ml-3.5 border-l pl-2', {
'mx-0 px-1.5': !open, 'mx-0 border-l-0 px-1.5': !open,
})} })}
> >
{items.map((child) => { {items.map((child) => {
@@ -241,7 +241,7 @@ function SidebarNavigationRouteChildItem({
<span <span
className={cn( className={cn(
'transition-width w-auto transition-opacity duration-500', 'transition-width w-auto transition-opacity duration-300',
{ {
'w-0 opacity-0': !open, 'w-0 opacity-0': !open,
}, },
@@ -250,9 +250,9 @@ function SidebarNavigationRouteChildItem({
<Trans i18nKey={child.label} defaults={child.label} /> <Trans i18nKey={child.label} defaults={child.label} />
</span> </span>
<ChevronDown <ChevronRight
className={cn( className={cn(
'ml-auto size-4 transition-transform group-data-[state=open]/collapsible:rotate-180', 'ml-auto size-4 shrink-0 transition-transform duration-200 data-[panel-open]:rotate-90',
{ {
'hidden size-0': !open, 'hidden size-0': !open,
}, },

View File

@@ -2,6 +2,8 @@
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible'; import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
import { cn } from '../lib/utils';
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
} }
@@ -12,9 +14,31 @@ function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
); );
} }
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { /**
* CollapsibleContent (Panel) with smooth height animation.
*
* Base UI sets `--collapsible-panel-height` automatically and provides
* `data-open`, `data-closed`, `data-starting-style`, `data-ending-style`
* for CSS-driven animations.
*/
function CollapsibleContent({
className,
...props
}: CollapsiblePrimitive.Panel.Props) {
return ( return (
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} /> <CollapsiblePrimitive.Panel
data-slot="collapsible-content"
className={cn(
[
'h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out',
'data-[closed]:h-0',
'data-[starting-style]:h-0',
'data-[ending-style]:h-0',
],
className,
)}
{...props}
/>
); );
} }