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)
143 lines
5.1 KiB
TypeScript
143 lines
5.1 KiB
TypeScript
// @ts-nocheck
|
|
import React from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { fetchPhotoShare } from '../services/photosApi';
|
|
import { Loader2, AlertCircle } from 'lucide-react';
|
|
|
|
interface ShareResponse {
|
|
slug: string;
|
|
expires_at?: string;
|
|
photo: {
|
|
id: number;
|
|
title?: string;
|
|
likes_count?: number;
|
|
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 }>();
|
|
return <SharedPhotoView slug={slug} />;
|
|
}
|
|
|
|
function SharedPhotoView({ slug }: ShareProps) {
|
|
const [state, setState] = React.useState<{
|
|
loading: boolean;
|
|
error: string | null;
|
|
data: ShareResponse | null;
|
|
}>({ loading: true, error: null, data: null });
|
|
|
|
React.useEffect(() => {
|
|
let active = true;
|
|
if (!slug) return;
|
|
|
|
setState({ loading: true, error: null, data: null });
|
|
fetchPhotoShare(slug)
|
|
.then((data) => { if (active) setState({ loading: false, error: null, data }); })
|
|
.catch((error: unknown) => {
|
|
if (!active) return;
|
|
setState({ loading: false, error: 'Dieses Foto ist nicht mehr verfügbar.', data: null });
|
|
});
|
|
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [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">Moment wird geladen …</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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">
|
|
<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-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-[28px] border border-white/60 bg-black shadow-lg">
|
|
<img
|
|
src={data.photo.image_urls.full}
|
|
alt={data.photo.title ?? 'Foto'}
|
|
className="h-full w-full object-cover"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
|
|
{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>
|
|
</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' });
|
|
}
|