Files
fotospiel-app/resources/js/admin/mobile/EventFormPage.tsx

262 lines
8.5 KiB
TypeScript

import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { CalendarDays, ChevronDown, MapPin } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton } from './components/Primitives';
import { createEvent, getEvent, updateEvent, TenantEvent } from '../api';
import { adminPath } from '../constants';
import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
type FormState = {
name: string;
date: string;
eventType: string;
description: string;
location: string;
enableBranding: boolean;
};
const EVENT_TYPES = ['Wedding', 'Corporate', 'Party', 'Other'];
export default function MobileEventFormPage() {
const { slug: slugParam } = useParams<{ slug?: string }>();
const slug = slugParam ?? null;
const isEdit = Boolean(slug);
const navigate = useNavigate();
const { t } = useTranslation('management');
const [form, setForm] = React.useState<FormState>({
name: '',
date: '',
eventType: EVENT_TYPES[0],
description: '',
location: '',
enableBranding: false,
});
const [loading, setLoading] = React.useState(isEdit);
const [saving, setSaving] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
if (!slug) return;
(async () => {
setLoading(true);
try {
const data = await getEvent(slug);
setForm({
name: renderName(data.name),
date: data.event_date ?? '',
eventType: data.event_type?.name ?? EVENT_TYPES[0],
description: typeof data.description === 'string' ? data.description : '',
location: resolveLocation(data),
enableBranding: Boolean((data.settings as Record<string, unknown>)?.branding_allowed ?? true),
});
setError(null);
} catch (err) {
if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.')));
}
} finally {
setLoading(false);
}
})();
}, [slug, t, isEdit]);
async function handleSubmit() {
setSaving(true);
setError(null);
try {
if (isEdit && slug) {
await updateEvent(slug, {
name: form.name,
event_date: form.date || undefined,
settings: { branding_allowed: form.enableBranding, location: form.location },
});
navigate(adminPath(`/mobile/events/${slug}`));
} else {
const payload = {
name: form.name || 'Event',
slug: `${Date.now()}`,
event_type_id: 1,
event_date: form.date || undefined,
status: 'draft' as const,
settings: { branding_allowed: form.enableBranding, location: form.location },
};
const { event } = await createEvent(payload as any);
navigate(adminPath(`/mobile/events/${event.slug}`));
}
} catch (err) {
if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Event konnte nicht gespeichert werden.')));
}
} finally {
setSaving(false);
}
}
return (
<MobileShell
activeTab="home"
title={isEdit ? t('events.form.editTitle', 'Edit Event') : t('events.form.createTitle', 'Create New Event')}
onBack={() => navigate(-1)}
>
{error ? (
<MobileCard>
<Text fontWeight="700" color="#b91c1c">
{error}
</Text>
</MobileCard>
) : null}
<MobileCard space="$3">
<Field label={t('events.form.name', 'Event Name')}>
<input
type="text"
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
placeholder="e.g., Smith Wedding"
style={inputStyle}
/>
</Field>
<Field label={t('events.form.date', 'Date & Time')}>
<XStack alignItems="center" space="$2">
<input
type="datetime-local"
value={form.date}
onChange={(e) => setForm((prev) => ({ ...prev, date: e.target.value }))}
style={{ ...inputStyle, flex: 1 }}
/>
<CalendarDays size={16} color="#9ca3af" />
</XStack>
</Field>
<Field label={t('events.form.type', 'Event Type')}>
<XStack space="$1" flexWrap="wrap">
{EVENT_TYPES.map((type) => {
const active = form.eventType === type;
return (
<button
key={type}
type="button"
onClick={() => setForm((prev) => ({ ...prev, eventType: type }))}
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${active ? '#007AFF' : '#e5e7eb'}`,
background: active ? '#e8f1ff' : 'white',
color: active ? '#0f172a' : '#111827',
fontWeight: 700,
minWidth: 90,
textAlign: 'center',
}}
>
{type}
</button>
);
})}
</XStack>
</Field>
<Field label={t('events.form.description', 'Optional Details')}>
<textarea
value={form.description}
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
placeholder={t('events.form.descriptionPlaceholder', 'Description')}
style={{ ...inputStyle, minHeight: 96 }}
/>
</Field>
<Field label={t('events.form.location', 'Location')}>
<XStack alignItems="center" space="$2">
<input
type="text"
value={form.location}
onChange={(e) => setForm((prev) => ({ ...prev, location: e.target.value }))}
placeholder={t('events.form.locationPlaceholder', 'Location')}
style={{ ...inputStyle, flex: 1 }}
/>
<MapPin size={16} color="#9ca3af" />
</XStack>
</Field>
<Field label={t('events.form.enableBranding', 'Enable Branding & Moderation')}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
checked={form.enableBranding}
onChange={(e) => setForm((prev) => ({ ...prev, enableBranding: e.target.checked }))}
/>
<Text fontSize="$sm" color="#111827">
{form.enableBranding ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')}
</Text>
</label>
</Field>
</MobileCard>
<YStack space="$2">
{!isEdit ? (
<button
type="button"
onClick={() => navigate(-1)}
style={{
...inputStyle,
height: 48,
borderRadius: 12,
border: '1px solid #e5e7eb',
background: '#f1f5f9',
fontWeight: 700,
}}
>
{t('events.form.saveDraft', 'Save as Draft')}
</button>
) : null}
<CTAButton label={saving ? t('events.form.saving', 'Saving...') : isEdit ? t('events.form.update', 'Update Event') : t('events.form.create', 'Create Event')} onPress={() => handleSubmit()} />
</YStack>
</MobileShell>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<YStack space="$1.5">
<Text fontSize="$sm" fontWeight="800" color="#111827">
{label}
</Text>
{children}
</YStack>
);
}
const inputStyle: React.CSSProperties = {
width: '100%',
height: 44,
borderRadius: 10,
border: '1px solid #e5e7eb',
padding: '0 12px',
fontSize: 14,
background: 'white',
};
function renderName(name: TenantEvent['name']): string {
if (typeof name === 'string') return name;
if (name && typeof name === 'object') {
return name.de ?? name.en ?? Object.values(name)[0] ?? '';
}
return '';
}
function resolveLocation(event: TenantEvent): string {
const settings = (event.settings ?? {}) as Record<string, unknown>;
const candidate =
(settings.location as string | undefined) ??
(settings.address as string | undefined) ??
(settings.city as string | undefined);
if (candidate && candidate.trim()) return candidate;
return '';
}