events werden nun erfolgreich gespeichert, branding wird nun erfolgreich gespeichert, emotionen können nun angelegt werden. Task Ansicht im Event admin verbessert, Buttons in FAB umgewandelt und vereinheitlicht. Teilen-Link Guest PWA schicker gemacht, SynGoogleFonts ausgebaut (mit Einzel-Family-Download).
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTriangle, ArrowLeft, CheckCircle2, Circle, Copy, Download, ExternalLink, Link2, Loader2, Mail, Printer, QrCode, RefreshCw, Share2, X, ShoppingCart } from 'lucide-react';
|
||||
import { AlertTriangle, ArrowLeft, CheckCircle2, Circle, Copy, Download, ExternalLink, Link2, Loader2, Mail, Printer, QrCode, RefreshCw, Share2, X, ShoppingCart, Save, Plus } from 'lucide-react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -29,8 +29,6 @@ import {
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import {
|
||||
ADMIN_EVENTS_PATH,
|
||||
ADMIN_EVENT_VIEW_PATH,
|
||||
ADMIN_EVENT_PHOTOS_PATH,
|
||||
} from '../constants';
|
||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||
import { buildEventTabs } from '../lib/eventTabs';
|
||||
@@ -57,6 +55,7 @@ import {
|
||||
triggerDownloadFromDataUrl,
|
||||
} from './components/invite-layout/export-utils';
|
||||
import { useOnboardingProgress } from '../onboarding';
|
||||
import { FloatingActionBar, type FloatingAction } from '../components/FloatingActionBar';
|
||||
|
||||
interface PageState {
|
||||
event: TenantEvent | null;
|
||||
@@ -219,7 +218,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
setAddonsCatalog(catalog);
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState({ event: null, invites: [], loading: false, error: 'QR-Einladungen konnten nicht geladen werden.' });
|
||||
setState({ event: null, invites: [], loading: false, error: 'QR-QR-Code konnten nicht geladen werden.' });
|
||||
}
|
||||
}
|
||||
}, [slug]);
|
||||
@@ -543,9 +542,11 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
try {
|
||||
await navigator.clipboard.writeText(invite.url);
|
||||
setCopiedInviteId(invite.id);
|
||||
toast.success(t('invites.actions.copied', 'Link kopiert'));
|
||||
} catch {
|
||||
// ignore clipboard failures
|
||||
}
|
||||
toast.success(t('invites.actions.created', 'QR-Code erstellt'));
|
||||
markStep({
|
||||
lastStep: 'invite',
|
||||
serverStep: 'invite_created',
|
||||
@@ -553,7 +554,8 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({ ...prev, error: 'QR-Einladung konnte nicht erstellt werden.' }));
|
||||
setState((prev) => ({ ...prev, error: 'QR-QR-Code konnte nicht erstellt werden.' }));
|
||||
toast.error(t('invites.actions.createFailed', 'QR-Code konnte nicht erstellt werden.'));
|
||||
}
|
||||
} finally {
|
||||
setCreatingInvite(false);
|
||||
@@ -564,8 +566,10 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
try {
|
||||
await navigator.clipboard.writeText(invite.url);
|
||||
setCopiedInviteId(invite.id);
|
||||
toast.success(t('invites.actions.copied', 'Link kopiert'));
|
||||
} catch (error) {
|
||||
console.warn('[Invites] Clipboard copy failed', error);
|
||||
toast.error(t('invites.actions.copyFailed', 'Link konnte nicht kopiert werden.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,9 +595,11 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
if (selectedInviteId === invite.id && !updated.is_active) {
|
||||
setSelectedInviteId((prevId) => (prevId === updated.id ? null : prevId));
|
||||
}
|
||||
toast.success(t('invites.actions.revoked', 'QR-Code deaktiviert'));
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({ ...prev, error: 'QR-Einladung konnte nicht deaktiviert werden.' }));
|
||||
setState((prev) => ({ ...prev, error: 'QR-QR-Code konnte nicht deaktiviert werden.' }));
|
||||
toast.error(t('invites.actions.revokeFailed', 'QR-Code konnte nicht deaktiviert werden.'));
|
||||
}
|
||||
} finally {
|
||||
setRevokingId(null);
|
||||
@@ -616,6 +622,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
invites: prev.invites.map((invite) => (invite.id === updated.id ? updated : invite)),
|
||||
}));
|
||||
setCustomizerDraft(null);
|
||||
toast.success(t('invites.customizer.toastSaved', 'Layout gespeichert'));
|
||||
markStep({
|
||||
lastStep: 'branding',
|
||||
serverStep: 'branding_configured',
|
||||
@@ -627,6 +634,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({ ...prev, error: 'Anpassung konnte nicht gespeichert werden.' }));
|
||||
toast.error(t('invites.customizer.toastSaveFailed', 'Layout konnte nicht gespeichert werden.'));
|
||||
}
|
||||
} finally {
|
||||
setCustomizerSaving(false);
|
||||
@@ -699,9 +707,9 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
|
||||
const eventDateSegment = normalizeEventDateSegment(eventDate);
|
||||
const filename = buildDownloadFilename(
|
||||
['Einladungslayout', eventName, exportLayout.name ?? null, eventDateSegment],
|
||||
['QR-Codeslayout', eventName, exportLayout.name ?? null, eventDateSegment],
|
||||
normalizedFormat,
|
||||
'einladungslayout',
|
||||
'QR-Codeslayout',
|
||||
);
|
||||
|
||||
const exportOptions = {
|
||||
@@ -792,36 +800,8 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="mr-1 h-3.5 w-3.5" />
|
||||
{t('invites.actions.backToList', 'Zurück')}
|
||||
{t('invites.actions.backToList', 'Zurück zur Übersicht')}
|
||||
</Button>
|
||||
{slug ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => navigate(ADMIN_EVENT_VIEW_PATH(slug))}
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
{t('invites.actions.backToEvent', 'Event öffnen')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => navigate(ADMIN_EVENT_PHOTOS_PATH(slug))}
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
{t('toolkit.actions.moderate', 'Fotos moderieren')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => navigate(ADMIN_EVENT_VIEW_PATH(slug))}
|
||||
className="hover:text-foreground"
|
||||
>
|
||||
{t('toolkit.actions.backToEvent', 'Event-Day Toolkit')}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -860,6 +840,43 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
[slug],
|
||||
);
|
||||
|
||||
const fabActions = React.useMemo<FloatingAction[]>(() => {
|
||||
const items: FloatingAction[] = [
|
||||
{
|
||||
key: 'create-invite',
|
||||
label: creatingInvite ? t('invites.actions.creating', 'Erstellen...') : t('invites.actions.create', 'Neue QR-Code erstellen'),
|
||||
icon: Plus,
|
||||
onClick: () => { void handleCreateInvite(); },
|
||||
loading: creatingInvite,
|
||||
disabled: creatingInvite || state.event?.limits?.can_add_guests === false,
|
||||
tone: 'primary',
|
||||
},
|
||||
{
|
||||
key: 'refresh',
|
||||
label: state.loading ? t('invites.actions.refreshing', 'Aktualisieren...') : t('invites.actions.refresh', 'Aktualisieren'),
|
||||
icon: RefreshCw,
|
||||
onClick: () => { void load(); },
|
||||
loading: state.loading,
|
||||
disabled: state.loading,
|
||||
tone: 'secondary',
|
||||
},
|
||||
];
|
||||
|
||||
if (activeTab === 'layout' && selectedInvite && effectiveCustomization) {
|
||||
items.unshift({
|
||||
key: 'save-layout',
|
||||
label: customizerSaving ? t('invites.customizer.actions.saving', 'Speichert...') : t('invites.customizer.actions.save', 'Layout speichern'),
|
||||
icon: Save,
|
||||
onClick: () => { void handleSaveCustomization(effectiveCustomization); },
|
||||
loading: customizerSaving,
|
||||
disabled: customizerSaving || customizerResetting,
|
||||
tone: 'primary',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [activeTab, selectedInvite, effectiveCustomization, customizerSaving, customizerResetting, creatingInvite, state.event?.limits?.can_add_guests, state.loading, t, handleSaveCustomization, load]);
|
||||
|
||||
const limitScopeLabels = React.useMemo(
|
||||
() => ({
|
||||
photos: tLimits('photosTitle'),
|
||||
@@ -882,70 +899,71 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
return (
|
||||
<AdminLayout
|
||||
title={eventName}
|
||||
subtitle={t('invites.subtitle', 'Manage QR-Einladungen, Drucklayouts und Branding für deine Gäste.')}
|
||||
subtitle={t('invites.subtitle', 'Manage QR-Codes, Drucklayouts und Branding für deine Gäste.')}
|
||||
actions={actions}
|
||||
tabs={eventTabs}
|
||||
currentTabKey="invites"
|
||||
>
|
||||
{limitWarnings.length > 0 && (
|
||||
<div className="mb-6 space-y-2">
|
||||
{limitWarnings.map((warning) => (
|
||||
<Alert
|
||||
key={warning.id}
|
||||
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
|
||||
className={warning.tone === 'warning' ? 'border-amber-400/50 bg-amber-50 text-amber-900' : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<AlertTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{limitScopeLabels[warning.scope]}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-sm">
|
||||
{warning.message}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
{warning.scope === 'guests' ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { void handleAddonPurchase(); }}
|
||||
disabled={addonBusy === 'guests'}
|
||||
>
|
||||
<ShoppingCart className="mr-2 h-4 w-4" />
|
||||
{t('invites.actions.buyMoreGuests', 'Mehr Gäste freischalten')}
|
||||
</Button>
|
||||
<AddonsPicker
|
||||
addons={addonsCatalog}
|
||||
scope="guests"
|
||||
onCheckout={(key) => { void handleAddonPurchase(key); }}
|
||||
busy={addonBusy === 'guests'}
|
||||
t={(key, fallback) => t(key, fallback)}
|
||||
/>
|
||||
<div className="pb-28">
|
||||
{limitWarnings.length > 0 && (
|
||||
<div className="mb-6 space-y-2">
|
||||
{limitWarnings.map((warning) => (
|
||||
<Alert
|
||||
key={warning.id}
|
||||
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
|
||||
className={warning.tone === 'warning' ? 'border-amber-400/50 bg-amber-50 text-amber-900' : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<AlertTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{limitScopeLabels[warning.scope]}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-sm">
|
||||
{warning.message}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{warning.scope === 'guests' ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { void handleAddonPurchase(); }}
|
||||
disabled={addonBusy === 'guests'}
|
||||
>
|
||||
<ShoppingCart className="mr-2 h-4 w-4" />
|
||||
{t('invites.actions.buyMoreGuests', 'Mehr Gäste freischalten')}
|
||||
</Button>
|
||||
<AddonsPicker
|
||||
addons={addonsCatalog}
|
||||
scope="guests"
|
||||
onCheckout={(key) => { void handleAddonPurchase(key); }}
|
||||
busy={addonBusy === 'guests'}
|
||||
t={(key, fallback) => t(key, fallback)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.event?.addons?.length ? (
|
||||
<Card className="mb-6 border-0 bg-white/85 shadow-lg shadow-slate-100/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-semibold text-slate-900">{t('events.sections.addons.title', 'Add-ons & Upgrades')}</CardTitle>
|
||||
<CardDescription>{t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AddonSummaryList addons={state.event.addons} t={(key, fallback) => t(key, fallback)} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{state.event?.addons?.length ? (
|
||||
<Card className="mb-6 border-0 bg-white/85 shadow-lg shadow-slate-100/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-semibold text-slate-900">{t('events.sections.addons.title', 'Add-ons & Upgrades')}</CardTitle>
|
||||
<CardDescription>{t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AddonSummaryList addons={state.event.addons} t={(key, fallback) => t(key, fallback)} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<InviteWorkflowSteps steps={workflowSteps} onSelectStep={(tab) => handleTabChange(tab)} />
|
||||
<InviteWorkflowSteps steps={workflowSteps} onSelectStep={(tab) => handleTabChange(tab)} />
|
||||
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-6">
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-6">
|
||||
<TabsList className="grid w-full max-w-2xl grid-cols-3 gap-1 rounded-full border border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-strong)] p-1 text-sm">
|
||||
<TabsTrigger value="share" className="rounded-full px-4 py-1.5 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
|
||||
{t('invites.tabs.share', 'Links & QR teilen')}
|
||||
@@ -969,7 +987,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
<section className="rounded-3xl border border-[var(--tenant-border-strong)] bg-gradient-to-br from-[var(--tenant-surface-muted)] via-[var(--tenant-surface)] to-[var(--tenant-surface-strong)] p-6 shadow-xl shadow-primary/10 backdrop-blur-sm transition-colors">
|
||||
<div className="mb-6 flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold text-foreground">{t('invites.customizer.heading', 'Einladungslayout anpassen')}</h2>
|
||||
<h2 className="text-lg font-semibold text-foreground">{t('invites.customizer.heading', 'QR-Codeslayout anpassen')}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('invites.customizer.copy', 'Bearbeite Texte, Farben und Positionen direkt neben der Live-Vorschau. Änderungen werden sofort sichtbar.')}
|
||||
</p>
|
||||
@@ -1014,7 +1032,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
disabled={state.invites.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full min-w-[200px] sm:w-60">
|
||||
<SelectValue placeholder={t('invites.export.selectPlaceholder', 'Einladung auswählen')} />
|
||||
<SelectValue placeholder={t('invites.export.selectPlaceholder', 'QR-Code auswählen')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{state.invites.map((invite) => (
|
||||
@@ -1216,7 +1234,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
{selectedInvite.qr_code_data_url ? (
|
||||
<img
|
||||
src={selectedInvite.qr_code_data_url}
|
||||
alt={t('invites.export.qr.alt', 'QR-Code der Einladung')}
|
||||
alt={t('invites.export.qr.alt', 'QR-Code der QR-Code')}
|
||||
className="h-40 w-40 rounded-2xl border border-[var(--tenant-border-strong)] bg-white p-3 shadow-md"
|
||||
/>
|
||||
) : (
|
||||
@@ -1263,12 +1281,12 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] p-8 text-sm text-[var(--tenant-foreground-soft)]">
|
||||
{t('invites.export.noLayouts', 'Für diese Einladung sind aktuell keine Layouts verfügbar.')}
|
||||
{t('invites.export.noLayouts', 'Für diese QR-Code sind aktuell keine Layouts verfügbar.')}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] p-8 text-sm text-[var(--tenant-foreground-soft)]">
|
||||
{t('invites.export.noInviteSelected', 'Wähle zunächst eine Einladung aus, um Downloads zu starten.')}
|
||||
{t('invites.export.noInviteSelected', 'Wähle zunächst eine QR-Code aus, um Downloads zu starten.')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -1291,13 +1309,13 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="flex items-center gap-2 text-lg text-foreground">
|
||||
<QrCode className="h-5 w-5 text-primary" />
|
||||
{t('invites.cardTitle', 'QR-Einladungen & Layouts')}
|
||||
{t('invites.cardTitle', 'QR-QR-Code & Layouts')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
{t('invites.cardDescription', 'Erzeuge Einladungen, passe Layouts an und stelle druckfertige Vorlagen bereit.')}
|
||||
{t('invites.cardDescription', 'Erzeuge QR-Code, passe Layouts an und stelle druckfertige Vorlagen bereit.')}
|
||||
</CardDescription>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] px-3 py-1 text-xs text-[var(--tenant-foreground-soft)]">
|
||||
<span>{t('invites.summary.active', 'Aktive Einladungen')}: {inviteCountSummary.active}</span>
|
||||
<span>{t('invites.summary.active', 'Aktive QR-Code')}: {inviteCountSummary.active}</span>
|
||||
<span className="text-primary">•</span>
|
||||
<span>{t('invites.summary.total', 'Gesamt')}: {inviteCountSummary.total}</span>
|
||||
</div>
|
||||
@@ -1319,7 +1337,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
className="bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90"
|
||||
>
|
||||
{creatingInvite ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Share2 className="mr-1 h-4 w-4" />}
|
||||
{t('invites.actions.create', 'Neue Einladung erstellen')}
|
||||
{t('invites.actions.create', 'Neue QR-Code erstellen')}
|
||||
</Button>
|
||||
{!state.loading && state.event?.limits?.can_add_guests === false && (
|
||||
<p className="w-full text-xs text-amber-600">
|
||||
@@ -1353,6 +1371,8 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<FloatingActionBar actions={fabActions} />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@@ -1372,7 +1392,7 @@ function InviteWorkflowSteps({ steps, onSelectStep }: { steps: InviteWorkflowSte
|
||||
<CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold text-foreground">
|
||||
{t('invites.workflow.title', 'Einladungs-Workflow')}
|
||||
{t('invites.workflow.title', 'QR-Codes-Workflow')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
{t('invites.workflow.description', 'Durchlaufe die Schritte in Reihenfolge – Layout gestalten, Links teilen, Export starten.')}
|
||||
@@ -1490,7 +1510,7 @@ function InviteShareSummaryCard({ invite, onCopy, onCreate, onOpenLayout, onOpen
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onCreate} className="flex-1">
|
||||
<Share2 className="mr-2 h-4 w-4" />
|
||||
{t('invites.share.actions.create', 'Weitere Einladung')}
|
||||
{t('invites.share.actions.create', 'Weitere QR-Code')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1562,7 +1582,7 @@ function InviteListCard({
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-foreground">{invite.label?.trim() || `Einladung #${invite.id}`}</span>
|
||||
<span className="text-sm font-semibold text-foreground">{invite.label?.trim() || `QR-Code #${invite.id}`}</span>
|
||||
<Badge variant="outline" className={statusBadgeClass(status)}>
|
||||
{status}
|
||||
</Badge>
|
||||
@@ -1651,11 +1671,11 @@ function EmptyState({ onCreate }: { onCreate: () => void }) {
|
||||
const { t } = useTranslation('management');
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-[var(--tenant-border-strong)] bg-[var(--tenant-surface-muted)] p-10 text-center transition-colors">
|
||||
<h3 className="text-base font-semibold text-foreground">{t('invites.empty.title', 'Noch keine Einladungen')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('invites.empty.copy', 'Erstelle eine Einladung, um druckfertige QR-Layouts zu erhalten.')}</p>
|
||||
<h3 className="text-base font-semibold text-foreground">{t('invites.empty.title', 'Noch keine QR-Code')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('invites.empty.copy', 'Erstelle eine QR-Code, um druckfertige QR-Layouts zu erhalten.')}</p>
|
||||
<Button onClick={onCreate} className="bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500 text-white">
|
||||
<Share2 className="mr-1 h-4 w-4" />
|
||||
{t('invites.actions.create', 'Neue Einladung erstellen')}
|
||||
{t('invites.actions.create', 'Neue QR-Code erstellen')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user