Hub und Bot-Produktseite lassen sich jetzt auf Englisch umstellen. Die Wahl haengt am Browser (Deutsch nur, wenn der Browser es will) und bleibt im localStorage stehen. - src/i18n.jsx: Context mit t(), tOr(), Datums- und Zahlenformat der Sprache. Keine Bibliothek, die Woerterbuecher werden mitgebaut — kein Nachlade-Blitzer. - Platzhalter im %s-Stil, dieselbe Schreibweise wie in den Bot-Texten. - Umschalter in der Navigation, zusaetzlich im Schubladen-Menue: .auth ist auf schmalen Schirmen ausgeblendet, sonst waere der Wechsel dort unerreichbar. - Datums- und Zahlenformat folgen mit (31.07.2026 / 31/07/2026, 45.231 / 45,231). - Die Funktionsliste kommt weiter deutsch vom Server; englische Modulnamen liegen unter feat.<id> im Woerterbuch. Fehlt einer, bleibt der Servertext stehen — ein neues Modul verschwindet nie von der Seite. Das Config-Panel und die Discord-Texte bleiben deutsch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97 lines
4.0 KiB
React
97 lines
4.0 KiB
React
import { useEffect, useState } from 'react';
|
|
import { apiGet } from '../api.js';
|
|
import { IconChevronLeft, IconChevronRight, IconGallery } from '../icons.jsx';
|
|
import EmptyState from '../components/EmptyState.jsx';
|
|
import Lightbox from '../components/Lightbox.jsx';
|
|
import { SkeletonGrid } from '../components/Skeleton.jsx';
|
|
import { useT, useFmt } from '../i18n.jsx';
|
|
|
|
const DATUM = { day: '2-digit', month: '2-digit', year: 'numeric' };
|
|
|
|
export default function Galerie() {
|
|
const { t } = useT();
|
|
const { datum } = useFmt();
|
|
const [data, setData] = useState(null);
|
|
const [page, setPage] = useState(1);
|
|
const [error, setError] = useState(false);
|
|
const [lightbox, setLightbox] = useState(null); // Start-Index oder null
|
|
|
|
useEffect(() => {
|
|
setError(false);
|
|
apiGet(`/api/gallery?page=${page}`).then(setData).catch(() => setError(true));
|
|
window.scrollTo(0, 0);
|
|
}, [page]);
|
|
|
|
const pages = data ? Math.ceil(data.total / data.pageSize) : 0;
|
|
const shots = (data?.items ?? []).flatMap((item) =>
|
|
item.images.map((src) => ({ src, author: item.author_name, date: item.posted_at, key: src }))
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<section className="page-header">
|
|
<div className="page-bg-text">GALERIE</div>
|
|
<div className="page-header-grid" aria-hidden="true" />
|
|
<div className="page-header-content">
|
|
<div className="page-tag">{t('gallery.tag')}</div>
|
|
<h1 className="page-title">{t('nav.gallery')}</h1>
|
|
<p className="page-subtitle">{t('gallery.subtitle')}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="content">
|
|
{error && <p className="notice">{t('gallery.error')}</p>}
|
|
{!error && !data && <SkeletonGrid n={9} />}
|
|
{data?.total === 0 && (
|
|
<EmptyState
|
|
Icon={IconGallery}
|
|
title={t('gallery.emptyTitle')}
|
|
text={t('gallery.emptyText')}
|
|
actions={[{ to: '/devlogs', label: t('common.readDevlogs') }]}
|
|
/>
|
|
)}
|
|
|
|
<div className="gallery-grid">
|
|
{shots.map((s, i) => (
|
|
<a
|
|
className="shot" href={s.src} key={s.key}
|
|
onClick={(e) => { e.preventDefault(); setLightbox(i); }}
|
|
>
|
|
<img src={s.src} alt={t('gallery.shotBy', s.author)} loading="lazy" />
|
|
<span className="shot-meta">
|
|
<span className="shot-author">{s.author}</span>
|
|
<span className="shot-date">{datum(s.date, DATUM)}</span>
|
|
</span>
|
|
</a>
|
|
))}
|
|
</div>
|
|
|
|
{lightbox !== null && (
|
|
<Lightbox
|
|
images={shots.map((s) => ({
|
|
src: s.src,
|
|
caption: `${s.author} · ${datum(s.date, DATUM)}`,
|
|
}))}
|
|
start={lightbox}
|
|
onClose={() => setLightbox(null)}
|
|
/>
|
|
)}
|
|
|
|
{pages > 1 && (
|
|
<div className="pager">
|
|
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
|
|
<IconChevronLeft size={14} /> {t('common.newer')}
|
|
</button>
|
|
<span className="pager-info">
|
|
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
|
|
</span>
|
|
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
|
|
{t('common.older')} <IconChevronRight size={14} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</>
|
|
);
|
|
}
|