Frontend: gemeinsamer Hook useLiveData, Komponenten nach Bereichen, große Seiten aufgeteilt
- useLiveData: Laden, Nachladen (sichtbarer Tab) und Speichern mit Fehlerbehandlung an einer Stelle; genutzt von Vorrat, Reisen, Kanban-Übersicht und Kanban-Board (Monitoring/Benutzer arbeiten anders) - components/<bereich>/: haushalt, reisen, kanban, monitoring, projekte, start, design (per git mv, Historie bleibt) - Reisen.jsx 783 → 227 Zeilen (CitySearch, VisitedList, InfoCard, ExportDialog, laender.js), Haushalt.jsx 634 → 193 (ListPanel, ListSwitcher, ListDialog, ProductsDialog), KanbanBoard.jsx 452 → 277 (board.js, TaskCard, TaskDialog) - storage.js für Einstellungen pro Browser; Städteliste in Reisen memoisiert - Verhalten unverändert (Browser-Durchlauf aller Seiten und Abläufe) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4aba48ce51
commit
fa58708e2d
41 files changed
+1264
-1241
No files matched your search
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { loadSet, saveSet } from '../../storage.js';
|
||||
import { BY_CODE, countryName } from './laender.js';
|
||||
|
||||
// Besuchte Orte: Kontinent → Land (ausklappbar) → Städte; auf-/zugeklappt wird pro Browser gemerkt
|
||||
|
||||
const CONTINENTS = ['Europa', 'Asien', 'Afrika', 'Nordamerika', 'Südamerika', 'Ozeanien', 'Antarktis'];
|
||||
// Gebiete der Karte ohne Eintrag in den Länderdaten
|
||||
const SPECIAL_CONTINENT = { 'X-Somaliland': 'Afrika', 'X-Nordzypern': 'Europa', 'X-Indischer Ozean': 'Ozeanien', 'X-Siachen-Gletscher': 'Asien' };
|
||||
const continentOf = (code) => BY_CODE.get(code)?.continent || SPECIAL_CONTINENT[code] || 'Sonstige';
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user