diff --git a/css/import.css b/css/import.css new file mode 100644 index 0000000..d2be4fb --- /dev/null +++ b/css/import.css @@ -0,0 +1,267 @@ +/* KI-JSON-Import + Backup-Export/Import (Phase 7), Modal mit zwei Tabs. + Struktur wird von js/ui/import.js in #import-modal eingehängt. Grundgerüst + analog zu css/promptGenerator.css (Vollbild auf Mobile, zentriertes + breites Panel ab 1024px). Formularbausteine (.form-field__label, + .form-checkbox, .place-form__section-label) nutzen bewusst dieselben + Klassen wie das Ausflugsziel-Formular statt sie zu duplizieren. */ + +.import-modal { + position: fixed; + inset: 0; + z-index: 2400; +} + +.import-modal[hidden] { + display: none; +} + +.import-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); +} + +.import-modal__panel { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: var(--color-surface); + overflow-y: auto; + padding: var(--space-lg); + padding-top: calc(var(--space-lg) + 36px); + gap: var(--space-md); +} + +.import-modal__close { + position: absolute; + top: var(--space-sm); + right: var(--space-sm); + z-index: 1; + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + background: rgba(0, 0, 0, 0.55); + color: #fff; + font-size: 1rem; + line-height: 1; + cursor: pointer; +} + +.import-modal__title { + font-size: 1.3rem; + font-weight: 700; +} + +.import-modal__tabs { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-sm); +} + +.import-modal__tab { + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + background: var(--color-background); + color: var(--color-text); + font: inherit; + font-weight: 600; + cursor: pointer; +} + +.import-modal__tab--active { + background: var(--color-primary); + border-color: var(--color-primary); + color: #fff; +} + +.import-panel { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.import-panel[hidden] { + display: none; +} + +.import-panel__intro { + color: var(--color-text-muted); + line-height: 1.4; +} + +.import-panel__section { + display: flex; + flex-direction: column; + gap: var(--space-sm); + border-top: 1px solid var(--color-border); + padding-top: var(--space-md); +} + +.import-panel__section:first-of-type { + border-top: none; + padding-top: 0; +} + +.import-input { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.import-input__textarea { + width: 100%; + min-height: 160px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: var(--space-sm); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.8rem; + line-height: 1.4; + background: var(--color-background); + color: var(--color-text); + resize: vertical; +} + +.import-input__file { + font-size: 0.85rem; +} + +.import-error { + color: var(--color-danger); + font-size: 0.85rem; +} + +.import-error[hidden] { + display: none; +} + +.import-status { + font-size: 0.85rem; + color: var(--color-text-muted); +} + +.import-preview { + display: flex; + flex-direction: column; + gap: var(--space-sm); + border-top: 1px solid var(--color-border); + padding-top: var(--space-md); +} + +.import-preview[hidden] { + display: none; +} + +.import-preview__summary { + font-weight: 700; +} + +.import-preview__global-warnings { + margin: 0; + padding-left: 1.25em; + color: var(--color-danger); + font-size: 0.85rem; +} + +.import-preview__global-warnings[hidden] { + display: none; +} + +.import-preview__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-sm); + max-height: 320px; + overflow-y: auto; +} + +.import-preview__item { + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: var(--space-sm); + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.import-preview__item--rejected { + border-color: var(--color-danger); + background: #fdecea; +} + +.import-preview__item-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-sm); +} + +.import-preview__name { + font-weight: 700; +} + +.import-preview__category { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-muted); +} + +.import-preview__badge { + font-size: 0.75rem; + font-weight: 700; + color: var(--color-danger); + text-transform: uppercase; +} + +.import-preview__duplicate-note { + font-size: 0.8rem; + color: var(--color-text-muted); + margin: 0; +} + +.import-preview__warnings, +.import-preview__reasons { + margin: 0; + padding-left: 1.25em; + font-size: 0.8rem; +} + +.import-preview__warnings { + color: var(--color-text-muted); +} + +.import-preview__reasons { + color: var(--color-danger); +} + +.import-preview__skip-duplicates { + font-size: 0.85rem; +} + +.import-preview__actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); +} + +@media (min-width: 1024px) { + .import-modal__panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(840px, 95vw); + height: min(90vh, 900px); + border-radius: var(--radius-md); + box-shadow: var(--shadow-card); + } +} diff --git a/docs/plan.md b/docs/plan.md index 9dcf6b0..d55dc0d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -161,7 +161,38 @@ Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne | 4 | Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste | ✅ umgesetzt | | 5 | Route/Navigation-Integration | ✅ bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht) | | 6 | KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) | ✅ umgesetzt | -| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur | offen | +| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import | ✅ umgesetzt | + +Damit sind alle in `anforderung.md` geforderten Kernfunktionen umgesetzt. +Offen bleibt optionale Politur (Empty-States, A11y-Feinschliff, +Adressgeocoding) — kein Muss laut Anforderung, bei Bedarf als eigene +Folgephase. + +## KI-JSON-Import + Backup (Phase 7) + +- `js/import/importValidator.js`: reine Funktion `parseAndValidate(jsonText)` + bewertet jeden Ausflugsziel-Eintrag im importierten KI-JSON EINZELN + (kein Alles-oder-nichts). Pflicht für Übernahme: `name`, `category`, + gültige Koordinaten — sonst Ablehnung mit Begründung. Weiche Probleme + (unbekannte Kategorie, unbekannter Kosten-/Umgebungstyp, ungültige + Bewertung, unsichere `website`/`image`-URL) führen zu einer Warnung, + nicht zur Ablehnung; `sanitizeEntry()` setzt diese Felder vor der + Übernahme sicherheitshalber zurück (z.B. `javascript:`-URLs → leerer + String), BEVOR `normalizePlace()` sie verarbeitet, da dieses selbst + keine Sicherheitsprüfung vornimmt. `findDuplicate()` erkennt + wahrscheinliche Duplikate (Name + Koordinaten < 0.3 km) gegen den + bestehenden Datenbestand. +- `js/ui/import.js`: Modal mit zwei Tabs. Tab 1 "KI-Ausflugsziele + importieren": Text einfügen ODER Datei hochladen (`FileReader`) → + Prüfen → Vorschau (gültig/Warnung/abgelehnt, Duplikat-Hinweise, globaler + "Duplikate überspringen"-Toggle) → Bestätigen. Tab 2 "Backup + sichern/wiederherstellen" (`js/import/backup.js`): Export als + JSON-Datei (`downloadTextFile`), Wiederherstellen mit Wahl + "Ersetzen" (mit `confirm()`-Sicherheitsabfrage) oder "Ergänzen" + (Upsert nach `id`). +- Einstiegspunkt: Toolbar-Button "📥 KI-Ausflugsziele importieren". +- Rendering der Vorschau ausschließlich über `el()`/`textContent`, da es + sich um echte, ungeprüfte Fremddaten handelt. ## KI-Prompt-Generator (Phase 6) diff --git a/index.html b/index.html index 4c6ecd4..d209887 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ +
@@ -53,6 +54,9 @@ +Karte wird geladen…
@@ -73,6 +77,7 @@ + @@ -110,6 +115,14 @@ + + + + +