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:
Codex Agent
2025-11-27 16:08:08 +01:00
parent bfa15cc48e
commit 96f8c5d63c
39 changed files with 1970 additions and 640 deletions

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { AlertTriangle, ArrowLeft, Loader2, Save, Sparkles } from 'lucide-react';
import toast from 'react-hot-toast';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -16,6 +16,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { AdminLayout } from '../components/AdminLayout';
import { FloatingActionBar } from '../components/FloatingActionBar';
import {
createEvent,
getEvent,
@@ -67,6 +68,7 @@ export default function EventFormPage() {
const slugParam = params.slug ?? searchParams.get('slug') ?? undefined;
const isEdit = Boolean(slugParam);
const navigate = useNavigate();
const queryClient = useQueryClient();
const { t: tErrors } = useTranslation('common', { keyPrefix: 'errors' });
const { t: tForm } = useTranslation('management', { keyPrefix: 'eventForm' });
@@ -88,6 +90,7 @@ export default function EventFormPage() {
const [showUpgradeHint, setShowUpgradeHint] = React.useState(false);
const [readOnlyPackageName, setReadOnlyPackageName] = React.useState<string | null>(null);
const [eventPackageMeta, setEventPackageMeta] = React.useState<EventPackageMeta | null>(null);
const formRef = React.useRef<HTMLFormElement | null>(null);
const { data: packages, isLoading: packagesLoading } = useQuery({
queryKey: ['packages', 'endcustomer'],
@@ -143,7 +146,7 @@ export default function EventFormPage() {
queryKey: ['tenant', 'events', slugParam],
queryFn: () => getEvent(slugParam!),
enabled: Boolean(isEdit && slugParam),
staleTime: 60_000,
staleTime: 0,
});
React.useEffect(() => {
@@ -277,6 +280,16 @@ export default function EventFormPage() {
}
}
const handleSubmitClick = React.useCallback(() => {
if (formRef.current) {
if (typeof formRef.current.requestSubmit === 'function') {
formRef.current.requestSubmit();
} else {
formRef.current.submit();
}
}
}, []);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const trimmedName = form.name.trim();
@@ -320,14 +333,20 @@ export default function EventFormPage() {
if (isEdit) {
const targetSlug = originalSlug ?? slugParam!;
const updated = await updateEvent(targetSlug, payload);
queryClient.invalidateQueries({ queryKey: ['tenant', 'events'] });
queryClient.invalidateQueries({ queryKey: ['tenant', 'events', targetSlug] });
queryClient.invalidateQueries({ queryKey: ['tenant', 'dashboard'] });
setOriginalSlug(updated.slug);
setShowUpgradeHint(false);
setError(null);
toast.success(tForm('actions.saved', 'Event gespeichert'));
navigate(ADMIN_EVENT_VIEW_PATH(updated.slug));
} else {
const { event: created } = await createEvent(payload);
queryClient.invalidateQueries({ queryKey: ['tenant', 'events'] });
setShowUpgradeHint(false);
setError(null);
toast.success(tForm('actions.saved', 'Event gespeichert'));
navigate(ADMIN_EVENT_VIEW_PATH(created.slug));
}
} catch (err) {
@@ -456,6 +475,26 @@ export default function EventFormPage() {
</Button>
);
const fabActions = [
{
key: 'save',
label: saving ? tForm('actions.saving', 'Speichert') : tForm('actions.save', 'Speichern'),
icon: Save,
onClick: handleSubmitClick,
loading: saving,
disabled: loading || !form.name.trim() || !form.slug.trim() || !form.eventTypeId,
tone: 'primary' as const,
},
{
key: 'cancel',
label: tForm('actions.cancel', 'Abbrechen'),
icon: ArrowLeft,
onClick: () => navigate(-1),
disabled: saving,
tone: 'secondary' as const,
},
];
return (
<AdminLayout
title={isEdit ? tForm('titles.edit', 'Event bearbeiten') : tForm('titles.create', 'Neues Event erstellen')}
@@ -500,7 +539,7 @@ export default function EventFormPage() {
</div>
)}
<Card className="border-0 bg-white/85 shadow-xl shadow-fuchsia-100/60">
<Card className="border-0 bg-white/85 shadow-xl shadow-fuchsia-100/60 pb-28">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
<Sparkles className="h-5 w-5 text-pink-500" /> {tForm('sections.details.title', 'Eventdetails')}
@@ -513,7 +552,7 @@ export default function EventFormPage() {
{loading ? (
<FormSkeleton />
) : (
<form className="space-y-6" onSubmit={handleSubmit}>
<form className="space-y-6" onSubmit={handleSubmit} ref={formRef}>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="event-name">{tForm('fields.name.label', 'Eventname')}</Label>
@@ -585,26 +624,6 @@ export default function EventFormPage() {
</div>
</div>
<div className="flex flex-wrap gap-3">
<Button
type="submit"
disabled={saving || !form.name.trim() || !form.slug.trim() || !form.eventTypeId}
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
>
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" /> {tForm('actions.saving', 'Speichert')}
</>
) : (
<>
<Save className="h-4 w-4" /> {tForm('actions.save', 'Speichern')}
</>
)}
</Button>
<Button variant="ghost" type="button" onClick={() => navigate(-1)}>
{tForm('actions.cancel', 'Abbrechen')}
</Button>
</div>
<div className="sm:col-span-2 mt-6">
<Accordion type="single" collapsible defaultValue="package">
<AccordionItem value="package" className="border-0">
@@ -695,6 +714,7 @@ export default function EventFormPage() {
)}
</CardContent>
</Card>
<FloatingActionBar actions={fabActions} />
</AdminLayout>
);
}