Files
p42.f-w/frontend/src/components/reisen/Timeline.jsx
T
guyg1474andClaude Opus 5.5 bbf740603e Reisen: Wunschliste, Besuche mit Fotos, Zeitleiste und Statistik
- Wunschliste (Spalte travel_cities.wish): eigene Punktform auf der Karte,
  zählt nicht als besucht, für andere Konten unsichtbar; "War ich da ✓"
- Besuche je Stadt (Tabellen travel_visits, travel_visit_photos): Zeitraum,
  Begleitung, Notizen, Fotos (Upload ohne Metadaten); nur für das eigene
  Konto; ein Besuch macht aus einem Wunsch einen Besuch
- Fotos von Besuchen zählen beim Aufräumen verwaister Bilder als benutzt;
  beim Entfernen eines Fotos, Besuchs oder einer Stadt werden die Dateien
  gelöscht
- Zeitleiste "Wann war ich wo?" nach Jahren, Fotos mit Lightbox
- Statistik: Länder (Welt, je Kontinent), Landfläche, äußerste Orte,
  Kilometer entlang der Besuche
- Ansichten unter der Karte als Tabs: Besuchte Orte, Zeitleiste,
  Statistik, Wunschliste
- Migration Schritt 4, Tests für Wunschliste, Besuche, Fotos und Aufräumen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:25:07 +02:00

57 lines
2.5 KiB
React

import { imgUrl } from '../../api.js';
import { BY_CODE, countryName, fmtRange } from './laender.js';
// Zeitleiste "Wann war ich wo?": alle Besuche, neueste zuerst, nach Jahren gruppiert
export default function Timeline({ visits, cities, onSelectCity, onEdit, onPhoto }) {
if (!visits.length) {
return <p className="hint travel-empty">Noch keine Besuche. Klick auf eine Stadt und dann auf „+ Besuch eintragen“.</p>;
}
const byId = new Map(cities.map((c) => [c.id, c]));
const years = [];
for (const v of visits) {
const year = v.dateFrom.slice(0, 4);
if (years.at(-1)?.year !== year) years.push({ year, visits: [] });
years.at(-1).visits.push(v);
}
return (
<div className="timeline-travel">
{years.map((y) => (
<section key={y.year} aria-labelledby={`jahr-${y.year}`}>
<h2 className="section-title" id={`jahr-${y.year}`}>{y.year}</h2>
<ol>
{y.visits.map((v) => {
const city = byId.get(v.cityId);
if (!city) return null;
return (
<li key={v.id} className="timeline-entry">
<span className="timeline-date">{fmtRange(v.dateFrom, v.dateTo)}</span>
<div className="timeline-body">
<button type="button" className="timeline-city" onClick={() => onSelectCity(city)}>
{BY_CODE.get(city.country)?.flag} {city.name}
<small> {countryName(city.country)}</small>
</button>
{v.companions && <span className="hint">mit {v.companions}</span>}
{v.notes && <p className="timeline-notes">{v.notes}</p>}
{v.photos.length > 0 && (
<ul className="visit-photos small">
{v.photos.map((p) => (
<li key={p}>
<button type="button" onClick={() => onPhoto(imgUrl(p))} aria-label="Foto vergrößern">
<img src={imgUrl(p)} alt="" loading="lazy" />
</button>
</li>
))}
</ul>
)}
</div>
<button className="link-btn" type="button" onClick={() => onEdit(city, v)} aria-label={`Besuch in ${city.name} bearbeiten`}>✎</button>
</li>
);
})}
</ol>
</section>
))}
</div>
);
}