- 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>
57 lines
2.5 KiB
React
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>
|
|
);
|
|
}
|