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:
guyg1474andClaude Opus 5.5 committed 2026-10-15 18:14:32 +02:00
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>
);
}