behoben: ohne aufgabe kann die kamera nicht gestartet werden (offensichtlich kein fehler mit browserzugriff auf kamera!)

platz zu begrenzt im aufnahmemodus - vollbildmodus möglich? Menü und Kopfleiste ausblenden?
	Bild aus eigener galerie auswählen - Upload schlägt fehl (zu groß? evtl fehlende Rechte - aber browser hat rechte auf bilder und dateien!)
	hochgeladene bilder tauchen in der galerie nicht beim filter "Meine Bilder" auf - fotos werden auch nicht gezählt in den stats und achievements zeigen keinen fortschriftt.
	geteilte fotos: ruft man den Link auf, bekommt man die meldung "Link abgelaufen"
	der im startbildschirm gewählte name mit Umlauten (Sören) ist nach erneutem aufruf der pwa ohne umlaut (Sren).
Aufgabenseite verbessert (Zwischenstand)
This commit is contained in:
Codex Agent
2025-12-04 11:58:07 +01:00
parent 899e742c38
commit c73a3163c0
15 changed files with 776 additions and 610 deletions

View File

@@ -1,11 +1,8 @@
// @ts-nocheck
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useParams } from 'react-router-dom';
import { fetchPhotoShare } from '../services/photosApi';
import { useTranslation } from '../i18n/useTranslation';
import { useToast } from '../components/ToastHost';
import { Loader2 } from 'lucide-react';
import { Loader2, AlertCircle } from 'lucide-react';
interface ShareResponse {
slug: string;
@@ -14,16 +11,29 @@ interface ShareResponse {
id: number;
title?: string;
likes_count?: number;
emotion?: { name?: string; emoji?: string } | null;
emotion?: { name?: string; emoji?: string | null } | null;
created_at?: string | null;
image_urls: { full: string; thumbnail: string };
};
event?: { id: number; name?: string | null } | null;
}
type ShareProps = { slug: string | undefined };
export function SharedPhotoStandalone() {
const slug = React.useMemo(() => {
const parts = window.location.pathname.split('/').filter(Boolean);
return parts.length >= 2 ? parts[1] : undefined;
}, []);
return <SharedPhotoView slug={slug} />;
}
export default function SharedPhotoPage() {
const { slug } = useParams<{ slug: string }>();
const { t } = useTranslation();
const toast = useToast();
return <SharedPhotoView slug={slug} />;
}
function SharedPhotoView({ slug }: ShareProps) {
const [state, setState] = React.useState<{
loading: boolean;
error: string | null;
@@ -36,34 +46,22 @@ export default function SharedPhotoPage() {
setState({ loading: true, error: null, data: null });
fetchPhotoShare(slug)
.then((data) => {
if (!active) return;
setState({ loading: false, error: null, data });
})
.then((data) => { if (active) setState({ loading: false, error: null, data }); })
.catch((error: unknown) => {
if (!active) return;
setState({ loading: false, error: error?.message ?? t('share.error', 'Moment konnte nicht geladen werden.'), data: null });
setState({ loading: false, error: 'Dieses Foto ist nicht mehr verfügbar.', data: null });
});
return () => {
active = false;
};
}, [slug, t]);
const handleCopy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(window.location.href);
toast.push({ text: t('share.copySuccess', 'Link kopiert!') });
} catch {
toast.push({ text: t('share.copyError', 'Link konnte nicht kopiert werden.'), type: 'error' });
}
}, [toast, t]);
}, [slug]);
if (state.loading) {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 bg-gradient-to-br from-pink-50 to-white px-4 text-center">
<Loader2 className="h-8 w-8 animate-spin text-pink-500" aria-hidden />
<p className="text-sm text-muted-foreground">{t('share.loading', 'Moment wird geladen...')}</p>
<p className="text-sm text-muted-foreground">Moment wird geladen </p>
</div>
);
}
@@ -71,29 +69,30 @@ export default function SharedPhotoPage() {
if (state.error || !state.data) {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-gradient-to-br from-pink-50 to-white px-6 text-center">
<p className="text-lg font-semibold text-foreground">{t('share.expiredTitle', 'Link abgelaufen')}</p>
<p className="text-sm text-muted-foreground max-w-md">{state.error ?? t('share.expiredDescription', 'Dieses Foto ist nicht mehr verfügbar.')}</p>
<Button asChild>
<Link to="/event">{t('share.openEvent', 'Event öffnen')}</Link>
</Button>
<div className="flex items-center gap-2 text-rose-600">
<AlertCircle className="h-5 w-5" />
<p className="text-lg font-semibold text-foreground">Link abgelaufen</p>
</div>
<p className="text-sm text-muted-foreground max-w-md">
{state.error ?? 'Dieses Foto ist nicht mehr verfügbar.'}
</p>
</div>
);
}
const { data } = state;
const chips = buildChips(data);
return (
<div className="min-h-screen bg-gradient-to-b from-white via-pink-50 to-white px-4 py-8">
<div className="mx-auto flex max-w-2xl flex-col gap-5">
<div className="rounded-3xl border border-white/60 bg-white/80 p-6 text-center shadow">
<p className="text-xs uppercase tracking-wide text-muted-foreground">{t('share.title', 'Geteiltes Foto')}</p>
<h1 className="mt-2 text-2xl font-semibold text-foreground">{data.event?.name ?? t('share.defaultEvent', 'Ein besonderer Moment')}</h1>
{data.photo.title && (
<p className="mt-1 text-sm text-muted-foreground">{data.photo.title}</p>
)}
<div className="min-h-screen bg-gradient-to-b from-white via-pink-50 to-white px-4 py-10">
<div className="mx-auto flex w-full max-w-xl flex-col gap-6">
<div className="rounded-3xl border border-white/60 bg-white/90 p-5 text-center shadow-sm">
<p className="text-[11px] uppercase tracking-[0.35em] text-muted-foreground">Geteiltes Foto</p>
<h1 className="mt-2 text-2xl font-semibold text-foreground">{data.event?.name ?? 'Ein besonderer Moment'}</h1>
{data.photo.title && <p className="mt-1 text-sm text-muted-foreground">{data.photo.title}</p>}
</div>
<div className="overflow-hidden rounded-[32px] border border-white/60 bg-black">
<div className="overflow-hidden rounded-[28px] border border-white/60 bg-black shadow-lg">
<img
src={data.photo.image_urls.full}
alt={data.photo.title ?? 'Foto'}
@@ -102,21 +101,42 @@ export default function SharedPhotoPage() {
/>
</div>
{data.photo.emotion && (
<p className="text-center text-sm text-muted-foreground">
{data.photo.emotion.emoji} {data.photo.emotion.name}
</p>
{chips.length > 0 && (
<div className="flex flex-wrap justify-center gap-2">
{chips.map((chip) => (
<span
key={chip.id}
className="inline-flex items-center gap-1 rounded-full border border-slate-200/80 bg-white/90 px-3 py-1 text-xs font-semibold text-slate-700 shadow-sm"
>
{chip.icon ? <span aria-hidden className="text-sm">{chip.icon}</span> : null}
<span className="text-[11px] uppercase tracking-wide opacity-70">{chip.label}</span>
<span className="text-[12px]">{chip.value}</span>
</span>
))}
</div>
)}
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
<Button variant="secondary" onClick={handleCopy}>
{t('share.copyLink', 'Link kopieren')}
</Button>
<Button asChild>
<Link to="/event">{t('share.openEvent', 'Event öffnen')}</Link>
</Button>
</div>
</div>
</div>
);
}
function buildChips(data: ShareResponse): { id: string; label: string; value: string; icon?: string }[] {
const list: { id: string; label: string; value: string; icon?: string }[] = [];
if (data.photo.emotion?.name) {
list.push({ id: 'emotion', label: 'Emotion', value: data.photo.emotion.name, icon: data.photo.emotion.emoji ?? '★' });
}
if (data.photo.title) {
list.push({ id: 'task', label: 'Aufgabe', value: data.photo.title });
}
if (data.photo.created_at) {
const date = formatDate(data.photo.created_at);
list.push({ id: 'date', label: 'Aufgenommen', value: date });
}
return list;
}
function formatDate(value: string): string {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return '';
return parsed.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' });
}