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>
This commit is contained in:
1 parent
9835f5e69e
commit
bbf740603e
16 files changed
+695
-32
No files matched your search
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user