typescript-typenfehler behoben.. npm run lint läuft nun fehlerfrei durch.

This commit is contained in:
Codex Agent
2025-11-22 11:49:47 +01:00
parent 6c78d7e281
commit eb41cb6194
74 changed files with 469 additions and 396 deletions

View File

@@ -6,13 +6,28 @@ import FiltersBar, { type GalleryFilter } from '../components/FiltersBar';
import { Heart, Image as ImageIcon, Share2 } from 'lucide-react';
import { likePhoto } from '../services/photosApi';
import PhotoLightbox from './PhotoLightbox';
import { fetchEvent, fetchStats, type EventData, type EventStats } from '../services/eventApi';
import { fetchEvent, type EventData } from '../services/eventApi';
import { useTranslation } from '../i18n/useTranslation';
import { sharePhotoLink } from '../lib/sharePhoto';
import { useToast } from '../components/ToastHost';
import { localizeTaskLabel } from '../lib/localizeTaskLabel';
const allowedGalleryFilters: GalleryFilter[] = ['latest', 'popular', 'mine', 'photobooth'];
type GalleryPhoto = {
id: number;
likes_count?: number | null;
created_at?: string | null;
ingest_source?: string | null;
session_id?: string | null;
task_id?: number | null;
task_title?: string | null;
emotion_id?: number | null;
emotion_name?: string | null;
thumbnail_path?: string | null;
file_path?: string | null;
title?: string | null;
uploader_name?: string | null;
};
const parseGalleryFilter = (value: string | null): GalleryFilter =>
allowedGalleryFilters.includes(value as GalleryFilter) ? (value as GalleryFilter) : 'latest';
@@ -46,7 +61,6 @@ export default function GalleryPage() {
const [hasOpenedPhoto, setHasOpenedPhoto] = useState(false);
const [event, setEvent] = useState<EventData | null>(null);
const [stats, setStats] = useState<EventStats | null>(null);
const [eventLoading, setEventLoading] = useState(true);
const toast = useToast();
const [shareTargetId, setShareTargetId] = React.useState<number | null>(null);
@@ -62,16 +76,18 @@ export default function GalleryPage() {
params.set('mode', next);
setSearchParams(params, { replace: true });
}, [searchParams, setSearchParams]);
const typedPhotos = photos as GalleryPhoto[];
// Auto-open lightbox if photoId in query params
useEffect(() => {
if (photoIdParam && photos.length > 0 && currentPhotoIndex === null && !hasOpenedPhoto) {
const index = photos.findIndex((photo: any) => photo.id === parseInt(photoIdParam, 10));
const index = typedPhotos.findIndex((photo) => photo.id === parseInt(photoIdParam, 10));
if (index !== -1) {
setCurrentPhotoIndex(index);
setHasOpenedPhoto(true);
}
}
}, [photos, photoIdParam, currentPhotoIndex, hasOpenedPhoto]);
}, [typedPhotos, photos.length, photoIdParam, currentPhotoIndex, hasOpenedPhoto]);
// Load event and package info
useEffect(() => {
@@ -80,12 +96,8 @@ export default function GalleryPage() {
const loadEventData = async () => {
try {
setEventLoading(true);
const [eventData, statsData] = await Promise.all([
fetchEvent(token),
fetchStats(token),
]);
const eventData = await fetchEvent(token);
setEvent(eventData);
setStats(statsData);
} catch (err) {
console.error('Failed to load event data', err);
} finally {
@@ -104,27 +116,22 @@ export default function GalleryPage() {
}, []);
const list = React.useMemo(() => {
let arr = photos.slice();
let arr = typedPhotos.slice();
if (filter === 'popular') {
arr.sort((a: any, b: any) => (b.likes_count ?? 0) - (a.likes_count ?? 0));
arr.sort((a, b) => (b.likes_count ?? 0) - (a.likes_count ?? 0));
} else if (filter === 'mine') {
arr = arr.filter((p: any) => myPhotoIds.has(p.id));
arr = arr.filter((p) => myPhotoIds.has(p.id));
} else if (filter === 'photobooth') {
arr = arr.filter((p: any) => p.ingest_source === 'photobooth');
arr.sort((a: any, b: any) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
arr = arr.filter((p) => p.ingest_source === 'photobooth');
arr.sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
} else {
arr.sort((a: any, b: any) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
arr.sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
}
return arr;
}, [photos, filter, myPhotoIds]);
}, [typedPhotos, filter, myPhotoIds]);
const [liked, setLiked] = React.useState<Set<number>>(new Set());
const [counts, setCounts] = React.useState<Record<number, number>>({});
const totalLikes = React.useMemo(
() => photos.reduce((sum, photo: any) => sum + (photo.likes_count ?? 0), 0),
[photos],
);
async function onLike(id: number) {
if (liked.has(id)) return;
setLiked(new Set(liked).add(id));
@@ -136,13 +143,16 @@ export default function GalleryPage() {
const raw = localStorage.getItem('liked-photo-ids');
const arr: number[] = raw ? JSON.parse(raw) : [];
if (!arr.includes(id)) localStorage.setItem('liked-photo-ids', JSON.stringify([...arr, id]));
} catch {}
} catch {
} catch (error) {
console.warn('Failed to persist liked-photo-ids', error);
}
} catch (error) {
console.warn('Like failed', error);
const s = new Set(liked); s.delete(id); setLiked(s);
}
}
async function onShare(photo: any) {
async function onShare(photo: GalleryPhoto) {
if (!token) return;
setShareTargetId(photo.id);
try {
@@ -223,7 +233,7 @@ export default function GalleryPage() {
<FiltersBar value={filter} onChange={setFilter} className="mt-2" />
{loading && <p className="px-4">{t('galleryPage.loading', 'Lade…')}</p>}
<div className="grid gap-3 px-4 pb-16 sm:grid-cols-2 lg:grid-cols-3">
{list.map((p: any) => {
{list.map((p: GalleryPhoto) => {
const imageUrl = normalizeImageUrl(p.thumbnail_path || p.file_path);
const createdLabel = p.created_at
? new Date(p.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
@@ -236,7 +246,7 @@ export default function GalleryPage() {
const altText = t('galleryPage.photo.alt', { id: p.id, suffix: altSuffix }, `Foto ${p.id}${altSuffix}`);
const openPhoto = () => {
const index = list.findIndex((photo: any) => photo.id === p.id);
const index = list.findIndex((photo) => photo.id === p.id);
setCurrentPhotoIndex(index >= 0 ? index : null);
};

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { Page } from './_util';
@@ -15,7 +14,6 @@ export default function HelpArticlePage() {
const { t } = useTranslation();
const [article, setArticle] = React.useState<HelpArticleDetail | null>(null);
const [state, setState] = React.useState<'loading' | 'ready' | 'error'>('loading');
const [servedFromCache, setServedFromCache] = React.useState(false);
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
const loadArticle = React.useCallback(async () => {
@@ -27,7 +25,6 @@ export default function HelpArticlePage() {
try {
const result = await getHelpArticle(slug, locale);
setArticle(result.article);
setServedFromCache(result.servedFromCache);
setState('ready');
} catch (error) {
console.error('[HelpArticle] Failed to load article', error);
@@ -117,7 +114,7 @@ function formatDate(value: string, locale: string): string {
month: 'short',
year: 'numeric',
});
} catch (error) {
} catch {
return value;
}
}

View File

@@ -147,7 +147,7 @@ function formatDate(value: string, locale: string): string {
month: 'short',
year: 'numeric',
});
} catch (error) {
} catch {
return value;
}
}

View File

@@ -6,10 +6,9 @@ import { Separator } from '@/components/ui/separator';
import EmotionPicker from '../components/EmotionPicker';
import GalleryPreview from '../components/GalleryPreview';
import { useGuestIdentity } from '../context/GuestIdentityContext';
import { useEventStats } from '../context/EventStatsContext';
import { useEventData } from '../hooks/useEventData';
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
import { Sparkles, UploadCloud, X, Camera, RefreshCw } from 'lucide-react';
import { Sparkles, UploadCloud, X, RefreshCw } from 'lucide-react';
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { useEventBranding } from '../context/EventBrandingContext';
import type { EventBranding } from '../types/event-branding';
@@ -17,7 +16,6 @@ import type { EventBranding } from '../types/event-branding';
export default function HomePage() {
const { token } = useParams<{ token: string }>();
const { name, hydrated } = useGuestIdentity();
const stats = useEventStats();
const { event } = useEventData();
const { completedCount } = useGuestTaskProgress(token ?? '');
const { t, locale } = useTranslation();
@@ -100,10 +98,10 @@ export default function HomePage() {
const payload = await response.json();
if (cancelled) return;
if (Array.isArray(payload) && payload.length) {
missionPoolRef.current = payload.map((task: any) => ({
missionPoolRef.current = payload.map((task: Record<string, unknown>) => ({
id: Number(task.id),
title: task.title ?? 'Mission',
description: task.description ?? '',
title: typeof task.title === 'string' ? task.title : 'Mission',
description: typeof task.description === 'string' ? task.description : '',
duration: typeof task.duration === 'number' ? task.duration : 3,
emotion: task.emotion ?? null,
}));

View File

@@ -38,8 +38,6 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
const toast = useToast();
const [standalonePhoto, setStandalonePhoto] = useState<Photo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [task, setTask] = useState<Task | null>(null);
const [taskLoading, setTaskLoading] = useState(false);
const [likes, setLikes] = useState<number>(0);
@@ -59,8 +57,6 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
useEffect(() => {
if (isStandalone && photoId && !standalonePhoto && eventToken) {
const fetchPhoto = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/v1/photos/${photoId}?locale=${encodeURIComponent(locale)}`, {
headers: {
@@ -76,20 +72,17 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
setStandalonePhoto(location.state.photo);
}
} else {
setError(t('lightbox.errors.notFound'));
toast.push({ text: t('lightbox.errors.notFound'), type: 'error' });
}
} catch (err) {
setError(t('lightbox.errors.loadFailed'));
} finally {
setLoading(false);
console.warn('Standalone photo load failed', err);
toast.push({ text: t('lightbox.errors.loadFailed'), type: 'error' });
}
};
fetchPhoto();
} else if (!isStandalone) {
setLoading(false);
}
}, [isStandalone, photoId, eventToken, standalonePhoto, location.state, t, locale]);
}, [isStandalone, photoId, eventToken, standalonePhoto, location.state, t, locale, toast]);
// Update likes when photo changes
React.useEffect(() => {
@@ -163,8 +156,8 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
}
);
if (res.ok) {
const tasks = await res.json();
const foundTask = tasks.find((t: any) => t.id === taskId);
const tasks = (await res.json()) as Task[];
const foundTask = tasks.find((t) => t.id === taskId);
if (foundTask) {
setTask({
id: foundTask.id,
@@ -207,7 +200,9 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
if (!arr.includes(photo.id)) {
localStorage.setItem('liked-photo-ids', JSON.stringify([...arr, photo.id]));
}
} catch {}
} catch (storageError) {
console.warn('Failed to persist liked photo IDs', storageError);
}
} catch (error) {
console.error('Like failed:', error);
setLiked(false);

View File

@@ -40,7 +40,7 @@ export default function PublicGalleryPage(): React.ReactElement | null {
const localeStorageKey = token ? `guestGalleryLocale_${token}` : 'guestGalleryLocale';
const storedLocale = typeof window !== 'undefined' && token ? localStorage.getItem(localeStorageKey) : null;
const effectiveLocale = storedLocale && isLocaleCode(storedLocale as any) ? (storedLocale as any) : DEFAULT_LOCALE;
const effectiveLocale: LocaleCode = storedLocale && isLocaleCode(storedLocale) ? storedLocale : DEFAULT_LOCALE;
const applyMeta = useCallback((meta: GalleryMetaResponse) => {
if (typeof window !== 'undefined' && token) {

View File

@@ -39,7 +39,7 @@ export default function SharedPhotoPage() {
if (!active) return;
setState({ loading: false, error: null, data });
})
.catch((error: any) => {
.catch((error: unknown) => {
if (!active) return;
setState({ loading: false, error: error?.message ?? t('share.error', 'Moment konnte nicht geladen werden.'), data: null });
});

View File

@@ -8,7 +8,6 @@ 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';
@@ -171,7 +170,7 @@ export default function TaskPickerPage() {
const { branding } = useEventBranding();
const { t, locale } = useTranslation();
const { completedCount, isCompleted } = useGuestTaskProgress(eventKey);
const { isCompleted } = useGuestTaskProgress(eventKey);
const [tasks, setTasks] = React.useState<Task[]>([]);
const [currentTask, setCurrentTask] = React.useState<Task | null>(null);