feat: localize guest endpoints and caching

This commit is contained in:
Codex Agent
2025-11-12 15:48:06 +01:00
parent d91108c883
commit 062932ce38
19 changed files with 1538 additions and 595 deletions

View File

@@ -8,6 +8,7 @@ import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { cn } from '@/lib/utils';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import EmotionPicker from '../components/EmotionPicker';
import { useEventBranding } from '../context/EventBrandingContext';
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
@@ -168,7 +169,7 @@ export default function TaskPickerPage() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { branding } = useEventBranding();
const { t } = useTranslation();
const { t, locale } = useTranslation();
const { completedCount, isCompleted } = useGuestTaskProgress(eventKey);
@@ -194,31 +195,61 @@ export default function TaskPickerPage() {
}), [branding.primaryColor, branding.secondaryColor]);
const recentTaskIdsRef = React.useRef<number[]>([]);
const tasksCacheRef = React.useRef<Map<string, { data: Task[]; etag?: string | null }>>(new Map());
const initialEmotionRef = React.useRef(false);
const fetchTasks = React.useCallback(async () => {
if (!eventKey) return;
const cacheKey = `${eventKey}:${locale}`;
const cached = tasksCacheRef.current.get(cacheKey);
setIsFetching(true);
setLoading(true);
setLoading(!cached);
setError(null);
if (cached) {
setTasks(cached.data);
}
try {
const response = await fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/tasks`);
const headers: HeadersInit = {
Accept: 'application/json',
'X-Locale': locale,
};
if (cached?.etag) {
headers['If-None-Match'] = cached.etag;
}
const response = await fetch(
`/api/v1/events/${encodeURIComponent(eventKey)}/tasks?locale=${encodeURIComponent(locale)}`,
{ headers }
);
if (response.status === 304 && cached) {
return;
}
if (!response.ok) throw new Error('Aufgaben konnten nicht geladen werden.');
const payload = await response.json();
if (Array.isArray(payload)) {
const entry = { data: payload, etag: response.headers.get('ETag') };
tasksCacheRef.current.set(cacheKey, entry);
setTasks(payload);
} else {
tasksCacheRef.current.set(cacheKey, { data: [], etag: response.headers.get('ETag') });
setTasks([]);
}
} catch (err) {
console.error('Failed to load tasks', err);
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
setTasks([]);
if (!cached) {
setTasks([]);
}
} finally {
setIsFetching(false);
setLoading(false);
}
}, [eventKey]);
}, [eventKey, locale]);
React.useEffect(() => {
fetchTasks();
@@ -348,8 +379,12 @@ export default function TaskPickerPage() {
const controller = new AbortController();
setPhotoPoolLoading(true);
setPhotoPoolError(null);
fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/photos?locale=de`, {
fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/photos?locale=${encodeURIComponent(locale)}`, {
signal: controller.signal,
headers: {
Accept: 'application/json',
'X-Locale': locale,
},
})
.then((res) => {
if (!res.ok) {
@@ -373,7 +408,7 @@ export default function TaskPickerPage() {
});
return () => controller.abort();
}, [eventKey, photoPool.length, t]);
}, [eventKey, photoPool.length, t, locale]);
const similarPhotos = React.useMemo(() => {
if (!currentTask) return [];