umfangreiche Behebung von TS-Fehlern. "npm run types" läuft nun ohne Fehler durch
This commit is contained in:
@@ -1021,14 +1021,16 @@ function normalizeGuestNotification(raw: JsonValue): GuestNotificationSummary |
|
|||||||
}
|
}
|
||||||
|
|
||||||
const record = raw as Record<string, JsonValue>;
|
const record = raw as Record<string, JsonValue>;
|
||||||
|
const status = typeof record.status === 'string' ? record.status : null;
|
||||||
|
const audience = typeof record.audience_scope === 'string' ? record.audience_scope : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(record.id ?? 0),
|
id: Number(record.id ?? 0),
|
||||||
type: typeof record.type === 'string' ? record.type : 'broadcast',
|
type: typeof record.type === 'string' ? record.type : 'broadcast',
|
||||||
title: typeof record.title === 'string' ? record.title : '',
|
title: typeof record.title === 'string' ? record.title : '',
|
||||||
body: typeof record.body === 'string' ? record.body : null,
|
body: typeof record.body === 'string' ? record.body : null,
|
||||||
status: (record.status as GuestNotificationSummary['status']) ?? 'active',
|
status: status === 'draft' || status === 'archived' || status === 'active' ? status : 'active',
|
||||||
audience_scope: (record.audience_scope as GuestNotificationSummary['audience_scope']) ?? 'all',
|
audience_scope: audience === 'guest' || audience === 'all' ? audience : 'all',
|
||||||
target_identifier: typeof record.target_identifier === 'string' ? record.target_identifier : null,
|
target_identifier: typeof record.target_identifier === 'string' ? record.target_identifier : null,
|
||||||
payload: (record.payload as Record<string, unknown>) ?? null,
|
payload: (record.payload as Record<string, unknown>) ?? null,
|
||||||
priority: Number(record.priority ?? 0),
|
priority: Number(record.priority ?? 0),
|
||||||
@@ -1384,7 +1386,7 @@ export async function sendGuestNotification(
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
const data = await jsonOrThrow<{ data?: JsonValue }>(response, 'Failed to send guest notification');
|
const data = await jsonOrThrow<{ data?: JsonValue }>(response, 'Failed to send guest notification');
|
||||||
return normalizeGuestNotification(data.data ?? {}) ?? normalizeGuestNotification({
|
const fallback = normalizeGuestNotification({
|
||||||
id: 0,
|
id: 0,
|
||||||
type: payload.type ?? 'broadcast',
|
type: payload.type ?? 'broadcast',
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
@@ -1397,6 +1399,14 @@ export async function sendGuestNotification(
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
expires_at: null,
|
expires_at: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const normalized = normalizeGuestNotification(data.data ?? {}) ?? fallback;
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error('Failed to normalize guest notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEventPhotoboothStatus(slug: string): Promise<PhotoboothStatus> {
|
export async function getEventPhotoboothStatus(slug: string): Promise<PhotoboothStatus> {
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
cacheTime: 1000 * 60 * 30,
|
|
||||||
retry: 1,
|
retry: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function UserMenu() {
|
|||||||
<Languages className="h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
<span>{t('app.languageSwitch')}</span>
|
<span>{t('app.languageSwitch')}</span>
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent align="end">
|
<DropdownMenuSubContent>
|
||||||
{SUPPORTED_LANGUAGES.map(({ code, labelKey }) => (
|
{SUPPORTED_LANGUAGES.map(({ code, labelKey }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={code}
|
key={code}
|
||||||
@@ -137,7 +137,7 @@ export function UserMenu() {
|
|||||||
{appearance === 'dark' ? <Moon className="h-4 w-4" /> : appearance === 'light' ? <Sun className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
|
{appearance === 'dark' ? <Moon className="h-4 w-4" /> : appearance === 'light' ? <Sun className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
|
||||||
<span>{t('app.theme', { defaultValue: 'Darstellung' })}</span>
|
<span>{t('app.theme', { defaultValue: 'Darstellung' })}</span>
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent align="end">
|
<DropdownMenuSubContent>
|
||||||
{(['light', 'dark', 'system'] as const).map((mode) => (
|
{(['light', 'dark', 'system'] as const).map((mode) => (
|
||||||
<DropdownMenuItem key={mode} onSelect={() => changeAppearance(mode)}>
|
<DropdownMenuItem key={mode} onSelect={() => changeAppearance(mode)}>
|
||||||
{mode === 'light' ? <Sun className="h-4 w-4" /> : mode === 'dark' ? <Moon className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
|
{mode === 'light' ? <Sun className="h-4 w-4" /> : mode === 'dark' ? <Moon className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
cacheTime: 5 * 60 * 1000,
|
|
||||||
enabled: authReady,
|
enabled: authReady,
|
||||||
|
initialData: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = authReady ? fetchedEvents : [];
|
const events = authReady ? fetchedEvents : [];
|
||||||
|
|||||||
@@ -57,8 +57,11 @@ if (import.meta.env.DEV || import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = { loginAs, clients: Object.keys(CREDENTIALS) };
|
const api = {
|
||||||
console.info('[DevAuth] Demo tenant helpers ready', api.clients);
|
loginAs,
|
||||||
|
clients: Object.fromEntries(Object.entries(CREDENTIALS).map(([key, value]) => [key, value.login])),
|
||||||
|
};
|
||||||
|
console.info('[DevAuth] Demo tenant helpers ready', Object.keys(api.clients));
|
||||||
|
|
||||||
(window as typeof window & { fotospielDemoAuth?: typeof api }).fotospielDemoAuth = api;
|
(window as typeof window & { fotospielDemoAuth?: typeof api }).fotospielDemoAuth = api;
|
||||||
(globalThis as typeof globalThis & { fotospielDemoAuth?: typeof api }).fotospielDemoAuth = api;
|
(globalThis as typeof globalThis & { fotospielDemoAuth?: typeof api }).fotospielDemoAuth = api;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 1000 * 60, // 1 minute
|
staleTime: 1000 * 60, // 1 minute
|
||||||
cacheTime: 1000 * 60 * 5,
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useAuth } from '../auth/context';
|
|||||||
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants';
|
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants';
|
||||||
import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
|
import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
|
||||||
|
|
||||||
export default function AuthCallbackPage(): JSX.Element {
|
export default function AuthCallbackPage(): React.ReactElement {
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [redirected, setRedirected] = React.useState(false);
|
const [redirected, setRedirected] = React.useState(false);
|
||||||
|
|||||||
@@ -463,20 +463,18 @@ export default function DashboardPage() {
|
|||||||
: translate('overview.title', 'Kurzer Überblick');
|
: translate('overview.title', 'Kurzer Überblick');
|
||||||
|
|
||||||
const heroDescription = singleEvent
|
const heroDescription = singleEvent
|
||||||
? translate('overview.eventHero.description', 'Alles richtet sich nach {{event}}. Nächster Termin: {{date}}.', {
|
? translate('overview.eventHero.description', { defaultValue: 'Alles richtet sich nach {{event}}. Nächster Termin: {{date}}.', event: singleEventName ?? '', date: singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt') })
|
||||||
event: singleEventName ?? '',
|
|
||||||
date: singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt'),
|
|
||||||
})
|
|
||||||
: translate('overview.description', 'Wichtigste Kennzahlen deines Tenants auf einen Blick.');
|
: translate('overview.description', 'Wichtigste Kennzahlen deines Tenants auf einen Blick.');
|
||||||
|
|
||||||
const heroSupportingCopy = onboardingCompletion === 100 ? onboardingCompletedCopy : onboardingCardDescription;
|
const heroSupportingCopy = onboardingCompletion === 100 ? onboardingCompletedCopy : onboardingCardDescription;
|
||||||
const heroSupporting = singleEvent
|
const heroSupporting = singleEvent
|
||||||
? [
|
? [
|
||||||
translate('overview.eventHero.supporting.status', 'Status: {{status}}', {
|
translate('overview.eventHero.supporting.status', {
|
||||||
|
defaultValue: 'Status: {{status}}',
|
||||||
status: formatEventStatus(singleEvent.status ?? null, tc),
|
status: formatEventStatus(singleEvent.status ?? null, tc),
|
||||||
}),
|
}),
|
||||||
singleEventDateLabel
|
singleEventDateLabel
|
||||||
? translate('overview.eventHero.supporting.date', 'Eventdatum: {{date}}', { date: singleEventDateLabel })
|
? translate('overview.eventHero.supporting.date', singleEventDateLabel ?? 'Noch kein Datum festgelegt.')
|
||||||
: translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt.'),
|
: translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt.'),
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
: [heroSupportingCopy];
|
: [heroSupportingCopy];
|
||||||
@@ -636,9 +634,10 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const adminTitle = singleEventName ?? greetingTitle;
|
const adminTitle = singleEventName ?? greetingTitle;
|
||||||
const adminSubtitle = singleEvent
|
const adminSubtitle = singleEvent
|
||||||
? translate('overview.eventHero.subtitle', 'Alle Funktionen konzentrieren sich auf dieses Event.', {
|
? translate('overview.eventHero.subtitle', {
|
||||||
date: singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt'),
|
defaultValue: 'Alle Funktionen konzentrieren sich auf dieses Event.',
|
||||||
})
|
date: singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt'),
|
||||||
|
})
|
||||||
: subtitle;
|
: subtitle;
|
||||||
|
|
||||||
const heroTitle = adminTitle;
|
const heroTitle = adminTitle;
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ function PendingPhotosCard({
|
|||||||
return (
|
return (
|
||||||
<div key={photo.id} className="relative">
|
<div key={photo.id} className="relative">
|
||||||
<img
|
<img
|
||||||
src={photo.thumbnail_url ?? photo.url}
|
src={photo.thumbnail_url ?? photo.url ?? undefined}
|
||||||
alt={photo.caption ?? 'Foto'}
|
alt={photo.caption ?? 'Foto'}
|
||||||
className={`h-24 w-full rounded-lg object-cover ${hidden ? 'opacity-60' : ''}`}
|
className={`h-24 w-full rounded-lg object-cover ${hidden ? 'opacity-60' : ''}`}
|
||||||
/>
|
/>
|
||||||
@@ -779,7 +779,7 @@ function RecentUploadsCard({ slug, photos }: { slug: string; photos: TenantPhoto
|
|||||||
return (
|
return (
|
||||||
<div key={photo.id} className="relative">
|
<div key={photo.id} className="relative">
|
||||||
<img
|
<img
|
||||||
src={photo.thumbnail_url ?? photo.url}
|
src={photo.thumbnail_url ?? photo.url ?? undefined}
|
||||||
alt={photo.caption ?? 'Foto'}
|
alt={photo.caption ?? 'Foto'}
|
||||||
className={`h-24 w-full rounded-lg object-cover ${hidden ? 'opacity-60' : ''}`}
|
className={`h-24 w-full rounded-lg object-cover ${hidden ? 'opacity-60' : ''}`}
|
||||||
/>
|
/>
|
||||||
@@ -811,7 +811,7 @@ function FeedbackCard({ slug }: { slug: string }) {
|
|||||||
const [message, setMessage] = React.useState('');
|
const [message, setMessage] = React.useState('');
|
||||||
const [busy, setBusy] = React.useState(false);
|
const [busy, setBusy] = React.useState(false);
|
||||||
const [submitted, setSubmitted] = React.useState(false);
|
const [submitted, setSubmitted] = React.useState(false);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
positive: t('events.feedback.positive', 'Super Lauf!'),
|
positive: t('events.feedback.positive', 'Super Lauf!'),
|
||||||
@@ -862,7 +862,7 @@ function FeedbackCard({ slug }: { slug: string }) {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (busy || submitted) return;
|
if (busy || submitted) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submitTenantFeedback({
|
await submitTenantFeedback({
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function EventFormPage() {
|
|||||||
|
|
||||||
const { data: packageOverview, isLoading: overviewLoading } = useQuery({
|
const { data: packageOverview, isLoading: overviewLoading } = useQuery({
|
||||||
queryKey: ['tenant', 'packages', 'overview'],
|
queryKey: ['tenant', 'packages', 'overview'],
|
||||||
queryFn: getTenantPackagesOverview,
|
queryFn: () => getTenantPackagesOverview(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const activePackage = packageOverview?.activePackage ?? null;
|
const activePackage = packageOverview?.activePackage ?? null;
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ export default function EventMembersPage() {
|
|||||||
<Label htmlFor="invite-role">{t('management.members.form.roleLabel', 'Rolle')}</Label>
|
<Label htmlFor="invite-role">{t('management.members.form.roleLabel', 'Rolle')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value={invite.role}
|
value={invite.role}
|
||||||
onValueChange={(value) => setInvite((prev) => ({ ...prev, role: value }))}
|
onValueChange={(value) => setInvite((prev) => ({ ...prev, role: value as InviteForm['role'] }))}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="invite-role">
|
<SelectTrigger id="invite-role">
|
||||||
<SelectValue placeholder={t('management.members.form.rolePlaceholder', 'Rolle wählen')} />
|
<SelectValue placeholder={t('management.members.form.rolePlaceholder', 'Rolle wählen')} />
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default function EventPhotoboothPage() {
|
|||||||
|
|
||||||
const { event, status, loading, updating, error } = state;
|
const { event, status, loading, updating, error } = state;
|
||||||
const title = event
|
const title = event
|
||||||
? t('management.photobooth.titleForEvent', { defaultValue: 'Fotobox-Uploads verwalten', event: resolveEventName(event) })
|
? t('management.photobooth.titleForEvent', { defaultValue: 'Fotobox-Uploads verwalten', event: resolveEventName(event.name) })
|
||||||
: t('management.photobooth.title', 'Fotobox-Uploads');
|
: t('management.photobooth.title', 'Fotobox-Uploads');
|
||||||
const subtitle = t(
|
const subtitle = t(
|
||||||
'management.photobooth.subtitle',
|
'management.photobooth.subtitle',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function EventPhotosPage() {
|
|||||||
|
|
||||||
const [photos, setPhotos] = React.useState<TenantPhoto[]>([]);
|
const [photos, setPhotos] = React.useState<TenantPhoto[]>([]);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | undefined>(undefined);
|
||||||
const [busyId, setBusyId] = React.useState<number | null>(null);
|
const [busyId, setBusyId] = React.useState<number | null>(null);
|
||||||
const [limits, setLimits] = React.useState<EventLimitSummary | null>(null);
|
const [limits, setLimits] = React.useState<EventLimitSummary | null>(null);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export default function EventPhotosPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(undefined);
|
||||||
try {
|
try {
|
||||||
const result = await getEventPhotos(slug);
|
const result = await getEventPhotos(slug);
|
||||||
setPhotos(result.photos);
|
setPhotos(result.photos);
|
||||||
@@ -150,7 +150,7 @@ export default function EventPhotosPage() {
|
|||||||
{photos.map((photo) => (
|
{photos.map((photo) => (
|
||||||
<div key={photo.id} className="rounded-2xl border border-white/80 bg-white/90 p-3 shadow-sm">
|
<div key={photo.id} className="rounded-2xl border border-white/80 bg-white/90 p-3 shadow-sm">
|
||||||
<div className="relative overflow-hidden rounded-xl">
|
<div className="relative overflow-hidden rounded-xl">
|
||||||
<img src={photo.thumbnail_url || photo.url} alt={photo.original_name ?? 'Foto'} className="aspect-square w-full object-cover" />
|
<img src={photo.thumbnail_url ?? photo.url ?? undefined} alt={photo.original_name ?? 'Foto'} className="aspect-square w-full object-cover" />
|
||||||
{photo.is_featured && (
|
{photo.is_featured && (
|
||||||
<span className="absolute left-3 top-3 rounded-full bg-pink-500/90 px-3 py-1 text-xs font-semibold text-white shadow">
|
<span className="absolute left-3 top-3 rounded-full bg-pink-500/90 px-3 py-1 text-xs font-semibold text-white shadow">
|
||||||
Featured
|
Featured
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type LoginResponse = {
|
|||||||
return (await response.json()) as LoginResponse;
|
return (await response.json()) as LoginResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginPage(): JSX.Element {
|
export default function LoginPage(): React.ReactElement {
|
||||||
const { status, applyToken, abilities } = useAuth();
|
const { status, applyToken, abilities } = useAuth();
|
||||||
const { t } = useTranslation('auth');
|
const { t } = useTranslation('auth');
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -95,7 +95,9 @@ export default function LoginPage(): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSubmitting = (mutation as { isPending?: boolean; isLoading: boolean }).isPending ?? mutation.isLoading;
|
const isSubmitting = (mutation as { isPending?: boolean; isLoading?: boolean }).isPending
|
||||||
|
?? (mutation as { isPending?: boolean; isLoading?: boolean }).isLoading
|
||||||
|
?? false;
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect } from 'react';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||||
|
|
||||||
export default function LoginStartPage(): JSX.Element {
|
export default function LoginStartPage(): React.ReactElement {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -327,14 +327,14 @@ export function DesignerCanvas({
|
|||||||
canvas.requestRenderAll();
|
canvas.requestRenderAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.on('editing:exited', handleEditingExited);
|
(canvas as any).on('editing:exited', handleEditingExited);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.off('selection:created', handleSelection);
|
canvas.off('selection:created', handleSelection);
|
||||||
canvas.off('selection:updated', handleSelection);
|
canvas.off('selection:updated', handleSelection);
|
||||||
canvas.off('selection:cleared', handleSelectionCleared);
|
canvas.off('selection:cleared', handleSelectionCleared);
|
||||||
canvas.off('object:modified', handleObjectModified);
|
canvas.off('object:modified', handleObjectModified);
|
||||||
canvas.off('editing:exited', handleEditingExited);
|
(canvas as any).off('editing:exited', handleEditingExited);
|
||||||
};
|
};
|
||||||
}, [onChange, onSelect, readOnly]);
|
}, [onChange, onSelect, readOnly]);
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ type NotificationButtonProps = {
|
|||||||
eventToken: string;
|
eventToken: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
panelRef: React.RefObject<HTMLDivElement>;
|
panelRef: React.RefObject<HTMLDivElement | null>;
|
||||||
checklistItems: string[];
|
checklistItems: string[];
|
||||||
taskProgress?: ReturnType<typeof useGuestTaskProgress>;
|
taskProgress?: ReturnType<typeof useGuestTaskProgress>;
|
||||||
t: TranslateFn;
|
t: TranslateFn;
|
||||||
@@ -316,7 +316,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
|
|||||||
<p className="text-sm font-semibold text-slate-900">{t('header.notifications.title', 'Benachrichtigungen')}</p>
|
<p className="text-sm font-semibold text-slate-900">{t('header.notifications.title', 'Benachrichtigungen')}</p>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
{center.unreadCount > 0
|
{center.unreadCount > 0
|
||||||
? t('header.notifications.unread', '{{count}} neu', { count: center.unreadCount })
|
? t('header.notifications.unread', { defaultValue: '{{count}} neu', count: center.unreadCount })
|
||||||
: t('header.notifications.allRead', 'Alles gelesen')}
|
: t('header.notifications.allRead', 'Alles gelesen')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export const demoFixtures: DemoFixtures = {
|
|||||||
],
|
],
|
||||||
share: {
|
share: {
|
||||||
slug: 'demo-share',
|
slug: 'demo-share',
|
||||||
expires_at: null,
|
expires_at: undefined,
|
||||||
photo: {
|
photo: {
|
||||||
id: 8801,
|
id: 8801,
|
||||||
title: 'First Look',
|
title: 'First Look',
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function usePushSubscription(eventToken?: string): PushSubscriptionState
|
|||||||
|
|
||||||
const newSubscription = await registration.pushManager.subscribe({
|
const newSubscription = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(pushConfig.vapidPublicKey),
|
applicationServerKey: urlBase64ToUint8Array(pushConfig.vapidPublicKey).buffer as ArrayBuffer,
|
||||||
});
|
});
|
||||||
|
|
||||||
await registerPushSubscription(eventToken, newSubscription);
|
await registerPushSubscription(eventToken, newSubscription);
|
||||||
|
|||||||
@@ -482,9 +482,6 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
optimizedNotice: 'Wir haben dein Foto verkleinert, damit der Upload schneller klappt. Eingespart: {saved}',
|
optimizedNotice: 'Wir haben dein Foto verkleinert, damit der Upload schneller klappt. Eingespart: {saved}',
|
||||||
optimizedFallback: 'Optimierung nicht möglich – wir laden das Original hoch.',
|
optimizedFallback: 'Optimierung nicht möglich – wir laden das Original hoch.',
|
||||||
retrying: 'Verbindung holperig – neuer Versuch ({attempt}).',
|
retrying: 'Verbindung holperig – neuer Versuch ({attempt}).',
|
||||||
errors: {
|
|
||||||
tooLargeHint: 'Das Foto war zu groß. Bitte erneut versuchen – wir verkleinern es automatisch.',
|
|
||||||
},
|
|
||||||
controls: {
|
controls: {
|
||||||
toggleGrid: 'Raster umschalten',
|
toggleGrid: 'Raster umschalten',
|
||||||
toggleCountdown: 'Countdown umschalten',
|
toggleCountdown: 'Countdown umschalten',
|
||||||
@@ -560,6 +557,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
packageMissing: 'Dieses Event akzeptiert derzeit keine Uploads.',
|
packageMissing: 'Dieses Event akzeptiert derzeit keine Uploads.',
|
||||||
galleryExpired: 'Die Galerie ist abgelaufen. Uploads sind nicht mehr möglich.',
|
galleryExpired: 'Die Galerie ist abgelaufen. Uploads sind nicht mehr möglich.',
|
||||||
generic: 'Upload fehlgeschlagen. Bitte versuche es erneut.',
|
generic: 'Upload fehlgeschlagen. Bitte versuche es erneut.',
|
||||||
|
tooLargeHint: 'Das Foto war zu groß. Bitte erneut versuchen – wir verkleinern es automatisch.',
|
||||||
},
|
},
|
||||||
cameraInactive: 'Kamera ist nicht aktiv. {hint}',
|
cameraInactive: 'Kamera ist nicht aktiv. {hint}',
|
||||||
cameraInactiveHint: 'Tippe auf "{label}", um loszulegen.',
|
cameraInactiveHint: 'Tippe auf "{label}", um loszulegen.',
|
||||||
@@ -1126,9 +1124,6 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
optimizedNotice: 'We optimized your photo to speed up the upload. Saved: {saved}',
|
optimizedNotice: 'We optimized your photo to speed up the upload. Saved: {saved}',
|
||||||
optimizedFallback: 'Could not optimize – uploading the original.',
|
optimizedFallback: 'Could not optimize – uploading the original.',
|
||||||
retrying: 'Connection unstable – retrying ({attempt}).',
|
retrying: 'Connection unstable – retrying ({attempt}).',
|
||||||
errors: {
|
|
||||||
tooLargeHint: 'The photo was too large. Please try again — we compress it automatically.',
|
|
||||||
},
|
|
||||||
controls: {
|
controls: {
|
||||||
toggleGrid: 'Toggle grid',
|
toggleGrid: 'Toggle grid',
|
||||||
toggleCountdown: 'Toggle countdown',
|
toggleCountdown: 'Toggle countdown',
|
||||||
@@ -1204,6 +1199,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
packageMissing: 'This event is not accepting uploads right now.',
|
packageMissing: 'This event is not accepting uploads right now.',
|
||||||
galleryExpired: 'The gallery has expired. Uploads are no longer possible.',
|
galleryExpired: 'The gallery has expired. Uploads are no longer possible.',
|
||||||
generic: 'Upload failed. Please try again.',
|
generic: 'Upload failed. Please try again.',
|
||||||
|
tooLargeHint: 'The photo was too large. Please try again — we compress it automatically.',
|
||||||
},
|
},
|
||||||
cameraInactive: 'Camera is not active. {hint}',
|
cameraInactive: 'Camera is not active. {hint}',
|
||||||
cameraInactiveHint: 'Tap "{label}" to get started.',
|
cameraInactiveHint: 'Tap "{label}" to get started.',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function HomePage() {
|
|||||||
const { name, hydrated } = useGuestIdentity();
|
const { name, hydrated } = useGuestIdentity();
|
||||||
const stats = useEventStats();
|
const stats = useEventStats();
|
||||||
const { event } = useEventData();
|
const { event } = useEventData();
|
||||||
const { completedCount } = useGuestTaskProgress(token);
|
const { completedCount } = useGuestTaskProgress(token ?? '');
|
||||||
const { t, locale } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const { branding } = useEventBranding();
|
const { branding } = useEventBranding();
|
||||||
|
|
||||||
@@ -86,15 +86,16 @@ export default function HomePage() {
|
|||||||
async function loadMissions() {
|
async function loadMissions() {
|
||||||
setMissionLoading(true);
|
setMissionLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const safeToken = token ?? '';
|
||||||
`/api/v1/events/${encodeURIComponent(token)}/tasks?locale=${encodeURIComponent(locale)}`,
|
const response = await fetch(
|
||||||
{
|
`/api/v1/events/${encodeURIComponent(safeToken)}/tasks?locale=${encodeURIComponent(locale)}`,
|
||||||
headers: {
|
{
|
||||||
Accept: 'application/json',
|
headers: {
|
||||||
'X-Locale': locale,
|
Accept: 'application/json',
|
||||||
},
|
'X-Locale': locale,
|
||||||
}
|
},
|
||||||
);
|
}
|
||||||
|
);
|
||||||
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
|
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const INITIAL_STATE: GalleryState = {
|
|||||||
|
|
||||||
const GALLERY_PAGE_SIZE = 30;
|
const GALLERY_PAGE_SIZE = 30;
|
||||||
|
|
||||||
export default function PublicGalleryPage(): JSX.Element | null {
|
export default function PublicGalleryPage(): React.ReactElement | null {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState<GalleryState>(INITIAL_STATE);
|
const [state, setState] = useState<GalleryState>(INITIAL_STATE);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Sparkles, RefreshCw, Smile, Camera, Timer as TimerIcon, Heart, ChevronRight } from 'lucide-react';
|
import { Sparkles, RefreshCw, Smile, Camera, Timer as TimerIcon, Heart, ChevronRight, CheckCircle2 } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
|
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
|
|||||||
@@ -882,7 +882,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
|||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t('upload.limitReached')
|
{t('upload.limitReached')
|
||||||
.replace('{used}', `${eventPackage?.used_photos || 0}`)
|
.replace('{used}', `${eventPackage?.used_photos || 0}`)
|
||||||
.replace('{max}', `${eventPackage?.package.max_photos || 0}`)}
|
.replace('{max}', `${eventPackage?.package?.max_photos || 0}`)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -147,12 +147,14 @@ export async function getHelpArticle(slug: string, locale: LocaleCode): Promise<
|
|||||||
locale,
|
locale,
|
||||||
});
|
});
|
||||||
const data = await requestJson<{ data?: HelpArticleDetail }>(`/api/v1/help/${encodeURIComponent(slug)}?${params.toString()}`);
|
const data = await requestJson<{ data?: HelpArticleDetail }>(`/api/v1/help/${encodeURIComponent(slug)}?${params.toString()}`);
|
||||||
const article = data?.data ?? { slug, title: slug, summary: '' };
|
const article: HelpArticleDetail | undefined = data?.data;
|
||||||
writeCache(cacheKey, article);
|
const safeArticle: HelpArticleDetail = article ?? { slug, title: slug, summary: '' };
|
||||||
return { article, servedFromCache: false };
|
writeCache(cacheKey, safeArticle);
|
||||||
|
return { article: safeArticle, servedFromCache: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cached) {
|
const cachedArticle: HelpArticleDetail | undefined = (cached as { data?: HelpArticleDetail } | null | undefined)?.data;
|
||||||
return { article: cached.data, servedFromCache: true };
|
if (cachedArticle) {
|
||||||
|
return { article: cachedArticle, servedFromCache: true };
|
||||||
}
|
}
|
||||||
console.error('[HelpApi] Failed to fetch help article', error);
|
console.error('[HelpApi] Failed to fetch help article', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user