f622dc0619
⭐ Sterne-Bewertungs-System (1-5 pro Person, Till + Lea unabhängig) 🏆 Dynamische Ranking-Liste mit Top-3-Badges (Gold/Silber/Bronze) 💬 Kommentare unter jeder Karte (aufklappbar, eigene löschbar) 👤 User-Profil-Switch (Till ↔ Lea) – Header-Pill mit Modal 💜 Eigene Ideen sind jetzt vollwertige Karten (im Ideen-Tab gemischt) ✏️ Eigene Ideen bearbeitbar + löschbar (eigener Tab) 📊 Sortierung: Ranking / Status / Preis / Name / Eigene zuerst 📈 Live-Stats: Ideen, Bewertet, Kommentare, Tage bis Reise 🎨 Bottom-Navigation mit 5 Tabs + Badge für eigene Ideen 📋 JSON-Export aller Daten für Codex 🔧 Daten-driven: destinations.json + dynamisches Rendering 📱 Mobile-optimiert mit Safe-Area-Insets Datenstruktur: - localStorage.f24_ratings, f24_comments, f24_own, f24_user - 18 built-in Destinations aus data/destinations.json - Gleiche Render-Pipeline für built-in und own
1150 lines
73 KiB
HTML
1150 lines
73 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1">
|
||
<title>Fredi24 – Reise-Planer</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
|
||
:root{
|
||
--bg:#F7F3ED;--bg2:#EDE8DF;--card:#FFFFFF;--border:#E4DDD4;
|
||
--coral:#E55A35;--coral-dark:#C4482A;--coral-light:#FCEAE4;
|
||
--gold:#D4A020;--gold-light:#FDF4DC;
|
||
--teal:#1F6B5E;--teal-light:#D4EDE8;
|
||
--blue:#3B7EC8;--blue-light:#DAEAF8;
|
||
--purple:#7B4DBA;--purple-light:#EDE0FA;
|
||
--text:#18181B;--muted:#78716C;--subtle:#A8A29E;
|
||
--navy:#0D1B2A;
|
||
--r:12px;--r-sm:8px;--r-lg:16px;
|
||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||
--safe-top: env(safe-area-inset-top, 0px);
|
||
}
|
||
html{-webkit-text-size-adjust:100%;}
|
||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:14px;min-height:100vh;padding-bottom:calc(72px + var(--safe-bottom));}
|
||
|
||
/* GATE */
|
||
#gate{position:fixed;inset:0;background:var(--navy);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;}
|
||
.gate-box{background:#fff;border-radius:20px;padding:36px 28px;text-align:center;max-width:320px;width:100%;}
|
||
.gate-paw{font-size:52px;margin-bottom:8px;}
|
||
.gate-brand{font-family:'Bricolage Grotesque',sans-serif;font-size:28px;font-weight:800;color:var(--coral);letter-spacing:-0.04em;}
|
||
.gate-sub{font-size:13px;color:var(--muted);margin:4px 0 24px;}
|
||
.gate-input{width:100%;padding:12px 16px;border:2px solid var(--border);border-radius:var(--r);font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border .2s;margin-bottom:10px;background:var(--bg);}
|
||
.gate-input:focus{border-color:var(--coral);}
|
||
.gate-btn{width:100%;padding:13px;background:var(--coral);color:#fff;border:none;border-radius:var(--r);font-size:14px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;transition:background .2s;letter-spacing:.01em;}
|
||
.gate-btn:hover{background:var(--coral-dark);}
|
||
.gate-err{color:var(--coral);font-size:12px;margin-top:8px;display:none;font-weight:500;}
|
||
|
||
/* PROFILE PICKER */
|
||
.profile-pick{display:flex;gap:8px;margin-bottom:12px;justify-content:center;}
|
||
.profile-opt{flex:1;padding:8px 6px;border:2px solid var(--border);border-radius:var(--r);background:var(--card);cursor:pointer;text-align:center;font-size:12px;font-weight:600;color:var(--muted);transition:all .2s;}
|
||
.profile-opt.active{border-color:var(--coral);background:var(--coral-light);color:var(--coral);}
|
||
.profile-opt .emoji{font-size:22px;display:block;margin-bottom:2px;}
|
||
|
||
/* HEADER */
|
||
.header{background:var(--navy);color:#fff;padding:calc(12px + var(--safe-top)) 16px 12px;position:sticky;top:0;z-index:100;}
|
||
.header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
||
.brand{display:flex;align-items:center;gap:6px;}
|
||
.brand-logo{font-size:22px;}
|
||
.brand-name{font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:800;color:#fff;letter-spacing:-0.04em;}
|
||
.brand-name span{color:var(--coral);}
|
||
.brand-sub{font-size:10px;color:rgba(255,255,255,0.5);margin-top:-2px;}
|
||
.user-pill{font-size:11px;color:rgba(255,255,255,0.85);background:var(--coral);padding:4px 10px;border-radius:20px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:4px;}
|
||
.user-pill:hover{background:var(--coral-dark);}
|
||
|
||
.header-pills{display:flex;gap:5px;flex-wrap:wrap;margin-top:9px;}
|
||
.hp{background:rgba(255,255,255,0.1);border-radius:20px;padding:3px 9px;font-size:11px;font-weight:500;color:rgba(255,255,255,0.75);white-space:nowrap;}
|
||
.hp.budget{background:rgba(212,160,32,0.3);color:#FFD966;}
|
||
|
||
.budget-indicator{margin-top:8px;display:flex;align-items:center;gap:8px;}
|
||
.budget-track{flex:1;height:3px;background:rgba(255,255,255,0.15);border-radius:2px;overflow:hidden;}
|
||
.budget-fill{height:100%;background:linear-gradient(90deg,#2D7A6A,#D4A020);border-radius:2px;}
|
||
.budget-label{font-size:10px;color:rgba(255,255,255,0.5);}
|
||
|
||
/* BOTTOM NAV */
|
||
.bottom-nav{position:fixed;bottom:0;left:0;right:0;z-index:200;background:#fff;border-top:1px solid var(--border);display:flex;padding-bottom:var(--safe-bottom);box-shadow:0 -4px 24px rgba(0,0,0,0.08);}
|
||
.bnav-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px;cursor:pointer;border:none;background:none;color:var(--muted);transition:color .15s;font-size:9px;font-weight:600;font-family:'Outfit',sans-serif;letter-spacing:.03em;position:relative;}
|
||
.bnav-item .icon{font-size:20px;line-height:1;}
|
||
.bnav-item.active{color:var(--coral);}
|
||
.bnav-item .nbadge{position:absolute;top:4px;right:25%;background:var(--coral);color:#fff;border-radius:10px;padding:1px 5px;font-size:9px;font-weight:700;min-width:14px;text-align:center;}
|
||
|
||
/* CONTENT */
|
||
.tab-content{display:none;padding:12px 12px 0;}
|
||
.tab-content.active{display:block;}
|
||
|
||
/* STATS DASHBOARD */
|
||
.stats-bar{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:12px;}
|
||
.stat-card{background:var(--card);border-radius:var(--r);padding:8px 6px;text-align:center;border:1px solid var(--border);}
|
||
.stat-num{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--coral);line-height:1;letter-spacing:-0.02em;}
|
||
.stat-lbl{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-top:2px;}
|
||
|
||
/* RECO HERO */
|
||
.reco-hero{background:linear-gradient(135deg,var(--teal),#1A4B3F);border-radius:var(--r-lg);padding:16px;margin-bottom:14px;color:#fff;position:relative;overflow:hidden;}
|
||
.reco-hero::before{content:'🏆';position:absolute;right:-10px;top:-15px;font-size:90px;opacity:0.08;}
|
||
.reco-label{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,0.6);margin-bottom:4px;}
|
||
.reco-title{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;line-height:1.1;margin-bottom:6px;letter-spacing:-0.02em;}
|
||
.reco-why{font-size:12px;color:rgba(255,255,255,0.75);margin-bottom:12px;line-height:1.5;}
|
||
.reco-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
|
||
.reco-price{font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:800;color:#FFD966;}
|
||
.reco-tag{background:rgba(255,255,255,0.15);border-radius:20px;padding:3px 10px;font-size:11px;font-weight:600;}
|
||
.reco-btn{margin-left:auto;background:#fff;color:var(--teal);border:none;border-radius:var(--r);padding:8px 14px;font-size:12px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;}
|
||
|
||
/* SECTION */
|
||
.section-head{display:flex;align-items:center;justify-content:space-between;margin:14px 0 10px;}
|
||
.section-title{font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:800;letter-spacing:-0.02em;}
|
||
.section-action{font-size:11px;color:var(--coral);font-weight:700;cursor:pointer;background:none;border:none;}
|
||
|
||
/* FILTER ROW */
|
||
.filter-row{display:flex;gap:6px;overflow-x:auto;padding-bottom:2px;margin-bottom:10px;scrollbar-width:none;}
|
||
.filter-row::-webkit-scrollbar{display:none;}
|
||
.chip{padding:5px 12px;border-radius:20px;border:1.5px solid var(--border);background:var(--card);font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;color:var(--muted);transition:all .15s;flex-shrink:0;}
|
||
.chip.active{background:var(--coral);border-color:var(--coral);color:#fff;}
|
||
|
||
/* SORT BAR */
|
||
.sort-bar{display:flex;align-items:center;gap:8px;margin-bottom:10px;padding:8px 12px;background:var(--card);border-radius:var(--r);border:1px solid var(--border);}
|
||
.sort-label{font-size:11px;color:var(--muted);font-weight:600;}
|
||
.sort-select{flex:1;background:none;border:none;font-family:'Outfit',sans-serif;font-size:12px;font-weight:600;color:var(--text);outline:none;cursor:pointer;}
|
||
|
||
/* DEST GRID */
|
||
.dest-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:10px;}
|
||
@media(max-width:660px){.dest-grid{grid-template-columns:1fr;}}
|
||
|
||
.dest-card{background:var(--card);border-radius:var(--r-lg);overflow:hidden;border:1.5px solid var(--border);transition:transform .15s,box-shadow .15s;position:relative;}
|
||
.dest-card:hover{transform:translateY(-1px);box-shadow:0 8px 32px rgba(0,0,0,0.1);}
|
||
.dest-card.dim{opacity:.7;}
|
||
.dest-card.own{border-color:var(--purple);border-style:dashed;}
|
||
|
||
.rank-badge{position:absolute;top:10px;right:10px;background:var(--gold);color:#fff;font-family:'Bricolage Grotesque',sans-serif;font-size:11px;font-weight:800;padding:3px 9px;border-radius:20px;z-index:2;display:none;}
|
||
.rank-badge.show{display:block;}
|
||
.rank-badge.top1{background:#FFD700;color:#7a5c00;}
|
||
.rank-badge.top2{background:#C0C0C0;color:#5a5a5a;}
|
||
.rank-badge.top3{background:#CD7F32;color:#4a2810;}
|
||
|
||
.card-hdr{padding:12px 14px 8px;display:flex;align-items:flex-start;gap:10px;}
|
||
.dest-em{font-size:30px;flex-shrink:0;line-height:1;}
|
||
.dest-meta{flex:1;min-width:0;}
|
||
.dest-name{font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:800;line-height:1.15;letter-spacing:-0.02em;}
|
||
.dest-sub{font-size:11px;color:var(--muted);margin-top:1px;}
|
||
.status-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:4px;}
|
||
.sbadge{padding:2px 8px;border-radius:20px;font-size:9px;font-weight:800;letter-spacing:.06em;color:#fff;white-space:nowrap;text-transform:uppercase;}
|
||
.s-TOP{background:var(--teal);}
|
||
.s-OK{background:var(--gold);}
|
||
.s-NEW{background:var(--purple);}
|
||
.s-SPÄTER{background:var(--blue);}
|
||
.s-MAYBE{background:var(--muted);}
|
||
.s-NEIN{background:#DC2626;}
|
||
.s-OWN{background:var(--purple);}
|
||
|
||
.card-price{padding:8px 14px;background:linear-gradient(90deg,var(--bg),var(--card));display:flex;align-items:center;gap:8px;}
|
||
.price-main{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--text);}
|
||
.price-range{font-size:10px;color:var(--muted);}
|
||
.price-badge{margin-left:auto;font-size:10px;padding:2px 8px;border-radius:10px;font-weight:700;}
|
||
.pb-cheap{background:var(--teal-light);color:var(--teal);}
|
||
.pb-mid{background:var(--gold-light);color:var(--gold);}
|
||
.pb-pricey{background:var(--coral-light);color:var(--coral);}
|
||
|
||
.card-scores{display:flex;gap:8px;padding:6px 14px;background:var(--bg);}
|
||
.sc{flex:1;}
|
||
.sc-lbl{font-size:9px;color:var(--muted);font-weight:700;margin-bottom:2px;text-transform:uppercase;letter-spacing:.05em;}
|
||
.sc-bar{height:3px;background:var(--border);border-radius:2px;overflow:hidden;}
|
||
.sc-fill{height:100%;border-radius:2px;}
|
||
.sc-f{background:var(--teal);}
|
||
.sc-l{background:var(--coral);}
|
||
.sc-m{background:var(--gold);}
|
||
|
||
.card-stats{display:flex;border-top:1px solid var(--bg);border-bottom:1px solid var(--bg);}
|
||
.cstat{flex:1;text-align:center;padding:7px 4px;border-right:1px solid var(--bg);}
|
||
.cstat:last-child{border-right:none;}
|
||
.cstat-val{font-size:11px;font-weight:700;color:var(--text);display:block;}
|
||
.cstat-key{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;}
|
||
|
||
.card-body{padding:10px 14px 12px;}
|
||
.card-highlight{font-size:12px;color:var(--teal);font-weight:600;margin-bottom:7px;font-style:italic;}
|
||
.pros-cons{display:grid;grid-template-columns:1fr 1fr;gap:4px;}
|
||
.pro-list li,.con-list li{list-style:none;font-size:11px;line-height:1.6;padding-left:13px;position:relative;}
|
||
.pro-list li::before{content:"✓";position:absolute;left:0;color:var(--teal);font-weight:700;}
|
||
.con-list li::before{content:"✗";position:absolute;left:0;color:var(--coral);font-weight:700;}
|
||
.card-note{margin-top:8px;padding:6px 9px;background:var(--bg);border-radius:6px;font-size:11px;color:var(--muted);border-left:2px solid var(--gold);}
|
||
|
||
/* STAR RATING */
|
||
.rating-bar{display:flex;align-items:center;padding:10px 14px;border-top:1px solid var(--bg);background:linear-gradient(180deg,var(--bg) 0%,var(--card) 100%);}
|
||
.rating-section{flex:1;}
|
||
.rating-row{display:flex;align-items:center;gap:6px;margin-bottom:4px;}
|
||
.rating-row:last-child{margin-bottom:0;}
|
||
.rating-who{font-size:10px;font-weight:700;color:var(--muted);width:36px;text-transform:uppercase;letter-spacing:.04em;}
|
||
.stars{display:flex;gap:2px;}
|
||
.star{font-size:18px;cursor:pointer;color:#E4DDD4;transition:all .15s;background:none;border:none;padding:0 1px;font-family:Arial,sans-serif;line-height:1;}
|
||
.star.filled{color:#FFB800;}
|
||
.star:hover{transform:scale(1.15);}
|
||
.star.hover-preview{color:#FFD966;}
|
||
|
||
.rating-summary{margin-left:8px;text-align:center;}
|
||
.avg-num{font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:800;color:var(--coral);line-height:1;letter-spacing:-0.02em;}
|
||
.avg-lbl{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-top:2px;}
|
||
|
||
/* COMMENT SECTION */
|
||
.comments-section{border-top:1px solid var(--bg);padding:0;}
|
||
.comments-toggle{width:100%;padding:8px 14px;background:none;border:none;display:flex;align-items:center;gap:6px;font-family:'Outfit',sans-serif;font-size:11px;font-weight:700;color:var(--coral);cursor:pointer;text-align:left;}
|
||
.comments-toggle .arr{transition:transform .2s;}
|
||
.comments-toggle.open .arr{transform:rotate(90deg);}
|
||
.comments-toggle .ccount{margin-left:auto;color:var(--muted);font-weight:600;}
|
||
.comments-body{display:none;padding:0 14px 12px;}
|
||
.comments-body.open{display:block;}
|
||
.comment{background:var(--bg);border-radius:var(--r-sm);padding:8px 10px;margin-bottom:6px;border-left:3px solid var(--coral);}
|
||
.comment.till{border-color:var(--teal);}
|
||
.comment.lea{border-color:var(--coral);}
|
||
.comment-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;}
|
||
.comment-who{font-size:11px;font-weight:700;color:var(--text);}
|
||
.comment-time{font-size:10px;color:var(--muted);}
|
||
.comment-text{font-size:12px;line-height:1.5;color:var(--text);}
|
||
.comment-del{background:none;border:none;color:var(--muted);font-size:14px;cursor:pointer;padding:0 4px;margin-left:4px;}
|
||
.comment-empty{font-size:11px;color:var(--muted);font-style:italic;padding:6px 0;}
|
||
.comment-input-row{display:flex;gap:6px;margin-top:8px;}
|
||
.comment-input{flex:1;padding:8px 10px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-family:'Outfit',sans-serif;font-size:12px;outline:none;background:var(--bg);}
|
||
.comment-input:focus{border-color:var(--coral);}
|
||
.comment-btn{padding:8px 12px;background:var(--coral);color:#fff;border:none;border-radius:var(--r-sm);font-weight:700;font-size:11px;cursor:pointer;font-family:'Outfit',sans-serif;}
|
||
|
||
/* MODAL */
|
||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:500;display:none;align-items:flex-end;justify-content:center;padding:0;}
|
||
.modal-overlay.open{display:flex;}
|
||
.modal{background:#fff;border-radius:20px 20px 0 0;padding:20px;max-height:90vh;overflow-y:auto;width:100%;max-width:540px;}
|
||
.modal-handle{width:36px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 14px;}
|
||
.modal-title{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;margin-bottom:14px;letter-spacing:-0.02em;}
|
||
.form-group{margin-bottom:12px;}
|
||
.form-label{font-size:12px;font-weight:600;color:var(--muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em;}
|
||
.form-input,.form-select,.form-textarea{width:100%;padding:10px 12px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-size:13px;font-family:'Outfit',sans-serif;outline:none;background:var(--bg);transition:border .2s;color:var(--text);}
|
||
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--coral);}
|
||
.form-textarea{resize:none;min-height:70px;}
|
||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
|
||
.modal-btn{width:100%;padding:13px;background:var(--coral);color:#fff;border:none;border-radius:var(--r);font-size:14px;font-weight:700;cursor:pointer;font-family:'Outfit',sans-serif;margin-top:4px;}
|
||
.modal-btn-secondary{background:var(--bg);color:var(--text);border:1.5px solid var(--border);}
|
||
.modal-close{float:right;background:none;border:none;font-size:20px;cursor:pointer;color:var(--muted);padding:0 4px;}
|
||
|
||
/* EMOJI PICKER */
|
||
.emoji-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;background:var(--bg);padding:8px;border-radius:var(--r-sm);max-height:120px;overflow-y:auto;}
|
||
.emoji-pick{background:none;border:none;font-size:20px;cursor:pointer;padding:4px;border-radius:6px;transition:background .15s;}
|
||
.emoji-pick:hover{background:rgba(0,0,0,0.05);}
|
||
.emoji-pick.selected{background:var(--coral-light);}
|
||
|
||
/* RANKING */
|
||
.rank-row{display:grid;grid-template-columns:30px 1fr auto auto;gap:10px;align-items:center;padding:10px 14px;background:var(--card);border-radius:var(--r);margin-bottom:5px;border:1px solid var(--border);}
|
||
.rank-num{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:800;color:var(--coral);text-align:center;letter-spacing:-0.02em;}
|
||
.rank-emoji{font-size:20px;margin-right:8px;}
|
||
.rank-name{font-weight:700;font-size:13px;display:flex;align-items:center;}
|
||
.rank-meta{font-size:10px;color:var(--muted);}
|
||
.rank-score{display:flex;align-items:center;gap:4px;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;color:var(--gold);}
|
||
.rank-stars{font-size:14px;line-height:1;}
|
||
.rank-price{font-family:'Bricolage Grotesque',sans-serif;font-size:13px;font-weight:700;color:var(--teal);}
|
||
|
||
/* HISTORY */
|
||
.hist-year{font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:800;color:var(--coral);margin:16px 0 8px;letter-spacing:-0.03em;}
|
||
.hist-item{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;background:var(--card);border-radius:var(--r-sm);margin-bottom:5px;border:1px solid var(--border);}
|
||
.hist-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:4px;}
|
||
.hotel .hist-dot{background:var(--teal);}
|
||
.cruise .hist-dot{background:var(--coral);}
|
||
.ferienhaus .hist-dot{background:var(--gold);}
|
||
.airbnb .hist-dot{background:var(--purple);}
|
||
.family .hist-dot{background:var(--blue);}
|
||
.hist-em{font-size:18px;flex-shrink:0;}
|
||
.hist-body{flex:1;}
|
||
.hist-dest-name{font-weight:700;font-size:13px;}
|
||
.hist-loc{font-size:11px;color:var(--muted);}
|
||
.hist-tags{display:flex;gap:4px;margin-top:3px;flex-wrap:wrap;}
|
||
.htag{font-size:9px;padding:1px 7px;border-radius:10px;font-weight:700;}
|
||
.htag-hotel{background:var(--teal-light);color:var(--teal);}
|
||
.htag-cruise{background:var(--coral-light);color:var(--coral);}
|
||
.htag-fh{background:var(--gold-light);color:var(--gold);}
|
||
.htag-airbnb{background:var(--purple-light);color:var(--purple);}
|
||
.htag-family{background:var(--blue-light);color:var(--blue);}
|
||
.hist-pers{font-size:10px;color:var(--muted);margin-left:auto;white-space:nowrap;flex-shrink:0;}
|
||
|
||
/* INFO TAB */
|
||
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
||
@media(max-width:540px){.info-grid{grid-template-columns:1fr;}}
|
||
.info-card{background:var(--card);border-radius:var(--r-lg);padding:14px;border:1.5px solid var(--border);}
|
||
.info-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;margin-bottom:8px;letter-spacing:-0.02em;}
|
||
.info-list li{list-style:none;font-size:11px;line-height:1.8;padding-left:13px;position:relative;}
|
||
.info-list li::before{content:"→";position:absolute;left:0;color:var(--coral);}
|
||
.info-list li.excl::before{content:"✗";color:#DC2626;}
|
||
.info-full{grid-column:1/-1;}
|
||
.step-row{display:flex;gap:10px;align-items:flex-start;padding:9px 0;border-bottom:1px solid var(--bg);}
|
||
.step-row:last-child{border-bottom:none;}
|
||
.step-num{width:22px;height:22px;border-radius:50%;background:var(--coral);color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
|
||
.step-text{font-size:11px;line-height:1.6;}
|
||
.step-text strong{color:var(--teal);}
|
||
.age-card{grid-column:1/-1;background:var(--navy);border-radius:var(--r-lg);padding:16px;color:#fff;}
|
||
.age-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:800;margin-bottom:10px;color:rgba(255,255,255,0.9);}
|
||
.age-row{display:flex;gap:10px;align-items:flex-start;margin-bottom:10px;}
|
||
.age-badge{background:var(--coral);color:#fff;border-radius:8px;padding:4px 10px;font-size:11px;font-weight:800;white-space:nowrap;flex-shrink:0;}
|
||
.age-body{font-size:11px;color:rgba(255,255,255,0.65);line-height:1.65;}
|
||
.age-body strong{color:#fff;}
|
||
|
||
/* FAB */
|
||
.fab{position:fixed;right:16px;bottom:calc(80px + var(--safe-bottom));background:var(--coral);color:#fff;border:none;border-radius:50%;width:56px;height:56px;font-size:26px;cursor:pointer;box-shadow:0 4px 20px rgba(229,90,53,0.4);z-index:150;display:flex;align-items:center;justify-content:center;transition:transform .15s;}
|
||
.fab:hover{transform:scale(1.05);}
|
||
|
||
/* TOAST */
|
||
.toast{position:fixed;bottom:calc(90px + var(--safe-bottom));left:50%;transform:translateX(-50%) translateY(20px);background:var(--navy);color:#fff;padding:10px 18px;border-radius:30px;font-size:12px;font-weight:600;opacity:0;transition:all .3s;z-index:400;pointer-events:none;}
|
||
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
|
||
|
||
/* OWN BADGE */
|
||
.own-marker{position:absolute;top:0;left:0;background:var(--purple);color:#fff;padding:3px 10px;font-size:9px;font-weight:800;letter-spacing:.05em;border-bottom-right-radius:8px;text-transform:uppercase;z-index:1;}
|
||
|
||
/* EMPTY */
|
||
.empty{text-align:center;padding:40px 20px;}
|
||
.empty-icon{font-size:48px;margin-bottom:12px;}
|
||
.empty-title{font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:800;margin-bottom:4px;}
|
||
.empty-sub{font-size:12px;color:var(--muted);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- GATE -->
|
||
<div id="gate">
|
||
<div class="gate-box">
|
||
<div class="gate-paw">🐾</div>
|
||
<div class="gate-brand">Fredi24</div>
|
||
<div class="gate-sub">Unser Reise-Planer · Till & Lea</div>
|
||
<input class="gate-input" type="password" id="pw" placeholder="Passwort" onkeydown="if(event.key==='Enter')auth()">
|
||
<button class="gate-btn" onclick="auth()">Einloggen</button>
|
||
<div class="gate-err" id="pw-err">Nicht korrekt – versuch's nochmal 🙅</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HEADER -->
|
||
<div class="header">
|
||
<div class="header-row">
|
||
<div class="brand">
|
||
<span class="brand-logo">🐾</span>
|
||
<div>
|
||
<div class="brand-name">Fredi<span>24</span></div>
|
||
<div class="brand-sub">Unser Reise-Planer</div>
|
||
</div>
|
||
</div>
|
||
<div class="user-pill" onclick="switchUser()" id="user-pill">👤 <span id="user-name">Till</span></div>
|
||
</div>
|
||
<div class="header-pills">
|
||
<span class="hp">✈️ HAM / HAJ · Direktflug</span>
|
||
<span class="hp">⭐⭐⭐⭐ Mind. 4★</span>
|
||
<span class="hp budget">💶 Max. 3.600 €</span>
|
||
<span class="hp">📅 ab 13. Juli 2026</span>
|
||
</div>
|
||
<div class="budget-indicator">
|
||
<div class="budget-track"><div class="budget-fill" style="width:78%"></div></div>
|
||
<span class="budget-label">2.800–3.600 € Range</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FAB -->
|
||
<button class="fab" onclick="openSubmit()" title="Eigene Idee einreichen">✚</button>
|
||
|
||
<!-- TOAST -->
|
||
<div class="toast" id="toast">✓ Gespeichert</div>
|
||
|
||
<!-- SUBMIT MODAL -->
|
||
<div class="modal-overlay" id="submit-modal" onclick="if(event.target===this)closeModal('submit-modal')">
|
||
<div class="modal">
|
||
<div class="modal-handle"></div>
|
||
<button class="modal-close" onclick="closeModal('submit-modal')">✕</button>
|
||
<div class="modal-title">💡 Eigene Idee einreichen</div>
|
||
<input type="hidden" id="edit-id" value="">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Emoji</label>
|
||
<input class="form-input" id="s-emoji" placeholder="🏖️" maxlength="4" value="🌍" type="text">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Status</label>
|
||
<select class="form-select" id="s-status">
|
||
<option value="TOP">⭐ TOP</option>
|
||
<option value="OK">👍 OK</option>
|
||
<option value="MAYBE">🤔 Vielleicht</option>
|
||
<option value="SPÄTER">⏳ Später</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Destination *</label>
|
||
<input class="form-input" id="s-dest" placeholder="z.B. Ibiza Nordküste" type="text">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Land / Region</label>
|
||
<input class="form-input" id="s-land" placeholder="Spanien" type="text">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Budget (€)</label>
|
||
<input class="form-input" id="s-budget" placeholder="3000" type="number">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Subtext (kurze Beschreibung)</label>
|
||
<input class="form-input" id="s-sub" placeholder="z.B. AI Resort · Familien-Hotel" type="text">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Warum interessant? (Begründung)</label>
|
||
<textarea class="form-textarea" id="s-why" placeholder="Was macht das Ziel spannend für euch?"></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Vorgeschlagen von</label>
|
||
<select class="form-select" id="s-who">
|
||
<option>Till</option>
|
||
<option>Lea</option>
|
||
<option>Beide</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Flugzeit (ca.)</label>
|
||
<input class="form-input" id="s-flight" placeholder="3h 30" type="text">
|
||
</div>
|
||
</div>
|
||
<button class="modal-btn" onclick="submitIdea()" id="submit-btn">Idee einreichen 🚀</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- USER SWITCH MODAL -->
|
||
<div class="modal-overlay" id="user-modal" onclick="if(event.target===this)closeModal('user-modal')">
|
||
<div class="modal">
|
||
<div class="modal-handle"></div>
|
||
<div class="modal-title">👤 Wer bewertet gerade?</div>
|
||
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Deine Sterne und Kommentare werden unter dem gewählten Profil gespeichert.</p>
|
||
<div class="profile-pick">
|
||
<div class="profile-opt" onclick="setUser('Till')" id="opt-till">
|
||
<span class="emoji">👨💼</span>
|
||
Till
|
||
</div>
|
||
<div class="profile-opt" onclick="setUser('Lea')" id="opt-lea">
|
||
<span class="emoji">👩🦰</span>
|
||
Lea
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============= IDEEN TAB ============= -->
|
||
<div id="tab-ideen" class="tab-content active">
|
||
|
||
<div class="stats-bar">
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="stat-total">18</div>
|
||
<div class="stat-lbl">Ideen</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="stat-rated">0</div>
|
||
<div class="stat-lbl">Bewertet</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="stat-comments">0</div>
|
||
<div class="stat-lbl">Kommentare</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-num" id="stat-days">–</div>
|
||
<div class="stat-lbl">Tage bis Reise</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reco-hero">
|
||
<div class="reco-label">🏆 Top-Empfehlung Sommer 2026</div>
|
||
<div class="reco-title">Sardinien direkt –<br>sofort buchbar</div>
|
||
<div class="reco-why">Direktflug Hamburg 2:20h · EU-Kindersitz-Standard · Neuland für euch · Lea sagt ja · Felix liebt Sandstrände · Im Budget.</div>
|
||
<div class="reco-row">
|
||
<span class="reco-price">~3.150 €</span>
|
||
<span class="reco-tag">✈️ 2h 20</span>
|
||
<span class="reco-tag">🏖️ 4★</span>
|
||
<button class="reco-btn" onclick="scrollToCard('sardinien')">Details →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sort-bar">
|
||
<span class="sort-label">📊 Sortierung:</span>
|
||
<select class="sort-select" id="sort-select" onchange="renderCards()">
|
||
<option value="ranking">⭐ Bewertung (höchste zuerst)</option>
|
||
<option value="status">📌 Status (TOP → SPÄTER)</option>
|
||
<option value="price-asc">💰 Preis (günstig → teuer)</option>
|
||
<option value="price-desc">💰 Preis (teuer → günstig)</option>
|
||
<option value="name">🔤 Name A-Z</option>
|
||
<option value="newest">🆕 Eigene Ideen zuerst</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="filter-row">
|
||
<button class="chip active" onclick="filterStatus('ALL',this)">Alle</button>
|
||
<button class="chip" onclick="filterStatus('TOP',this)">⭐ TOP</button>
|
||
<button class="chip" onclick="filterStatus('OK',this)">👍 OK</button>
|
||
<button class="chip" onclick="filterStatus('NEW',this)">🆕 Neu</button>
|
||
<button class="chip" onclick="filterStatus('MAYBE',this)">🤔 Vielleicht</button>
|
||
<button class="chip" onclick="filterStatus('SPÄTER',this)">⏳ Später</button>
|
||
<button class="chip" onclick="filterStatus('OWN',this)">💜 Eigene</button>
|
||
</div>
|
||
|
||
<div class="dest-grid" id="dg"></div>
|
||
</div>
|
||
|
||
<!-- ============= RANKING TAB ============= -->
|
||
<div id="tab-ranking" class="tab-content">
|
||
<div class="section-head">
|
||
<span class="section-title">🏆 Ranking-Liste</span>
|
||
<button class="section-action" onclick="exportData()">📋 Export</button>
|
||
</div>
|
||
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Sortiert nach Durchschnitt aller Bewertungen. Stimmt mehr ab → Liste wird dynamischer.</p>
|
||
<div id="ranking-list"></div>
|
||
</div>
|
||
|
||
<!-- ============= EIGENE IDEEN TAB ============= -->
|
||
<div id="tab-eigene" class="tab-content">
|
||
<div class="section-head">
|
||
<span class="section-title">💜 Eigene Vorschläge</span>
|
||
<button class="section-action" onclick="openSubmit()">+ Neu</button>
|
||
</div>
|
||
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;">Hier landen alle Ideen, die ihr selbst eingereicht habt. Werden gleichberechtigt im Ideen-Dashboard angezeigt.</p>
|
||
<div class="dest-grid" id="own-list"></div>
|
||
</div>
|
||
|
||
<!-- ============= ARCHIV TAB ============= -->
|
||
<div id="tab-archiv" class="tab-content">
|
||
<div class="filter-row">
|
||
<button class="chip active" onclick="filterHistory('ALL',this)">Alle</button>
|
||
<button class="chip" onclick="filterHistory('hotel',this)">🏨 Hotel</button>
|
||
<button class="chip" onclick="filterHistory('cruise',this)">🚢 Kreuzfahrt</button>
|
||
<button class="chip" onclick="filterHistory('ferienhaus',this)">🏡 Ferienhaus</button>
|
||
<button class="chip" onclick="filterHistory('airbnb',this)">🏠 Airbnb</button>
|
||
<button class="chip" onclick="filterHistory('family',this)">👨👩👦 Familie</button>
|
||
</div>
|
||
<div id="hl"></div>
|
||
</div>
|
||
|
||
<!-- ============= INFO TAB ============= -->
|
||
<div id="tab-info" class="tab-content">
|
||
<div class="info-grid">
|
||
<div class="info-card">
|
||
<div class="info-card-title">❌ Ausschlussliste</div>
|
||
<ul class="info-list">
|
||
<li class="excl">Türkei, Ägypten (Lea)</li>
|
||
<li class="excl">Dänemark im Sommer (Lea)</li>
|
||
<li class="excl">Kroatien (überfüllt)</li>
|
||
<li class="excl">Griechenland (Kindersitz-Problem)</li>
|
||
<li class="excl">AIDA Norwegen (2024 gemacht)</li>
|
||
<li class="excl">Kein Umsteigeflug</li>
|
||
<li class="excl">Nur HAM oder HAJ als Abflug</li>
|
||
<li class="excl">Unter 4 Sterne</li>
|
||
<li class="excl">P&V Cap d'Agde / Houlgate</li>
|
||
</ul>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="info-card-title">✅ Reiseprofil</div>
|
||
<ul class="info-list">
|
||
<li>Direktflug HAM/HAJ</li>
|
||
<li>Mind. 4★ oder Glamping-Village</li>
|
||
<li>Pool + täglich Mietwagen</li>
|
||
<li>Felix: Kinderclub, Strand</li>
|
||
<li>EU-Kindersitz Pflicht</li>
|
||
<li>Lea: HP/AI bevorzugt</li>
|
||
<li>Till: Abwechslung + Fotografie</li>
|
||
<li>Caddy = Dienstwagen HHGmbH</li>
|
||
<li>Frieda bleibt zuhause</li>
|
||
</ul>
|
||
</div>
|
||
<div class="age-card">
|
||
<div class="age-card-title">📅 Felix-Alterskurve</div>
|
||
<div class="age-row"><div class="age-badge">2026<br>3,5J</div><div class="age-body"><strong>Jetzt:</strong> Strand, Pool, Kinderclub ab 3J, Wasserpark. Max. 6h Flug.</div></div>
|
||
<div class="age-row"><div class="age-badge">2027<br>4J</div><div class="age-body"><strong>Bald:</strong> + Burgen (Gruyères!), Astrid Lindgrens Welt, Tierparks.</div></div>
|
||
<div class="age-row"><div class="age-badge">2028-29<br>5-6J</div><div class="age-body"><strong>Madeira-Fenster:</strong> + Levadas, Schweden Schären, Norwegen Natur.</div></div>
|
||
<div class="age-row"><div class="age-badge">2030+<br>7J+</div><div class="age-body"><strong>Träume:</strong> + Bali, Japan, Lappland Nordlichter.</div></div>
|
||
</div>
|
||
<div class="info-card info-full">
|
||
<div class="info-card-title">⚡ Nächste Schritte</div>
|
||
<div class="step-row"><div class="step-num">1</div><div class="step-text"><strong>AIDA prüfen:</strong> aida.de Route PMI07307 – Sardinien-Route 13.–16. Juli? Kinder 35% Rabatt.</div></div>
|
||
<div class="step-row"><div class="step-num">2</div><div class="step-text"><strong>Glamping ASAP:</strong> Union Lido/Cavallino, Punta Lunga/Vieste. acsi.eu + pitchup.com.</div></div>
|
||
<div class="step-row"><div class="step-num">3</div><div class="step-text"><strong>Sterne vergeben:</strong> Beide voten unabhängig. Ranking-Tab zeigt Match.</div></div>
|
||
<div class="step-row"><div class="step-num">4</div><div class="step-text"><strong>Diskutieren:</strong> Kommentare unter jeder Karte. Lea kann ihre Bedenken festhalten, Till seine Punkte.</div></div>
|
||
<div class="step-row"><div class="step-num">5</div><div class="step-text"><strong>Entscheiden + buchen:</strong> Wenn beide ⭐⭐⭐⭐+ = Match. Sonst weiter abwägen.</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BOTTOM NAV -->
|
||
<div class="bottom-nav">
|
||
<button class="bnav-item active" onclick="navTo('ideen',this)">
|
||
<span class="icon">🗺️</span><span>Ideen</span>
|
||
</button>
|
||
<button class="bnav-item" onclick="navTo('ranking',this)">
|
||
<span class="icon">🏆</span><span>Ranking</span>
|
||
</button>
|
||
<button class="bnav-item" onclick="navTo('eigene',this)">
|
||
<span class="icon">💜</span><span>Eigene</span>
|
||
<span class="nbadge" id="own-badge" style="display:none">0</span>
|
||
</button>
|
||
<button class="bnav-item" onclick="navTo('archiv',this)">
|
||
<span class="icon">📖</span><span>Archiv</span>
|
||
</button>
|
||
<button class="bnav-item" onclick="navTo('info',this)">
|
||
<span class="icon">⚡</span><span>Info</span>
|
||
</button>
|
||
</div>
|
||
|
||
<script>
|
||
// ============= DESTINATIONS DATA =============
|
||
const DESTINATIONS_BUILTIN = [{"id": "sardinien", "emoji": "🏖️", "name": "Sardinien", "country": "🇮🇹 Italien", "sub": "Nordsardinien · Hotel 4★ · Mietwagen", "status": "TOP", "isRecommendation": true, "price": 3150, "priceLabel": "für 3 Pers. 7N", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 80, "scoreCar": 100, "flight": "2h 20", "from": "HAM→OLB", "climate": "30–35°C", "available": "ab 13.Jul ✓", "highlight": "Karibik-Strände · EU-Kindersitz-Standard · Neuland", "pros": ["Direktflug HAM–Olbia 2:20h", "Traumstrände (La Pelosa, Principe)", "Lea hat Sardinien okayed", "Sehr kinderfreundlich"], "cons": ["30–35°C im Juli heiß", "Kein AI = Eigenplanung", "Hochsaison teuer + voll"], "note": "Hotels: Club Hotel Baja Sardinia (4★), Sporting Beach Hotel. Nordsardinien bevorzugen.", "felixMinAge": 0, "season": ["6", "7", "8", "9"]}, {"id": "aida-kreuzfahrt", "emoji": "🚢", "name": "AIDAcosma Kreuzfahrt", "country": "🇮🇹 Mittelmeer", "sub": "Korsika · Sardinien · Rom · Marseille · Barcelona", "status": "TOP", "price": 3320, "priceLabel": "Schiff + Flug HAM→PMI", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 100, "scoreCar": 20, "flight": "2h", "from": "HAM→PMI", "climate": "25–28°C", "available": "Prüfen!", "highlight": "Kreuzfahrt-Wunsch erfüllt · Lea muss nichts planen · Vollpension", "pros": ["Korsika + Sardinien + Rom kombiniert", "Felix Kids-Club 3–6J + Rutschen", "AIDA kennt ihr seit 2 Reisen", "Kinder 35% Rabatt"], "cons": ["Korsika-Route: 4.Juli oder 15.Aug", "18.Juli = Gibraltar (weniger gut)", "Kein echtes Mietwagen-Feeling"], "note": "⚠️ aida.de Route PMI07307 direkt prüfen: Sardinien-Route 13.–16. Juli verfügbar?", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "algarve", "emoji": "🌊", "name": "Algarve", "country": "🇵🇹 Portugal", "sub": "Carvoeiro · Lagos · Atlantik-Klippenküste", "status": "TOP", "price": 2950, "priceLabel": "günstigste TOP-Option", "priceBadge": "Budget-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 100, "scoreLea": 65, "scoreCar": 100, "flight": "3h 30", "from": "HAM→FAO", "climate": "25–28°C", "available": "ab 13.Jul ✓", "highlight": "Mildestes Klima für Felix · Spektakuläre Mietwagen-Touren", "pros": ["25–28°C ideal für Kleinkind", "Cabo de São Vicente, Lagos, Sagres", "Carvoeiro, Praia da Luz – ruhig", "Günstigstes Budget aller TOPs"], "cons": ["Lea etwas zögerlich (neu)", "Weniger AI-Auswahl als Kanaren"], "note": "HP statt AI – ihr esst täglich unterwegs bei Mietwagen-Touren.", "felixMinAge": 0, "season": ["4", "5", "6", "7", "8", "9", "10"]}, {"id": "glamping", "emoji": "🏕️", "name": "Glamping Village", "country": "🇮🇹 Italien", "sub": "Bungalow direkt am Meer · Pool · Kinderanimation", "status": "NEW", "price": 2500, "priceLabel": "günstigste Option!", "priceBadge": "Preis-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 100, "scoreLea": 70, "scoreCar": 80, "flight": "2h", "from": "HAM→VCE", "climate": "28–33°C", "available": "ASAP!", "highlight": "Tipp vom Meister: Bungalow am Meer mit Pool – unterschätzt!", "pros": ["Direkter Meerzugang + eigener Pool", "Kinderclub, Animation, Strand", "30–50% günstiger als Hotel", "Absolutes Neuland"], "cons": ["Kein 4★-Hotelkomfort", "Qualität je nach Anlage variabel", "Juli-Plätze schnell weg"], "note": "Top: Union Lido/Cavallino (Venetien), Punta Lunga/Vieste (Gargano). Buchung: acsi.eu · pitchup.com", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "schweiz", "emoji": "🏔️", "name": "Schweiz + Norditalien", "country": "🇨🇭 Schweiz", "sub": "Fribourg · Gruyères · Lago Maggiore", "status": "OK", "price": 2000, "priceLabel": "günstigster Kurztrip", "priceBadge": "Budget-Winner 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 60, "scoreLea": 90, "scoreCar": 80, "flight": "860 km", "from": "Caddy*", "climate": "22–28°C", "available": "Flexibel", "highlight": "Lea würde es sofort machen · Freunde besuchen · Château de Gruyères", "pros": ["Lea will Schweiz ausdrücklich", "Château de Gruyères – Felix findet Burgen spannend!", "Tagestour Lago Maggiore (2,5h)", "Freunde bei Fribourg = ggf. günstig"], "cons": ["860 km Caddy (Dienstwagen)", "Schweizer Hotels teuer", "Kein klassischer Strandurlaub"], "note": "*Caddy ist Dienstwagen HHGmbH – keine persönlichen Kosten. Alternativ: Flug HAM→GVA + Mietwagen.", "felixMinAge": 3, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "cote-azur", "emoji": "🇫🇷", "name": "Côte d'Azur", "country": "🇫🇷 Frankreich", "sub": "Nizza · Antibes · Familien-Feriendorf", "status": "OK", "price": 3000, "priceLabel": "Flug + Hotel/Feriendorf", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 80, "scoreLea": 85, "scoreCar": 90, "flight": "2h", "from": "HAM→NCE", "climate": "28–33°C", "available": "ab 13.Jul ✓", "highlight": "Lea würde sofort Frankreich machen · Charmant", "pros": ["Lea mag Frankreich", "Nizza direkt ab Hamburg 2h", "Traumküste + Mietwagen (Monaco, Cannes)", "Familienfreundliche Feriendörfer"], "cons": ["Juli sehr teuer an der Côte d'Azur", "Wenig AI-Hotels"], "note": "Feriendorf-Tipp: Center Parcs Les Lacs de l'Escapade (Var) oder Antibes direkt.", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "taurito", "emoji": "🌴", "name": "Taurito Princess", "country": "🇪🇸 Gran Canaria", "sub": "TUI KIDS CLUB · All Inclusive", "status": "OK", "price": 3100, "priceLabel": "AI Pauschal inkl. Flug", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 100, "scoreLea": 100, "scoreCar": 60, "flight": "5h", "from": "HAM→LPA", "climate": "25–28°C", "available": "ab 13.Jul ✓", "highlight": "Lea's ausdrücklicher Favorit · Felix Mini-Club ab 3J", "pros": ["Lea will es", "Felix Mini-Club + Kinderpool", "AI-Vollversorgung", "Wasserpark 5 Min entfernt"], "cons": ["4. Kanaren-Reise in 4 Jahren", "Till will was anderes", "AI-Bunker ohne echte Touren"], "note": "Link: c.24.de/pr/F8F2FX · Kinderfestpreis (2–12J) Mai–Sep 2026.", "felixMinAge": 3, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "fuerteventura", "emoji": "🏜️", "name": "Fuerteventura", "country": "🇪🇸 Kanaren", "sub": "Aldiana / Robinson · Ruhiger als GC", "status": "OK", "price": 3200, "priceLabel": "AI-Clubhotel", "priceBadge": "Im Budget ✓", "priceBadgeClass": "pb-mid", "scoreFelix": 90, "scoreLea": 80, "scoreCar": 60, "flight": "4h 30", "from": "HAM→FUE", "climate": "24–28°C", "available": "ab 13.Jul ✓", "highlight": "Ruhigste Kanareninseln · Riesige Sandstrände", "pros": ["Weniger touristisch als GC", "Aldiana/Robinson Club-Konzept", "Riesige flache Strände für Felix"], "cons": ["Kanaren = nochmal dasselbe", "Windig (Kitesurf-Destination)"], "felixMinAge": 0, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "menorca", "emoji": "🏝️", "name": "Menorca", "country": "🇪🇸 Balearen", "sub": "Ruhigste Baleareninsel · Cala Galdana", "status": "OK", "price": 2900, "priceLabel": "HP 4★ + Flug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 85, "scoreLea": 75, "scoreCar": 90, "flight": "2h 30", "from": "HAM→MAH", "climate": "28–33°C", "available": "ab 13.Jul ✓", "highlight": "Ruhiger als Mallorca · Türkises Wasser", "pros": ["Ruhiger als Mallorca + Ibiza", "Biosphärenreservat – sauber", "Schöne Buchten mit Mietwagen"], "cons": ["Weniger Kinderclub-Angebot", "Sehr heiß im Juli"], "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "kapverden", "emoji": "🌍", "name": "Kapverden", "country": "🌍 Atlantik", "sub": "Sal · Boa Vista · AI-Resort · Exotisch", "status": "OK", "price": 3400, "priceLabel": "AI Direktflug", "priceBadge": "Knapp am Budget", "priceBadgeClass": "pb-pricey", "scoreFelix": 80, "scoreLea": 75, "scoreCar": 40, "flight": "6h", "from": "HAM→SID", "climate": "27–30°C", "available": "ok", "highlight": "Exotisch ohne langen Flug · Kilometer-Strände", "pros": ["Direktflug 6h – kein Umsteigen", "Riesige flache Strände (Felix-Gold)", "RIU/Iberostar All Inclusive", "Echtes Neuland"], "cons": ["Knapp am Budget", "Wenig Mietwagen-Sightseeing", "Windige Insel"], "felixMinAge": 2, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "malta", "emoji": "⛪", "name": "Malta", "country": "🇲🇹 Malta", "sub": "Gozo · St. Julian's · Familienhotel 4★", "status": "OK", "price": 2400, "priceLabel": "günstig für Europa", "priceBadge": "Preis-Tipp 💚", "priceBadgeClass": "pb-cheap", "scoreFelix": 75, "scoreLea": 70, "scoreCar": 75, "flight": "3h", "from": "HAM→MLA", "climate": "30–34°C", "available": "ab 13.Jul ✓", "highlight": "Kleinste Insel Europas · EU · Günstig · Englisch", "pros": ["Direktflug 3h, günstig", "EU-Standards, kein Visum", "Gozo für Mietwagen-Touren"], "cons": ["Kaum Sandstrand (Felsküste)", "Sehr heiß + touristisch im Juli"], "felixMinAge": 0, "season": ["4", "5", "6", "7", "8", "9", "10"]}, {"id": "lanzarote", "emoji": "🌋", "name": "Lanzarote", "country": "🇪🇸 Kanaren", "sub": "Vulkaninsel · Puerto del Carmen", "status": "OK", "price": 2800, "priceLabel": "HP + Flug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 80, "scoreLea": 70, "scoreCar": 80, "flight": "4h 45", "from": "HAM→ACE", "climate": "25–28°C", "available": "ab 13.Jul ✓", "highlight": "Ruhigste Kanareninseln · Vulkan-Landschaft · César Manrique", "pros": ["Ruhiger als GC/Teneriffa", "Timanfaya Vulkan per Mietwagen", "Passat-Klima: 25–28°C"], "cons": ["Kanaren = schon dreimal", "Weniger Kinderclub-Auswahl"], "felixMinAge": 0, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "costa-sol", "emoji": "🌞", "name": "Costa del Sol / Málaga", "country": "🇪🇸 Andalusien", "sub": "Nerja · Marbella · Direktflug HAM", "status": "OK", "price": 2700, "priceLabel": "HP + Direktflug", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 80, "scoreLea": 70, "scoreCar": 90, "flight": "3h 20", "from": "HAM→AGP", "climate": "32–38°C", "available": "ab 13.Jul ✓", "highlight": "Günstig · Andalusien Mietwagen-Touren", "pros": ["Direktflug HAM→Málaga 3h20", "Nerja (schön + ruhig)", "Mietwagen: Granada, Alhambra, Ronda"], "cons": ["32–38°C im Juli sehr heiß", "Viele Touristenstädte"], "felixMinAge": 0, "season": ["4", "5", "6", "9", "10"]}, {"id": "msc-kreuzfahrt", "emoji": "🛳️", "name": "MSC / TUI Cruises", "country": "🚢 Mittelmeer", "sub": "Alternative zu AIDA", "status": "MAYBE", "price": 3000, "priceLabel": "inkl. Flug", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 90, "scoreLea": 80, "scoreCar": 20, "flight": "2h", "from": "Flug FCO/VCE", "climate": "25–30°C", "available": "Prüfen", "highlight": "Alternative Reederei · Evtl. günstigere Routen", "pros": ["MSC Grandiosa familienfreundlich", "Evtl. günstigere Kabinen", "Mein Schiff deutschsprachig"], "cons": ["Kein AIDA-Erfahrungsvorteil", "Kinderprogramm variiert"], "felixMinAge": 0, "season": ["5", "6", "7", "8", "9", "10"]}, {"id": "korsika", "emoji": "⛰️", "name": "Korsika direkt", "country": "🇫🇷 Frankreich", "sub": "Palombaggia · Porto-Vecchio · Bonifacio", "status": "MAYBE", "price": 3200, "priceLabel": "Ferienwohnung + Flug", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 70, "scoreLea": 70, "scoreCar": 100, "flight": "via Paris", "from": "Umsteigen!", "climate": "28–32°C", "available": "Hochpreis", "highlight": "Schönste Strände Europas · Lea hat Korsika okayed", "pros": ["Palombaggia: traumhaft", "Lea okay für Korsika"], "cons": ["Kein Direktflug ab HAM/HAJ", "Hochpreisinsel im Sommer"], "note": "Besser als AIDA-Stopp in Ajaccio als Kreuzfahrt-Erlebnis.", "felixMinAge": 0, "season": ["5", "6", "7", "8", "9"]}, {"id": "madeira", "emoji": "🌺", "name": "Madeira", "country": "🇵🇹 Portugal", "sub": "Dritter Anlauf · Levadas · Porto Moniz", "status": "SPÄTER", "price": 3000, "priceLabel": "Flug + Hotel HP", "priceBadge": "Im Budget", "priceBadgeClass": "pb-mid", "scoreFelix": 40, "scoreLea": 80, "scoreCar": 100, "flight": "4h 30", "from": "HAM→FNC", "climate": "22–25°C", "available": "2028?", "highlight": "Lea: \"wenn Felix älter ist\" · 2× Corona-Storno · 3. Anlauf!", "pros": ["22–25°C – mildestes Klima", "Mietwagen-Paradies (Levadas)", "Vertraut – 2× geplant gewesen"], "cons": ["Kaum Sandstrand für Felix", "Lea: explizit \"warten bis Felix größer\""], "note": "📅 Ab Felix 5J+ (ca. 2028/29)", "felixMinAge": 5, "season": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}, {"id": "schweden", "emoji": "🦌", "name": "Schweden", "country": "🇸🇪 Schweden", "sub": "Bohuslän · Astrid Lindgrens Welt", "status": "SPÄTER", "price": 2800, "priceLabel": "Ferienhäuser + Mietwagen", "priceBadge": "Günstig ✓", "priceBadgeClass": "pb-cheap", "scoreFelix": 85, "scoreLea": 50, "scoreCar": 100, "flight": "1h 20", "from": "HAM→GOT", "climate": "18–24°C", "available": "2027", "highlight": "Till's Herzensthema · Astrid Lindgrens Welt!", "pros": ["Astrid Lindgrens Welt (Felix mit 4J+)", "Direktflug HAM→Göteborg 1:20h", "Natur pur: Seen, Wälder, Schären"], "cons": ["Lea: \"nicht meine Vorstellung von Sommerurlaub\"", "Wetter wechselhaft"], "note": "📅 Ab Felix 4J (ca. 2027) – Astrid Lindgrens Welt!", "felixMinAge": 4, "season": ["6", "7", "8"]}, {"id": "bali", "emoji": "🌴", "name": "Bali", "country": "🇮🇩 Indonesien", "sub": "Jenny's Verbindung · Ubud · Seminyak", "status": "SPÄTER", "price": 6000, "priceLabel": "Flug + Villa", "priceBadge": "Über Budget", "priceBadgeClass": "pb-pricey", "scoreFelix": 50, "scoreLea": 95, "scoreCar": 80, "flight": "~14h", "from": "Umsteigen", "climate": "28–32°C", "available": "2030+", "highlight": "Lea's Freundin Jenny hat Mann aus Bali", "pros": ["Traumziel für beide", "Günstige Luxusvillen mit Pool", "Jenny als Insider-Kontakt"], "cons": ["Langer Flug + Zeitzone (+6h)", "Gesundheit/Impfungen", "Aktuell zu teuer"], "note": "📅 Ab Felix 7–8J (ca. 2030)", "felixMinAge": 7, "season": ["4", "5", "6", "7", "8", "9"]}];
|
||
|
||
// ============= HISTORY DATA =============
|
||
const HISTORY = [
|
||
{year:2015,emoji:"🇬🇧",dest:"Zimmer in London (Bermondsey)",loc:"London, UK · Airbnb bei Gordon",type:"airbnb",pers:"Till + Bruder Jasper",date:"26.–31. Aug. · 5N"},
|
||
{year:2016,emoji:"🌲",dest:"A-Frame Cabin in Sulzfeld",loc:"Feriendorf Sulzfeld, Deutschland",type:"airbnb",pers:"Till solo",date:"5.–10. Sept. · 5N"},
|
||
{year:2016,emoji:"🇵🇹",dest:"Porto – Zimmer in Baixa",loc:"Porto, Portugal",type:"airbnb",pers:"Till solo",date:"26.–29. Sept. · 3N"},
|
||
{year:2016,emoji:"🇳🇱",dest:"Kortenhoef (Niederlande)",loc:"Kortenhoef, NL",type:"airbnb",pers:"Till solo",date:"1.–3. Okt. · 2N"},
|
||
{year:2017,emoji:"🇵🇹",dest:"Lissabon (Anjos)",loc:"Lisboa, Portugal",type:"airbnb",pers:"Till solo",date:"26.–28. Sept. · 2N"},
|
||
{year:2017,emoji:"🌺",dest:"Funchal, Madeira (8N)",loc:"Funchal, Madeira",type:"airbnb",pers:"Till solo",date:"28. Sept.–6. Okt. · 8N"},
|
||
{year:2017,emoji:"🍷",dest:"Vila Nova de Gaia / Porto",loc:"Porto / Douro, Portugal",type:"airbnb",pers:"Till solo",date:"6.–10. Okt. · 4N"},
|
||
{year:2018,emoji:"🇸🇪",dest:"Sunne, Värmland – Hochzeit",loc:"Sunne, Schweden · Till schwärmt seitdem",type:"family",pers:"Till + Familie",date:"Sommer"},
|
||
{year:2019,emoji:"🇬🇧",dest:"Greater London (Battersea)",loc:"London, UK · bei Jilly",type:"airbnb",pers:"Till + Lea",date:"22.–25. März · 3N"},
|
||
{year:2019,emoji:"🌴",dest:"Vanilla Garden · Teneriffa",loc:"Playa de las Americas · 4★ HP",type:"hotel",pers:"Till + Lea",date:"31.07–07.08 · 7N · #41629995"},
|
||
{year:2019,emoji:"🇩🇰",dest:"Oksbøl Ferienhaus",loc:"Oksbøl, Dänemark",type:"airbnb",pers:"Till + Lea",date:"29.Okt.–2.Nov. · 4N"},
|
||
{year:2020,emoji:"🚤",dest:"Waterside Cabin Vinkeveen",loc:"Vinkeveen (nahe Amsterdam), NL",type:"airbnb",pers:"Till + Lea",date:"22.–29. Aug. · 7N"},
|
||
{year:2021,emoji:"🌴",dest:"Radisson Blu Gran Canaria",loc:"Patalavaca · 5★ HP",type:"hotel",pers:"Till + Lea",date:"21.–28.07 · 7N · #47201487"},
|
||
{year:2022,emoji:"🌴",dest:"Dreams Jardin Tropical",loc:"Costa Adeje · Teneriffa · 4★ HP · 3.276 €",type:"hotel",pers:"Till + Lea",date:"08.–15.07 · 7N · #12852050"},
|
||
{year:2023,emoji:"🚢",dest:"AIDAblu Adria-Kreuzfahrt",loc:"Korfu · Triest · Zadar · Kotor · Dubrovnik · Bari",type:"cruise",pers:"Till+Lea+Felix+Astrid",date:"13.–20.08 · 7N · 5.557 € · #14997795"},
|
||
{year:2023,emoji:"🏨",dest:"Acharavi Beach Hotel · Korfu",loc:"Acharavi, GR · 546 €",type:"hotel",pers:"Till+Lea+Felix",date:"20.–22.08 · 2N · #410637840"},
|
||
{year:2023,emoji:"🏡",dest:"MarinaPark Bad Nederrijn",loc:"Maurik, NL",type:"ferienhaus",pers:"Familie",date:"01.–05.10 · 4N"},
|
||
{year:2024,emoji:"🏡",dest:"EuroParcs De Utrechtse Heuvelrug",loc:"Maarn, NL · 422 €",type:"ferienhaus",pers:"Familie",date:"23.–28.03 · 5N"},
|
||
{year:2024,emoji:"🚢",dest:"AIDAprima Norwegen",loc:"HAM→Ålesund→Geiranger→Bergen→HAM",type:"cruise",pers:"Familie",date:"15.–22.06 · 7N · 1.998 € · #15808257"},
|
||
{year:2024,emoji:"🏡",dest:"Sonne und Strand, Følle Strand",loc:"Rønde, Dänemark",type:"ferienhaus",pers:"Familie",date:"14.–21.09 · 7N"},
|
||
{year:2025,emoji:"🏡",dest:"Residence Berger Duinen",loc:"Bergen aan Zee, NL",type:"ferienhaus",pers:"Familie",date:"24.–28.03 · 4N"},
|
||
{year:2025,emoji:"🏡",dest:"De Strabrechtse Vennen",loc:"Niederlande · Haus E8",type:"ferienhaus",pers:"Familie",date:"14.–21.07 · 7N"},
|
||
{year:2025,emoji:"🏡",dest:"EuroParcs Esonstad",loc:"Niederlande",type:"ferienhaus",pers:"Familie",date:"27.–31.10 · 4N"},
|
||
{year:2026,emoji:"🚤",dest:"De Reeuwijkse Plassen",loc:"NL · 4-Pers.-Wasserhaus #191",type:"ferienhaus",pers:"Familie",date:"30.03–03.04 · 4N"},
|
||
];
|
||
|
||
// ============= AUTH =============
|
||
(function(){if(sessionStorage.getItem('f24')==='1')document.getElementById('gate').style.display='none';})();
|
||
function auth(){
|
||
if(document.getElementById('pw').value==='frediurlaub26'){
|
||
document.getElementById('gate').style.display='none';
|
||
sessionStorage.setItem('f24','1');
|
||
init();
|
||
} else {
|
||
document.getElementById('pw-err').style.display='block';
|
||
document.getElementById('pw').style.borderColor='var(--coral)';
|
||
setTimeout(()=>document.getElementById('pw').style.borderColor='',1500);
|
||
}
|
||
}
|
||
|
||
// ============= USER PROFILE =============
|
||
function getCurrentUser(){return localStorage.getItem('f24_user')||'Till';}
|
||
function setUser(u){
|
||
localStorage.setItem('f24_user',u);
|
||
document.getElementById('user-name').textContent=u;
|
||
document.getElementById('opt-till').classList.toggle('active',u==='Till');
|
||
document.getElementById('opt-lea').classList.toggle('active',u==='Lea');
|
||
closeModal('user-modal');
|
||
renderCards();
|
||
showToast('👤 Profil: '+u);
|
||
}
|
||
function switchUser(){
|
||
const cur=getCurrentUser();
|
||
document.getElementById('opt-till').classList.toggle('active',cur==='Till');
|
||
document.getElementById('opt-lea').classList.toggle('active',cur==='Lea');
|
||
document.getElementById('user-modal').classList.add('open');
|
||
}
|
||
|
||
// ============= NAV =============
|
||
function navTo(tab,btn){
|
||
document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
|
||
document.querySelectorAll('.bnav-item').forEach(b=>b.classList.remove('active'));
|
||
document.getElementById('tab-'+tab).classList.add('active');
|
||
btn.classList.add('active');
|
||
if(tab==='ranking')renderRanking();
|
||
if(tab==='eigene')renderOwn();
|
||
if(tab==='archiv')renderHistory();
|
||
}
|
||
|
||
// ============= DATA ACCESS =============
|
||
function getOwn(){return JSON.parse(localStorage.getItem('f24_own')||'[]');}
|
||
function saveOwn(arr){localStorage.setItem('f24_own',JSON.stringify(arr));}
|
||
function getRatings(){return JSON.parse(localStorage.getItem('f24_ratings')||'{}');}
|
||
function saveRatings(r){localStorage.setItem('f24_ratings',JSON.stringify(r));}
|
||
function getComments(){return JSON.parse(localStorage.getItem('f24_comments')||'{}');}
|
||
function saveComments(c){localStorage.setItem('f24_comments',JSON.stringify(c));}
|
||
|
||
function getAllDestinations(){
|
||
return [...DESTINATIONS_BUILTIN, ...getOwn()];
|
||
}
|
||
|
||
// ============= STAR RATING =============
|
||
function setRating(destId,user,stars){
|
||
const r=getRatings();
|
||
if(!r[destId])r[destId]={};
|
||
r[destId][user]=r[destId][user]===stars?0:stars;
|
||
saveRatings(r);
|
||
renderCards();
|
||
if(document.getElementById('tab-ranking').classList.contains('active'))renderRanking();
|
||
updateStats();
|
||
showToast(stars+' Sterne von '+user);
|
||
}
|
||
|
||
function getAvgRating(destId){
|
||
const r=getRatings()[destId]||{};
|
||
const vals=Object.values(r).filter(v=>v>0);
|
||
return vals.length?(vals.reduce((a,b)=>a+b,0)/vals.length):0;
|
||
}
|
||
|
||
function renderStars(destId){
|
||
const r=getRatings()[destId]||{};
|
||
const avg=getAvgRating(destId);
|
||
return `<div class="rating-section">
|
||
<div class="rating-row">
|
||
<span class="rating-who">Till</span>
|
||
<div class="stars">${[1,2,3,4,5].map(s=>`<button class="star ${(r.Till||0)>=s?'filled':''}" onclick="setRating('${destId}','Till',${s})">★</button>`).join('')}</div>
|
||
</div>
|
||
<div class="rating-row">
|
||
<span class="rating-who">Lea</span>
|
||
<div class="stars">${[1,2,3,4,5].map(s=>`<button class="star ${(r.Lea||0)>=s?'filled':''}" onclick="setRating('${destId}','Lea',${s})">★</button>`).join('')}</div>
|
||
</div>
|
||
</div>
|
||
<div class="rating-summary">
|
||
<div class="avg-num">${avg.toFixed(1)}</div>
|
||
<div class="avg-lbl">Ø Sterne</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ============= COMMENTS =============
|
||
function toggleComments(destId){
|
||
const body=document.getElementById('cm-body-'+destId);
|
||
const tog=document.getElementById('cm-tog-'+destId);
|
||
body.classList.toggle('open');
|
||
tog.classList.toggle('open');
|
||
}
|
||
|
||
function addComment(destId){
|
||
const inp=document.getElementById('cm-inp-'+destId);
|
||
const txt=inp.value.trim();
|
||
if(!txt)return;
|
||
const c=getComments();
|
||
if(!c[destId])c[destId]=[];
|
||
c[destId].push({
|
||
user:getCurrentUser(),
|
||
text:txt,
|
||
ts:Date.now()
|
||
});
|
||
saveComments(c);
|
||
inp.value='';
|
||
renderCards();
|
||
updateStats();
|
||
setTimeout(()=>{
|
||
const body=document.getElementById('cm-body-'+destId);
|
||
const tog=document.getElementById('cm-tog-'+destId);
|
||
if(body){body.classList.add('open');tog.classList.add('open');}
|
||
},50);
|
||
showToast('💬 Kommentar gepostet');
|
||
}
|
||
|
||
function deleteComment(destId,idx){
|
||
if(!confirm('Diesen Kommentar löschen?'))return;
|
||
const c=getComments();
|
||
c[destId].splice(idx,1);
|
||
saveComments(c);
|
||
renderCards();
|
||
}
|
||
|
||
function formatTime(ts){
|
||
const d=new Date(ts);
|
||
const now=new Date();
|
||
const diff=now-d;
|
||
if(diff<60000)return'gerade';
|
||
if(diff<3600000)return Math.floor(diff/60000)+' Min';
|
||
if(diff<86400000)return Math.floor(diff/3600000)+' Std';
|
||
return d.toLocaleDateString('de',{day:'2-digit',month:'short'});
|
||
}
|
||
|
||
function renderComments(destId){
|
||
const c=getComments()[destId]||[];
|
||
const cur=getCurrentUser();
|
||
return `<button class="comments-toggle" id="cm-tog-${destId}" onclick="toggleComments('${destId}')">
|
||
<span class="arr">▸</span>
|
||
💬 Kommentare
|
||
<span class="ccount">${c.length}</span>
|
||
</button>
|
||
<div class="comments-body" id="cm-body-${destId}">
|
||
${c.length===0?'<div class="comment-empty">Noch keine Kommentare. Macht den Anfang!</div>':c.map((cm,i)=>`
|
||
<div class="comment ${cm.user.toLowerCase()}">
|
||
<div class="comment-head">
|
||
<span class="comment-who">${cm.user}</span>
|
||
<div>
|
||
<span class="comment-time">${formatTime(cm.ts)}</span>
|
||
${cm.user===cur?`<button class="comment-del" onclick="deleteComment('${destId}',${i})" title="Löschen">×</button>`:''}
|
||
</div>
|
||
</div>
|
||
<div class="comment-text">${escapeHtml(cm.text)}</div>
|
||
</div>
|
||
`).join('')}
|
||
<div class="comment-input-row">
|
||
<input class="comment-input" id="cm-inp-${destId}" placeholder="Schreib was als ${cur}..." onkeydown="if(event.key==='Enter')addComment('${destId}')">
|
||
<button class="comment-btn" onclick="addComment('${destId}')">Posten</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function escapeHtml(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);}
|
||
|
||
// ============= RENDER CARDS =============
|
||
let currentFilter='ALL';
|
||
|
||
function filterStatus(status,btn){
|
||
document.querySelectorAll('#tab-ideen .filter-row .chip').forEach(p=>p.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
currentFilter=status;
|
||
renderCards();
|
||
}
|
||
|
||
function renderCards(){
|
||
const all=getAllDestinations();
|
||
const sort=document.getElementById('sort-select').value;
|
||
let filtered=all;
|
||
|
||
if(currentFilter==='OWN'){
|
||
filtered=all.filter(d=>d.isOwn);
|
||
} else if(currentFilter!=='ALL'){
|
||
filtered=all.filter(d=>d.status===currentFilter);
|
||
}
|
||
|
||
// Sort
|
||
if(sort==='ranking'){
|
||
filtered.sort((a,b)=>{
|
||
const ra=getAvgRating(a.id),rb=getAvgRating(b.id);
|
||
if(rb!==ra)return rb-ra;
|
||
return (a.price||0)-(b.price||0);
|
||
});
|
||
} else if(sort==='status'){
|
||
const order={'TOP':0,'NEW':1,'OK':2,'MAYBE':3,'SPÄTER':4,'NEIN':5};
|
||
filtered.sort((a,b)=>(order[a.status]||9)-(order[b.status]||9));
|
||
} else if(sort==='price-asc'){
|
||
filtered.sort((a,b)=>(a.price||9999)-(b.price||9999));
|
||
} else if(sort==='price-desc'){
|
||
filtered.sort((a,b)=>(b.price||0)-(a.price||0));
|
||
} else if(sort==='name'){
|
||
filtered.sort((a,b)=>a.name.localeCompare(b.name));
|
||
} else if(sort==='newest'){
|
||
filtered.sort((a,b)=>{
|
||
if(a.isOwn&&!b.isOwn)return -1;
|
||
if(!a.isOwn&&b.isOwn)return 1;
|
||
return (b.createdAt||0)-(a.createdAt||0);
|
||
});
|
||
}
|
||
|
||
const html=filtered.map((d,idx)=>renderCard(d,sort==='ranking'?idx:null)).join('');
|
||
document.getElementById('dg').innerHTML=html||'<div class="empty"><div class="empty-icon">🔍</div><div class="empty-title">Keine Treffer</div><div class="empty-sub">Filter ändern oder neue Idee einreichen.</div></div>';
|
||
|
||
updateStats();
|
||
updateOwnBadge();
|
||
}
|
||
|
||
function renderCard(d,rank){
|
||
const avg=getAvgRating(d.id);
|
||
const isOwn=d.isOwn===true;
|
||
const rankBadge=rank!==null&&avg>0?`<div class="rank-badge show ${rank===0?'top1':rank===1?'top2':rank===2?'top3':''}">#${rank+1}</div>`:'';
|
||
|
||
return `<div class="dest-card ${isOwn?'own':''}" data-status="${d.status}" data-id="${d.id}" id="card-${d.id}">
|
||
${isOwn?'<div class="own-marker">💜 Eigener Vorschlag</div>':''}
|
||
${rankBadge}
|
||
<div class="card-hdr" style="${isOwn?'padding-top:24px':''}">
|
||
<span class="dest-em">${d.emoji||'🌍'}</span>
|
||
<div class="dest-meta">
|
||
<div class="dest-name">${escapeHtml(d.name)}</div>
|
||
<div class="dest-sub">${escapeHtml(d.sub||d.country||'')}</div>
|
||
</div>
|
||
<div class="status-wrap">
|
||
<span class="sbadge s-${d.status}">${d.status}</span>
|
||
${d.isRecommendation?'<span class="sbadge s-NEW">⭐ Empfehlung</span>':''}
|
||
</div>
|
||
</div>
|
||
${d.price?`<div class="card-price">
|
||
<span class="price-main">~${d.price.toLocaleString('de')} €</span>
|
||
<span class="price-range">${escapeHtml(d.priceLabel||'')}</span>
|
||
${d.priceBadge?`<span class="price-badge ${d.priceBadgeClass||'pb-mid'}">${escapeHtml(d.priceBadge)}</span>`:''}
|
||
</div>`:''}
|
||
${d.scoreFelix!==undefined?`<div class="card-scores">
|
||
<div class="sc"><div class="sc-lbl">Felix</div><div class="sc-bar"><div class="sc-fill sc-f" style="width:${d.scoreFelix||50}%"></div></div></div>
|
||
<div class="sc"><div class="sc-lbl">Lea</div><div class="sc-bar"><div class="sc-fill sc-l" style="width:${d.scoreLea||50}%"></div></div></div>
|
||
<div class="sc"><div class="sc-lbl">Mietwagen</div><div class="sc-bar"><div class="sc-fill sc-m" style="width:${d.scoreCar||50}%"></div></div></div>
|
||
</div>`:''}
|
||
${d.flight?`<div class="card-stats">
|
||
<div class="cstat"><span class="cstat-val">${escapeHtml(d.flight)}</span><span class="cstat-key">Flug</span></div>
|
||
<div class="cstat"><span class="cstat-val">${escapeHtml(d.from||'')}</span><span class="cstat-key">Von</span></div>
|
||
<div class="cstat"><span class="cstat-val">${escapeHtml(d.climate||'')}</span><span class="cstat-key">Klima</span></div>
|
||
<div class="cstat"><span class="cstat-val">${escapeHtml(d.available||'')}</span><span class="cstat-key">Verfügbar</span></div>
|
||
</div>`:''}
|
||
${d.highlight||d.pros||d.note||d.why?`<div class="card-body">
|
||
${d.highlight?`<div class="card-highlight">${escapeHtml(d.highlight)}</div>`:''}
|
||
${d.why&&!d.highlight?`<div class="card-highlight">${escapeHtml(d.why)}</div>`:''}
|
||
${(d.pros&&d.pros.length)||(d.cons&&d.cons.length)?`<div class="pros-cons">
|
||
<ul class="pro-list">${(d.pros||[]).map(p=>`<li>${escapeHtml(p)}</li>`).join('')}</ul>
|
||
<ul class="con-list">${(d.cons||[]).map(c=>`<li>${escapeHtml(c)}</li>`).join('')}</ul>
|
||
</div>`:''}
|
||
${d.note?`<div class="card-note">${escapeHtml(d.note)}</div>`:''}
|
||
${isOwn?`<div style="display:flex;gap:6px;margin-top:8px"><button onclick="editOwn('${d.id}')" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:Outfit;color:var(--muted)">✏️ Bearbeiten</button><button onclick="deleteOwn('${d.id}')" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:Outfit;color:var(--coral)">🗑 Löschen</button></div>`:''}
|
||
</div>`:''}
|
||
<div class="rating-bar">
|
||
${renderStars(d.id)}
|
||
</div>
|
||
<div class="comments-section">
|
||
${renderComments(d.id)}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ============= RANKING =============
|
||
function renderRanking(){
|
||
const all=getAllDestinations();
|
||
const ranked=all.map(d=>({...d,avg:getAvgRating(d.id),ratingCount:Object.values(getRatings()[d.id]||{}).filter(v=>v>0).length}))
|
||
.filter(d=>d.avg>0)
|
||
.sort((a,b)=>b.avg-a.avg);
|
||
|
||
if(ranked.length===0){
|
||
document.getElementById('ranking-list').innerHTML='<div class="empty"><div class="empty-icon">⭐</div><div class="empty-title">Noch keine Bewertungen</div><div class="empty-sub">Vergebt Sterne bei den Ideen-Karten – Ranking erscheint dann automatisch.</div></div>';
|
||
return;
|
||
}
|
||
|
||
const html=ranked.map((d,i)=>{
|
||
const r=getRatings()[d.id]||{};
|
||
const tStars=r.Till?'⭐'.repeat(r.Till):'–';
|
||
const lStars=r.Lea?'⭐'.repeat(r.Lea):'–';
|
||
return`<div class="rank-row" onclick="scrollToCard('${d.id}');navTo('ideen',document.querySelector('.bnav-item'))" style="cursor:pointer">
|
||
<div class="rank-num">#${i+1}</div>
|
||
<div>
|
||
<div class="rank-name"><span class="rank-emoji">${d.emoji}</span>${escapeHtml(d.name)}</div>
|
||
<div class="rank-meta">Till: ${tStars} · Lea: ${lStars}</div>
|
||
</div>
|
||
<div class="rank-price">${d.price?'~'+d.price.toLocaleString('de')+'€':'–'}</div>
|
||
<div class="rank-score">${d.avg.toFixed(1)}<span class="rank-stars">★</span></div>
|
||
</div>`;
|
||
}).join('');
|
||
document.getElementById('ranking-list').innerHTML=html;
|
||
}
|
||
|
||
// ============= OWN IDEAS =============
|
||
function renderOwn(){
|
||
const own=getOwn();
|
||
if(own.length===0){
|
||
document.getElementById('own-list').innerHTML='<div class="empty"><div class="empty-icon">💡</div><div class="empty-title">Noch keine eigenen Ideen</div><div class="empty-sub">Tippe auf den ✚-Button rechts unten und reiche deine erste Idee ein.</div></div>';
|
||
return;
|
||
}
|
||
document.getElementById('own-list').innerHTML=own.map(d=>renderCard({...d,isOwn:true},null)).join('');
|
||
}
|
||
|
||
function updateOwnBadge(){
|
||
const own=getOwn();
|
||
const badge=document.getElementById('own-badge');
|
||
if(!badge)return;
|
||
if(own.length>0){
|
||
badge.style.display='block';
|
||
badge.textContent=own.length;
|
||
} else {
|
||
badge.style.display='none';
|
||
}
|
||
}
|
||
|
||
// ============= SUBMIT IDEA =============
|
||
function openSubmit(){
|
||
document.getElementById('edit-id').value='';
|
||
document.getElementById('submit-btn').textContent='Idee einreichen 🚀';
|
||
['s-dest','s-land','s-budget','s-why','s-sub','s-flight'].forEach(id=>document.getElementById(id).value='');
|
||
document.getElementById('s-emoji').value='🌍';
|
||
document.getElementById('submit-modal').classList.add('open');
|
||
}
|
||
|
||
function editOwn(id){
|
||
const own=getOwn();
|
||
const d=own.find(x=>x.id===id);
|
||
if(!d)return;
|
||
document.getElementById('edit-id').value=id;
|
||
document.getElementById('s-emoji').value=d.emoji||'🌍';
|
||
document.getElementById('s-dest').value=d.name||'';
|
||
document.getElementById('s-land').value=d.country||'';
|
||
document.getElementById('s-budget').value=d.price||'';
|
||
document.getElementById('s-sub').value=d.sub||'';
|
||
document.getElementById('s-why').value=d.why||d.highlight||'';
|
||
document.getElementById('s-who').value=d.submittedBy||'Till';
|
||
document.getElementById('s-status').value=d.status||'OK';
|
||
document.getElementById('s-flight').value=d.flight||'';
|
||
document.getElementById('submit-btn').textContent='Änderungen speichern 💾';
|
||
document.getElementById('submit-modal').classList.add('open');
|
||
}
|
||
|
||
function deleteOwn(id){
|
||
if(!confirm('Diese Idee wirklich löschen?'))return;
|
||
const own=getOwn().filter(d=>d.id!==id);
|
||
saveOwn(own);
|
||
renderCards();
|
||
renderOwn();
|
||
showToast('🗑 Idee gelöscht');
|
||
}
|
||
|
||
function submitIdea(){
|
||
const dest=document.getElementById('s-dest').value.trim();
|
||
if(!dest){
|
||
document.getElementById('s-dest').style.borderColor='var(--coral)';
|
||
setTimeout(()=>document.getElementById('s-dest').style.borderColor='',1500);
|
||
return;
|
||
}
|
||
const editId=document.getElementById('edit-id').value;
|
||
const own=getOwn();
|
||
const data={
|
||
id:editId||'own-'+Date.now(),
|
||
isOwn:true,
|
||
emoji:document.getElementById('s-emoji').value||'🌍',
|
||
name:dest,
|
||
country:document.getElementById('s-land').value||'',
|
||
sub:document.getElementById('s-sub').value||document.getElementById('s-land').value||'',
|
||
status:document.getElementById('s-status').value,
|
||
price:parseInt(document.getElementById('s-budget').value)||null,
|
||
priceLabel:'',
|
||
priceBadge:'',
|
||
priceBadgeClass:'pb-mid',
|
||
flight:document.getElementById('s-flight').value||'',
|
||
from:'',
|
||
climate:'',
|
||
available:'',
|
||
why:document.getElementById('s-why').value||'',
|
||
highlight:document.getElementById('s-why').value||'',
|
||
pros:[],
|
||
cons:[],
|
||
submittedBy:document.getElementById('s-who').value,
|
||
createdAt:editId?(own.find(x=>x.id===editId)||{}).createdAt||Date.now():Date.now()
|
||
};
|
||
|
||
if(editId){
|
||
const idx=own.findIndex(d=>d.id===editId);
|
||
if(idx>=0)own[idx]=data;
|
||
} else {
|
||
own.push(data);
|
||
}
|
||
saveOwn(own);
|
||
closeModal('submit-modal');
|
||
showToast(editId?'💾 Geändert!':'💡 Idee eingereicht!');
|
||
renderCards();
|
||
renderOwn();
|
||
updateOwnBadge();
|
||
}
|
||
|
||
function closeModal(id){document.getElementById(id).classList.remove('open');}
|
||
|
||
// ============= HISTORY =============
|
||
function renderHistory(){
|
||
const grouped={};
|
||
HISTORY.forEach(h=>{
|
||
if(!grouped[h.year])grouped[h.year]=[];
|
||
grouped[h.year].push(h);
|
||
});
|
||
const years=Object.keys(grouped).sort();
|
||
let html='';
|
||
years.forEach(y=>{
|
||
html+=`<div class="hist-year" data-year="${y}">${y}${y==='2023'?' – ab hier mit Felix 👶':''}</div>`;
|
||
grouped[y].forEach(h=>{
|
||
html+=`<div class="hist-item ${h.type}" data-type="${h.type}">
|
||
<span class="hist-em">${h.emoji}</span>
|
||
<div class="hist-body">
|
||
<div class="hist-dest-name">${escapeHtml(h.dest)}</div>
|
||
<div class="hist-loc">${escapeHtml(h.loc)}</div>
|
||
<div class="hist-tags">
|
||
<span class="htag htag-${h.type==='ferienhaus'?'fh':h.type}">${h.type==='hotel'?'Hotel':h.type==='cruise'?'Kreuzfahrt':h.type==='ferienhaus'?'Ferienhaus':h.type==='airbnb'?'Airbnb':'Familie'}</span>
|
||
<span class="htag" style="background:var(--bg2);color:var(--muted)">${escapeHtml(h.date)}</span>
|
||
</div>
|
||
</div>
|
||
<span class="hist-pers">${escapeHtml(h.pers)}</span>
|
||
</div>`;
|
||
});
|
||
});
|
||
// Add open slot 2026
|
||
html+=`<div class="hist-item" style="border:2px dashed var(--gold);background:var(--gold-light);" data-type="hotel">
|
||
<span class="hist-em">❓</span>
|
||
<div class="hist-body">
|
||
<div class="hist-dest-name" style="color:var(--gold)">Sommerurlaub 2026 – noch offen!</div>
|
||
<div class="hist-loc">Noch nicht gebucht · ab 13. Juli · max. 3.600 €</div>
|
||
<div class="hist-tags"><span class="htag" style="background:var(--gold);color:#fff">OFFEN</span></div>
|
||
</div>
|
||
<span class="hist-pers">Familie</span>
|
||
</div>`;
|
||
document.getElementById('hl').innerHTML=html;
|
||
}
|
||
|
||
function filterHistory(type,btn){
|
||
document.querySelectorAll('#tab-archiv .filter-row .chip').forEach(p=>p.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.querySelectorAll('#hl .hist-item').forEach(i=>{
|
||
i.style.display=(type==='ALL'||i.dataset.type===type)?'':'none';
|
||
});
|
||
document.querySelectorAll('#hl .hist-year').forEach(yr=>{
|
||
let el=yr.nextElementSibling;
|
||
let found=false;
|
||
while(el&&el.classList&&el.classList.contains('hist-item')){
|
||
if(el.style.display!=='none'){found=true;break;}
|
||
el=el.nextElementSibling;
|
||
}
|
||
yr.style.display=found?'':'none';
|
||
});
|
||
}
|
||
|
||
// ============= STATS =============
|
||
function updateStats(){
|
||
const all=getAllDestinations();
|
||
document.getElementById('stat-total').textContent=all.length;
|
||
const ratings=getRatings();
|
||
const ratedIds=Object.keys(ratings).filter(id=>Object.values(ratings[id]).some(v=>v>0));
|
||
document.getElementById('stat-rated').textContent=ratedIds.length;
|
||
const comments=getComments();
|
||
const cCount=Object.values(comments).reduce((sum,arr)=>sum+arr.length,0);
|
||
document.getElementById('stat-comments').textContent=cCount;
|
||
|
||
// Days until July 13 2026
|
||
const now=new Date();
|
||
const target=new Date('2026-07-13');
|
||
const days=Math.max(0,Math.ceil((target-now)/(1000*60*60*24)));
|
||
document.getElementById('stat-days').textContent=days>0?days:'🎉';
|
||
}
|
||
|
||
// ============= SCROLL TO CARD =============
|
||
function scrollToCard(id){
|
||
// Reset filter to ALL first
|
||
filterStatus('ALL',document.querySelector('#tab-ideen .filter-row .chip'));
|
||
setTimeout(()=>{
|
||
const card=document.getElementById('card-'+id);
|
||
if(card)card.scrollIntoView({behavior:'smooth',block:'start'});
|
||
},100);
|
||
}
|
||
|
||
// ============= EXPORT =============
|
||
function exportData(){
|
||
const data={
|
||
ratings:getRatings(),
|
||
comments:getComments(),
|
||
ownIdeas:getOwn(),
|
||
exportedAt:new Date().toISOString()
|
||
};
|
||
navigator.clipboard.writeText(JSON.stringify(data,null,2)).then(()=>{
|
||
showToast('📋 Daten in Zwischenablage kopiert');
|
||
});
|
||
}
|
||
|
||
// ============= TOAST =============
|
||
function showToast(msg){
|
||
const t=document.getElementById('toast');
|
||
t.textContent=msg;t.classList.add('show');
|
||
setTimeout(()=>t.classList.remove('show'),2200);
|
||
}
|
||
|
||
// ============= INIT =============
|
||
function init(){
|
||
document.getElementById('user-name').textContent=getCurrentUser();
|
||
renderCards();
|
||
updateStats();
|
||
updateOwnBadge();
|
||
setInterval(updateStats,60000); // Update days counter every minute
|
||
}
|
||
|
||
window.addEventListener('load',()=>{
|
||
if(sessionStorage.getItem('f24')==='1')init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|