- 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>
138 lines
5.9 KiB
React
138 lines
5.9 KiB
React
import { useEffect, useState } from 'react';
|
|
import { loadSet, saveSet } from '../../storage.js';
|
|
import { BY_CODE, CONTINENTS, continentOf, countryName } from './laender.js';
|
|
|
|
// Besuchte Orte: Kontinent → Land (ausklappbar) → Städte; auf-/zugeklappt wird pro Browser gemerkt
|
|
|
|
const OPEN_KEY = 'p42.reisenOffen';
|
|
const CLOSED_KEY = 'p42.reisenKontinenteZu';
|
|
|
|
const loadOpen = () => loadSet(OPEN_KEY);
|
|
|
|
export default function VisitedList({ visited, cities, manual, selected, onSelect }) {
|
|
const [open, setOpen] = useState(loadOpen);
|
|
|
|
const save = (next) => {
|
|
setOpen(next);
|
|
saveSet(OPEN_KEY, next);
|
|
};
|
|
const toggle = (code) => {
|
|
const next = new Set(open);
|
|
if (next.has(code)) next.delete(code); else next.add(code);
|
|
save(next);
|
|
};
|
|
|
|
// Kontinente: standardmäßig offen, zugeklappte werden gemerkt
|
|
const [closed, setClosed] = useState(() => loadSet(CLOSED_KEY));
|
|
const saveClosed = (next) => {
|
|
setClosed(next);
|
|
saveSet(CLOSED_KEY, next);
|
|
};
|
|
const toggleContinent = (name) => {
|
|
const next = new Set(closed);
|
|
if (next.has(name)) next.delete(name); else next.add(name);
|
|
saveClosed(next);
|
|
};
|
|
|
|
// Wird eine Stadt (z. B. auf der Karte) gewählt, ihr Land aufklappen
|
|
const selectedCountry = selected?.type === 'city' ? cities.find((c) => c.id === selected.key)?.country : null;
|
|
useEffect(() => {
|
|
if (selectedCountry && !open.has(selectedCountry)) save(new Set(open).add(selectedCountry));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [selectedCountry]);
|
|
|
|
// Ein gewähltes Land oder eine gewählte Stadt macht ihren Kontinent sichtbar
|
|
const selectedCode = selected?.type === 'country' ? selected.key : selectedCountry;
|
|
const selectedContinent = selectedCode ? continentOf(selectedCode) : null;
|
|
useEffect(() => {
|
|
if (selectedContinent && closed.has(selectedContinent)) {
|
|
const next = new Set(closed);
|
|
next.delete(selectedContinent);
|
|
saveClosed(next);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [selectedContinent, selected]);
|
|
|
|
if (!visited.length) {
|
|
return <p className="hint travel-empty">Noch keine Orte. Füge eine Stadt hinzu oder klick ein Land auf der Karte an und markiere es.</p>;
|
|
}
|
|
|
|
const groups = [...CONTINENTS, 'Sonstige']
|
|
.map((name) => ({ name, countries: visited.filter((code) => continentOf(code) === name) }))
|
|
.filter((g) => g.countries.length);
|
|
|
|
return (
|
|
<div className="visited-list">
|
|
{groups.map((group) => {
|
|
const cityCount = cities.filter((c) => continentOf(c.country) === group.name).length;
|
|
const contOpen = !closed.has(group.name);
|
|
const id = `cont-${group.name}`;
|
|
return (
|
|
<section key={group.name} className={`visited-continent${contOpen ? '' : ' closed'}`} aria-labelledby={id}>
|
|
<h2 className="section-title">
|
|
<button
|
|
type="button"
|
|
id={id}
|
|
className="continent-toggle"
|
|
aria-expanded={contOpen}
|
|
aria-controls={`${id}-list`}
|
|
onClick={() => toggleContinent(group.name)}
|
|
>
|
|
<span className="continent-arrow" aria-hidden="true">▸</span>
|
|
{group.name}
|
|
<small className="visited-count">
|
|
{group.countries.length} {group.countries.length === 1 ? 'Land' : 'Länder'} · {cityCount} {cityCount === 1 ? 'Stadt' : 'Städte'}
|
|
</small>
|
|
</button>
|
|
</h2>
|
|
{contOpen && (
|
|
<ul className="visited-countries" id={`${id}-list`}>
|
|
{group.countries.map((code) => {
|
|
const list = cities.filter((c) => c.country === code);
|
|
const isOpen = open.has(code) && list.length > 0;
|
|
const active = selected?.type === 'country' && selected.key === code;
|
|
return (
|
|
<li key={code} className={`visited-country${isOpen ? ' open' : ''}${active ? ' active' : ''}`}>
|
|
<div className="visited-row">
|
|
<button
|
|
type="button"
|
|
className="visited-toggle"
|
|
aria-expanded={isOpen}
|
|
aria-controls={`cities-${code}`}
|
|
disabled={!list.length}
|
|
onClick={() => toggle(code)}
|
|
aria-label={list.length ? `Städte in ${countryName(code)} ${isOpen ? 'zuklappen' : 'aufklappen'}` : `Keine Städte in ${countryName(code)}`}
|
|
>
|
|
<span aria-hidden="true">{list.length ? '▸' : '·'}</span>
|
|
</button>
|
|
<button type="button" className="visited-name" onClick={() => onSelect({ type: 'country', key: code })}>
|
|
<span className="visited-flag" aria-hidden="true">{BY_CODE.get(code)?.flag}</span>
|
|
{countryName(code)}
|
|
</button>
|
|
<span className="visited-meta">
|
|
{list.length ? `${list.length} ${list.length === 1 ? 'Stadt' : 'Städte'}` : manual.includes(code) ? 'markiert' : ''}
|
|
</span>
|
|
</div>
|
|
{isOpen && (
|
|
<ul className="chip-list visited-cities" id={`cities-${code}`}>
|
|
{list.map((c) => (
|
|
<li key={c.id}>
|
|
<button type="button" className={`chip${selected?.type === 'city' && selected.key === c.id ? ' active' : ''}`} onClick={() => onSelect({ type: 'city', key: c.id })}>
|
|
{c.name}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|