Files
holiday-2026/public/index.html
T

538 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0ea5b7">
<title>Urlaub 2026 Familie Heidrich · Vergleich & Abstimmung</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<style>
:root{
--bg:#f7f9fc; --surf:#ffffff; --surf2:#f1f5f9; --line:#e6ebf2; --line2:#dbe3ee;
--ink:#0f172a; --ink2:#475569; --mut:#7c8aa0;
--accent:#0ea5b7; --accent-d:#0b7e8c; --hr:#e8643c; --gold:#f6b73c; --green:#16a36b;
--kro:#e8643c; --kan:#f1a23b; --mad:#19a37a; --cruise:#2f7fd6;
--shadow:0 1px 2px rgba(15,23,42,.04),0 8px 24px -12px rgba(15,23,42,.12);
--r:18px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
line-height:1.55;-webkit-text-size-adjust:100%}
a{color:var(--accent-d)}
img{max-width:100%}
.wrap{max-width:1180px;margin:0 auto;padding:0 16px}
/* ---------- Voter bar ---------- */
.voterbar{position:sticky;top:0;z-index:1000;background:rgba(255,255,255,.92);
backdrop-filter:saturate(160%) blur(10px);border-bottom:1px solid var(--line)}
.voterbar .in{max-width:1180px;margin:0 auto;padding:8px 16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.voterbar b{font-size:13px;color:var(--ink2);white-space:nowrap}
.who{display:flex;gap:6px;flex-wrap:wrap}
.who button{border:1px solid var(--line2);background:#fff;color:var(--ink2);
padding:6px 12px;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:6px}
.who button .av{width:18px;height:18px;border-radius:50%;display:inline-block}
.who button.on{color:#fff;border-color:transparent}
.who button.on.till{background:#2f7fd6}.who button.on.lea{background:#e8643c}.who button.on.astrid{background:#19a37a}
.av.till{background:#2f7fd6}.av.lea{background:#e8643c}.av.astrid{background:#19a37a}
.voterbar .sp{flex:1}
.linkbtn{font-size:12.5px;color:var(--ink2);text-decoration:none;border:1px solid var(--line2);padding:5px 10px;border-radius:999px}
/* ---------- Hero ---------- */
.hero{position:relative;border-radius:0 0 24px 24px;overflow:hidden;margin-bottom:8px}
.hero .bg{position:absolute;inset:0;background-size:cover;background-position:center;transform:scale(1.03)}
.hero .ov{position:absolute;inset:0;background:linear-gradient(180deg,rgba(8,40,55,.34),rgba(8,40,55,.66))}
.hero .ct{position:relative;max-width:1180px;margin:0 auto;padding:54px 18px 30px;color:#fff}
.hero h1{font-size:clamp(26px,5.4vw,42px);margin:0 0 8px;letter-spacing:-.02em;line-height:1.1;text-shadow:0 2px 18px rgba(0,0,0,.25)}
.hero p{margin:0;max-width:680px;font-size:clamp(14px,2.4vw,16.5px);color:#eaf3f6;text-shadow:0 1px 10px rgba(0,0,0,.3)}
.hero .facts{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}
.hero .facts span{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.25);
padding:5px 11px;border-radius:999px;font-size:12.5px;backdrop-filter:blur(4px)}
section{padding:26px 0}
h2.sec{font-size:clamp(19px,3.6vw,24px);letter-spacing:-.02em;margin:0 0 4px;display:flex;align-items:center;gap:9px}
.sub{color:var(--ink2);margin:0 0 16px;font-size:14.5px;max-width:720px}
/* ---------- Recommendation cards ---------- */
.recos{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.reco{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow);display:flex;flex-direction:column}
.reco .top{padding:16px 16px 12px;border-bottom:1px solid var(--line)}
.reco .rank{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--accent-d)}
.reco h3{margin:4px 0 2px;font-size:17.5px;letter-spacing:-.01em}
.reco .place{color:var(--mut);font-size:12.5px}
.reco .body{padding:14px 16px;display:flex;flex-direction:column;gap:10px;flex:1}
.reco p{margin:0;font-size:13.5px;color:var(--ink2)}
.pricebox{background:var(--surf2);border-radius:12px;padding:11px 12px;font-size:13px}
.pricebox .big{font-size:20px;font-weight:800;color:var(--ink);letter-spacing:-.02em}
.pricebox .li{display:flex;justify-content:space-between;gap:8px;margin-top:5px;color:var(--ink2)}
.pricebox .li b{color:var(--ink);font-weight:700}
.reco .links{display:flex;gap:8px;flex-wrap:wrap;margin-top:auto;padding-top:4px}
.btn{display:inline-block;background:var(--accent);color:#fff;text-decoration:none;font-weight:700;
font-size:12.5px;padding:9px 13px;border-radius:10px;text-align:center}
.btn.ghost{background:#fff;color:var(--accent-d);border:1px solid var(--line2)}
.btn:active{transform:scale(.97)}
/* ---------- Map ---------- */
#map{height:430px;border-radius:var(--r);border:1px solid var(--line);box-shadow:var(--shadow)}
.legend{display:flex;gap:14px;flex-wrap:wrap;margin:10px 2px 0;font-size:12.5px;color:var(--ink2)}
.legend span{display:inline-flex;align-items:center;gap:6px}
.legend i{width:11px;height:11px;border-radius:50%;display:inline-block}
/* ---------- Region + hotel cards ---------- */
.region-h{display:flex;align-items:center;gap:10px;margin:6px 0 14px}
.region-h .flag{font-size:22px}
.region-h h2{font-size:20px;margin:0;letter-spacing:-.01em}
.region-h .cnt{color:var(--mut);font-size:13px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
.card{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow);display:flex;flex-direction:column}
.card.fav{border-color:var(--gold);box-shadow:0 0 0 1.5px var(--gold) inset,var(--shadow)}
.ph{position:relative;aspect-ratio:16/10;background:linear-gradient(135deg,#dfeaf2,#eef3f8);overflow:hidden}
.ph img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s}
.ph img.on{opacity:1}
.ph .nav{position:absolute;top:0;bottom:0;width:42%;cursor:pointer;z-index:2}
.ph .nav.l{left:0}.ph .nav.r{right:0}
.ph .dots{position:absolute;bottom:8px;left:0;right:0;display:flex;gap:5px;justify-content:center;z-index:3}
.ph .dot{width:6px;height:6px;border-radius:50%;background:#ffffff90;box-shadow:0 0 2px rgba(0,0,0,.3)}
.ph .dot.on{background:#fff}
.rtag{position:absolute;top:10px;left:10px;z-index:3;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;color:#fff}
.favtag{position:absolute;top:10px;right:10px;z-index:3;background:var(--gold);color:#4a3000;font-size:10.5px;font-weight:800;padding:3px 9px;border-radius:999px}
.card .bd{padding:13px 14px 15px;display:flex;flex-direction:column;gap:9px;flex:1}
.card h3{margin:0;font-size:16px;letter-spacing:-.01em}
.loc{color:var(--mut);font-size:12.5px;margin-top:-3px}
.stars-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.crate{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--ink2)}
.crate b{background:#e7f6f4;color:#0b6f6b;padding:2px 7px;border-radius:6px;font-weight:800}
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{font-size:11.5px;background:var(--surf2);border:1px solid var(--line);color:var(--ink2);padding:3px 8px;border-radius:7px}
.why{font-size:12.5px;color:var(--ink2);display:flex;flex-direction:column;gap:3px;background:#fbfdff;border:1px solid var(--line);border-radius:10px;padding:9px 10px}
.why .ok{color:var(--green);font-weight:700}.why .no{color:var(--hr);font-weight:700}
.foot{margin-top:auto;display:flex;justify-content:space-between;align-items:center;gap:8px;padding-top:2px}
.price{font-size:12.5px;color:var(--mut)}.price b{color:var(--ink);font-size:15px}
/* ---------- Voting widget ---------- */
.vote{border-top:1px dashed var(--line2);padding-top:10px;margin-top:2px}
.vote .lead{font-size:11.5px;color:var(--mut);margin-bottom:5px}
.starpick{display:flex;gap:3px}
.starpick .s{font-size:22px;line-height:1;cursor:pointer;color:#d7dee8;transition:transform .08s}
.starpick .s.fill{color:var(--gold)}
.starpick .s:active{transform:scale(1.2)}
.voteinfo{display:flex;align-items:center;gap:8px;margin-top:7px;flex-wrap:wrap}
.avg{font-size:12.5px;color:var(--ink2)}
.avg b{color:var(--ink);font-weight:800}
.voters-mini{display:flex;gap:4px}
.vm{width:20px;height:20px;border-radius:50%;color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;position:relative}
.vm .n{position:absolute;-bottom:-2px;right:-3px;background:#fff;color:#0f172a;border-radius:6px;font-size:8.5px;padding:0 2px;border:1px solid var(--line)}
.vm.dim{opacity:.28}
/* ---------- Results ---------- */
.resultbox{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow);padding:18px}
.rank-list{display:flex;flex-direction:column;gap:8px;margin-top:8px}
.rrow{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;background:var(--surf2)}
.rrow .pos{width:26px;height:26px;border-radius:50%;background:#fff;border:1px solid var(--line2);font-weight:800;display:flex;align-items:center;justify-content:center;font-size:13px;flex:0 0 auto}
.rrow.top1{background:#fff7e6;border:1px solid #f6cf7a}
.rrow .nm{font-weight:600;font-size:14px;flex:1;min-width:0}
.rrow .nm small{display:block;color:var(--mut);font-weight:400;font-size:11.5px}
.rrow .bar{flex:1.2;height:8px;background:#e7edf4;border-radius:6px;overflow:hidden;max-width:160px}
.rrow .bar i{display:block;height:100%;background:linear-gradient(90deg,#f6b73c,#e8643c)}
.rrow .sc{font-weight:800;font-size:14px;white-space:nowrap}
.rrow .sc small{color:var(--mut);font-weight:500;font-size:11px}
.resethint{margin-top:14px;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
.reset{background:#fff;border:1px solid #f0c2c2;color:#c0392b;font-weight:700;font-size:12.5px;padding:8px 13px;border-radius:10px;cursor:pointer}
.reset:active{transform:scale(.97)}
.saved{font-size:12px;color:var(--green);opacity:0;transition:opacity .3s}
.saved.on{opacity:1}
.note{font-size:12px;color:var(--mut);margin-top:8px}
footer{padding:30px 0 50px;color:var(--mut);font-size:12px}
footer a{color:var(--ink2)}
/* ---------- Mobile ---------- */
@media(max-width:860px){ .recos{grid-template-columns:1fr} #map{height:360px} }
@media(max-width:560px){
.wrap{padding:0 12px}
.hero .ct{padding:40px 14px 24px}
.grid{grid-template-columns:1fr;gap:13px}
section{padding:20px 0}
.voterbar .in{padding:7px 12px;gap:8px}
.voterbar b{flex-basis:100%;margin-bottom:-2px}
.linkbtn{display:none}
.rrow .bar{display:none}
}
</style>
</head>
<body>
<!-- Voter bar -->
<div class="voterbar">
<div class="in">
<b>Wer bist du?</b>
<div class="who" id="who"></div>
<span class="sp"></span>
<a class="linkbtn" href="#ergebnis">🏆 Ergebnis</a>
</div>
</div>
<!-- Hero -->
<div class="hero">
<div class="bg" id="herobg"></div>
<div class="ov"></div>
<div class="ct">
<h1>Wohin im Sommer 2026? 🌅</h1>
<p>Hotels &amp; AIDA-Kreuzfahrt im Vergleich mit Fotos, Karte, Preisen und Mietwagen-Schätzung. Till, Lea und Astrid geben jeweils Sterne ab, unten seht ihr das Familien-Ergebnis.</p>
<div class="facts">
<span>👨‍👩‍👦 4 Personen (Till, Lea, Felix 3 J., Astrid)</span>
<span>📅 ~11.21. Juli 2026</span>
<span>🧳 3 große Koffer</span>
<span>🚗 max. 2 h Fahrt</span>
</div>
</div>
</div>
<div class="wrap">
<!-- 3 Empfehlungen -->
<section id="empfehlungen">
<h2 class="sec">🎯 Unsere 3 Empfehlungen</h2>
<p class="sub">Familien-gewichtet (kleiner Felix, entspannt für Astrid, kurze Wege). Preise als Orientierung für 4 Personen Hotelpreise auf Check24 mit eurer Belegung gegenprüfen.</p>
<div class="recos" id="recos"></div>
<div class="note">💡 Mietwagen für 4 Pers. + 3 große Koffer = mind. Kombi (z. B. VW Passat Variant) oder Kompakt-SUV/Van (Touran, Qashqai) kein Kleinwagen. Hochsaison Juli: Kroatien ab Split ~6080 €/Tag, Kanaren ~4560 €/Tag.</div>
</section>
<!-- Karte -->
<section id="karte">
<h2 class="sec">🗺️ Alles auf einer Karte</h2>
<p class="sub">Hotels als Punkte, die AIDA-Route als Linie. Tippe auf einen Marker für Details &amp; Link.</p>
<div id="map"></div>
<div class="legend">
<span><i style="background:var(--kro)"></i> Kroatien</span>
<span><i style="background:var(--kan)"></i> Kanaren</span>
<span><i style="background:var(--mad)"></i> Madeira</span>
<span><i style="background:var(--cruise)"></i> AIDA-Route</span>
<span>⭐ = unsere Empfehlung</span>
</div>
</section>
<!-- Regions render here -->
<div id="regions"></div>
<!-- Ergebnis -->
<section id="ergebnis">
<h2 class="sec">🏆 Familien-Ergebnis</h2>
<p class="sub">Durchschnitt aller abgegebenen Sterne. Aktualisiert sich live, sobald jemand abstimmt.</p>
<div class="resultbox">
<div class="rank-list" id="ranklist"></div>
<div class="resethint">
<span class="note">Stand wird auf dem Server gespeichert alle drei sehen dasselbe.</span>
<div style="display:flex;align-items:center;gap:10px">
<span class="saved" id="saved">✓ gespeichert</span>
<button class="reset" id="resetBtn">↺ Alle Bewertungen zurücksetzen</button>
</div>
</div>
</div>
</section>
<footer>
Fotos &amp; Hoteldaten: Check24 · Kreuzfahrt: AIDA / Ahoi-Schiff.de · Karten: OpenStreetMap.
Preise &amp; Verfügbarkeit ändern sich laufend bitte vor Buchung prüfen. Erstellt für Familie Heidrich.
</footer>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const C24 = u => u; // check24 proxy urls already absolute
const REG = {
kroatien:{flag:'🇭🇷',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'},
kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'},
madeira:{flag:'🇵🇹',name:'Madeira',color:'var(--mad)',mk:'#19a37a'},
kreuzfahrt:{flag:'🚢',name:'AIDA-Kreuzfahrt',color:'var(--cruise)',mk:'#2f7fd6'}
};
const OPTIONS = [
{id:'amadria',name:'Amadria Park Hotel Jakov',region:'kroatien',loc:'Šibenik · Norddalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:12,price:'ab 3.125 €',fav:true,geo:[43.698492,15.889556],
url:'https://urlaub.check24.de/suche/angebot?countryId=60&hotelId=12950&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Direkte Strandlage','Familienresort','Flughafen Split 37 km'],
why:[['ok','Krka-Nationalpark ~30 Min'],['ok','Kornati-Inseln per Boot ab Resort'],['ok','Shopping Split ~1 h / Šibenik 6 km'],['ok','Sehr kinderfreundlich']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8zNDAwMDAwMC8zMzYzMDAwMC8zMzYyNTkwMC8zMzYyNTg1OC9iM2EyNzQxY193LmpwZw==!07717e/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9ob3RlbGltYWdlcy5zdW5ob3RlbHMubmV0L0hvdGVsSW5mby9ob3RlbEltYWdlLmFzcHg-aWQ9MTYxODE3MjUmZnVsbD0x!cc720d/picture.jpg']},
{id:'valamar',name:'Valamar Meteor Hotel',region:'kroatien',loc:'Makarska · Mitteldalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:128,price:'ab 3.531 €',fav:false,geo:[43.29907,17.014845],
url:'https://urlaub.check24.de/suche/angebot?countryId=60&hotelId=4619&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Top Hotel Award','Direkte Strandlage','Flughafen Split 64 km'],
why:[['ok','Top bewertet, Strand + Promenade'],['ok','Inselfähren Brač/Hvar via Split'],['no','Krka ~1h45, Plitvice 3h+'],['ok','Familientauglich']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzI1LzI1MzUxOS8yNTM1MTlhX2hiX3NfMDA1LmpwZw==!a9bbf6/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy84MDAwMDAwLzc3MDAwMDAvNzY5NzYwMC83Njk3NTMxLzhkMDY0YTM3X3cuanBn!7c92d2/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL21lZGlhLmRldi5wYXhpbXVtLmNvbS9ob3RlbGltYWdlcy8yNzA0MTgvNDcuanBn!8c2535/picture.jpg']},
{id:'bluesun',name:'Bluesun Hotel Berulia',region:'kroatien',loc:'Brela · Mitteldalmatien',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:13,price:'ab 4.706 €',fav:false,geo:[43.362448,16.938868],
url:'https://urlaub.check24.de/suche/angebot?countryId=60&hotelId=1739068&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Direkte Strandlage','Traumstrand Brela','Flughafen Split 55 km'],
why:[['ok','Einer der schönsten Strände Kroatiens'],['no','Krka ~1h302h, Plitvice zu weit'],['ok','Inseln via Split'],['no','Teuerste Hotel-Option']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8zMDAwMDAwLzI4MjAwMDAvMjgxNDUwMC8yODE0NDA5LzZjYjI4NzAwX3cuanBn!774551/picture.jpg']},
{id:'morenia',name:'Morenia Beach Resort',region:'kroatien',loc:'Podaca · Mitteldalmatien',stars:4,
rate:'7,8',rlabel:'Gut',bew:48,price:'ab 3.527 €',fav:false,geo:[43.130196,17.286675],
url:'https://urlaub.check24.de/suche/angebot?hotelId=1859719&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Direkte Strandlage','Familie & Strand','Flughafen Split 92 km'],
why:[['ok','Ruhige Bucht, familienorientiert'],['no','Weit von Split & Nationalparks'],['no','Längster Flughafentransfer'],['ok','Solide Preis-Leistung']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9hcGktaW1nLmhvdGVsc3Rvbi5jb20vcmVzb3VyY2UvaG90ZWwvaW1hZ2VzLzcvMy84LzMvNy8yLzYvMy8xMDMxMzQzNTY0LmpwZw==!9db0ac/picture.jpg']},
{id:'tui',name:'TUI KIDS CLUB Taurito Princess',region:'kanaren',loc:'Taurito · Gran Canaria',stars:4,
rate:'8,0',rlabel:'Sehr gut',bew:400,price:'Preis auf Check24',fav:true,geo:[27.815366,-15.754307],
url:'https://urlaub.check24.de/suche/angebot?hotelId=3265&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Echter Kinderclub','Sommer mild ~26°C','Flughafen LPA 38 km'],
why:[['ok','Top für Felix (Betreuung & Animation)'],['ok','400 Bewertungen, sehr beliebt'],['no','Keine Kroatien-Wünsche (Insel/NP)'],['ok','Dünen & Berge für Ausflüge']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy85MDAwMDAwLzgxNjAwMDAvODE1ODAwMC84MTU3OTc5LzRjNjE3OTlkX3cuanBn!55230f/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL21lZGlhLmRldi5wYXhpbXVtLmNvbS9ob3RlbGltYWdlcy8yMjY0NTYvZDFhYzA3MGQ0OTg1Zjg3ZmI4YjM5OTA2MzAxNTMzZGQuanBn!c32a55/picture.jpg']},
{id:'iberostar',name:'Iberostar Waves Bouganville Playa',region:'kanaren',loc:'Playa de las Américas · Teneriffa',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:103,price:'Preis auf Check24',fav:false,geo:[28.074417,-16.732332],
url:'https://urlaub.check24.de/suche/angebot?hotelId=2134&extendedSearch=1&airport=HAM,HAJ&transportType=flight&days=1w&departureDate=2026-07-11&returnDate=2026-07-21&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Am Meer','Sehr kurzer Transfer','Flughafen TFS 16 km'],
why:[['ok','Nur 16 km vom Flughafen'],['ok','Teide-Nationalpark als Ausflug'],['no','Keine Boots-/Inselkultur wie HR'],['ok','Gut bewertet']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzAwLzAwMTEzNi8wMDExMzZhX2hiX3RfMDExLmpwZw==!e1aee2/picture.jpg']},
{id:'riu',name:'Hotel Riu Madeira',region:'madeira',loc:'Caniço de Baixo · Madeira',stars:4,
rate:'8,4',rlabel:'Sehr gut',bew:752,price:'Preis auf Check24',fav:false,geo:[32.645679,-16.826868],
url:'https://urlaub.check24.de/suche/angebot?countryId=88&hotelId=8761&extendedSearch=1&airport=HAM,HAJ,BRE,RLG,LBC,GWT&transportType=flight&days=1w&departureDate=2026-07-12&returnDate=2026-07-22&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Strand ~400 m','752 Bewertungen','Wander-Insel'],
why:[['ok','Sehr viele gute Bewertungen'],['no','Madeira = Wandern, kaum Sandstrand'],['no','Kein klassischer Badeurlaub für Felix'],['ok','Mild, grün, schöne Natur']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy81MDAwMDAwLzQ4MjAwMDAvNDgxMzQwMC80ODEzMzUxL2Y4ZjNkYTE3X3cuanBn!cb047b/picture.jpg',
'https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cDovL3Bob3Rvcy5ob3RlbGJlZHMuY29tL2dpYXRhL29yaWdpbmFsLzAwLzAwNTUyOS8wMDU1MjlhX2hiX2FfMDIzLmpwZw==!406b07/picture.jpg']},
{id:'dreams',name:'Dreams Madeira Resort, Spa & Marina',region:'madeira',loc:'Caniçal · Madeira',stars:5,
rate:'8,4',rlabel:'Sehr gut',bew:111,price:'Preis auf Check24',fav:false,geo:[32.742452,-16.709186],
url:'https://urlaub.check24.de/suche/angebot?countryId=88&hotelId=30324&extendedSearch=1&airport=HAM,HAJ,BRE,RLG,LBC,GWT&transportType=flight&days=1w&departureDate=2026-07-12&returnDate=2026-07-22&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['5 Sterne','Direkte Strandlage','Marina & Spa'],
why:[['ok','Hochwertiges 5★-Resort'],['no','Ruhiger Osten eher Ruhe/Paar'],['no','Madeira kein Bade-Klassiker'],['ok','Schön für Astrid']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy83MDAwMDAwLzYxNTAwMDAvNjE0MTgwMC82MTQxNzIzLzI5YmZiMjE0X3cuanBn!da02c7/picture.jpg']},
{id:'sentido',name:'Sentido Galosol',region:'madeira',loc:'Caniço de Baixo · Madeira',stars:4,
rate:null,rlabel:null,bew:216,price:'Preis auf Check24',fav:false,geo:[32.642268,-16.831963],
url:'https://urlaub.check24.de/suche/angebot?countryId=88&hotelId=2205&extendedSearch=1&airport=HAM,HAJ,BRE,RLG,LBC,GWT&transportType=flight&days=1w&departureDate=2026-07-12&returnDate=2026-07-22&roomAllocation=A-A-5&directFlight=1&hotelCategoryList=4,5&noRedirect=1',
chips:['Top Lage','216 Bewertungen','Meerblick'],
why:[['ok','Beliebte Lage, Klippenbad'],['no','Kein Sandstrand'],['no','Eher ruhig, nicht kinderfokussiert'],['ok','Gutes Preisniveau']],
imgs:['https://cdn1.urlaub.check24.de/size=625c440/di=3/nfc=200/source=aHR0cHM6Ly9pLnRyYXZlbGFwaS5jb20vbG9kZ2luZy8xMDAwMDAwLzkzMDAwMC85MjI0MDAvOTIyMzE5Lzg3YjE3ODc3X3ouanBn!d36a31/picture.jpg']},
{id:'aida',name:'AIDA Cosma · Mediterrane Schätze mit Korsika',region:'kreuzfahrt',loc:'ab/bis Mallorca · 7 Nächte · 11.18.07.',stars:0,
rate:'4,8',rlabel:'Ahoi',bew:null,price:'ab 6.477 €',fav:true,geo:[39.5696,2.6502],
url:'https://www.ahoi-schiff.de/aida/routen/mediterrane-schaetze-mit-korsika-ab-mallorca/aidacosma-2026-07-11?paxe=2',
url2:'https://aida.de/buchen/CO07260711/CLASSIC/meine-reise/anreise',
chips:['4 Pers., 2 Balkonkabinen','inkl. Flug & Vollpension','La Spezia · Rom · Korsika · Barcelona'],
why:[['ok','Null Selbstfahren ideal für Astrid'],['ok','Fixer Komplettpreis, Vollpension'],['ok','Jeden Tag ein neues Ziel'],['no','Mit Felix (3) straffer Rhythmus']],
imgs:['https://files.ahoi-schiff.de/aida-cruises/s/aidacosma.webp']}
];
// AIDA route ports (for map polyline)
const ROUTE = [
{n:'Palma de Mallorca',c:[39.5696,2.6502]},
{n:'La Spezia / Florenz',c:[44.1025,9.8200]},
{n:'Rom / Civitavecchia',c:[42.0930,11.7896]},
{n:'Ajaccio (Korsika)',c:[41.9192,8.7386]},
{n:'Barcelona',c:[41.3568,2.1597]},
{n:'Palma de Mallorca',c:[39.5696,2.6502]}
];
const RECOS = [
{rank:'Empfehlung 1 · Rundum-Kroatien',opt:'amadria',
text:'Erfüllt alle Wünsche auf einmal: Krka-Nationalpark in ~30 Min, Kornati-Inseln per Boot, Shopping in Split und ein großes, kinderfreundliches Strand-Resort. Kürzeste Wege, größtes „für alle was dabei".',
price:'ab ~5.400 €',
lines:[['Pauschal (Flug HAM/HAJ + Hotel)','~4.700 €*'],['Mietwagen Kombi/SUV (10 T)','~650 €'],['Nationalparks/Inseln','Krka, Kornati, Plitvice']],
foot:'*für 4 Pers. hochgerechnet auf Check24 mit eurer Belegung prüfen.'},
{rank:'Empfehlung 2 · Entspannt ohne Fahren',opt:'aida',
text:'AIDA Cosma ab Mallorca: Florenz, Rom, Korsika und Barcelona ohne Koffer-Schleppen und ohne Mietwagen. Fixer All-in-Preis mit Vollpension, perfekt wenn Astrid mitkommt. Mit Felix etwas straffer Tagesrhythmus.',
price:'~6.480 €',
lines:[['4 Pers., 2 Balkonkabinen','inkl. Flug'],['Vollpension an Bord','inkl.'],['Mietwagen','nicht nötig (0 €)']],
foot:'LIGHT/CLASSIC ab Hannover 6.477 € / ab HH 6.4876.677 €. Landausflüge optional extra.'},
{rank:'Empfehlung 3 · Stressfrei mit Kind',opt:'tui',
text:'TUI KIDS CLUB auf Gran Canaria: der entspannteste Familienurlaub echter Kinderclub für Felix, mildes Sommerklima, kurzer Transfer. Ohne die Kroatien-Wünsche (Nationalpark/Insel), dafür maximal unkompliziert.',
price:'~4.5005.500 €',
lines:[['Pauschal (Flug + Hotel)','auf Check24 prüfen'],['Kinderclub für Felix','inkl.'],['Mietwagen optional','~500 € / 10 T']],
foot:'Preis grob für 4 Pers. Gran Canaria & Teneriffa beide in der Auswahl unten.'}
];
const byId = id => OPTIONS.find(o=>o.id===id);
const VOTER_LABEL = {till:'Till',lea:'Lea',astrid:'Astrid'};
let me_ = localStorage.getItem('voter') || null;
let STATE = {votes:{}};
/* ---------- hero ---------- */
document.getElementById('herobg').style.backgroundImage =
"url('"+byId('bluesun').imgs[0]+"')";
/* ---------- voter selector ---------- */
const whoEl = document.getElementById('who');
function renderWho(){
whoEl.innerHTML='';
['till','lea','astrid'].forEach(v=>{
const b=document.createElement('button');
b.className=(me_===v?'on '+v:'');
b.innerHTML=`<span class="av ${v}"></span>${VOTER_LABEL[v]}`;
b.onclick=()=>{me_=v;localStorage.setItem('voter',v);renderWho();renderAll();};
whoEl.appendChild(b);
});
}
/* ---------- recommendations ---------- */
function renderRecos(){
const el=document.getElementById('recos');el.innerHTML='';
RECOS.forEach(r=>{
const o=byId(r.opt);
const links=`<a class="btn" href="${o.url}" target="_blank" rel="noopener">${o.region==='kreuzfahrt'?'Ahoi-Schiff →':'Check24 →'}</a>`
+ (o.url2?`<a class="btn ghost" href="${o.url2}" target="_blank" rel="noopener">AIDA.de →</a>`:'')
+ `<a class="btn ghost" href="#${o.id}">Details ↓</a>`;
const d=document.createElement('div');d.className='reco';
d.innerHTML=`<div class="top"><div class="rank">${r.rank}</div><h3>${o.name.replace(' · Mediterrane Schätze mit Korsika','')}</h3><div class="place">📍 ${o.loc}</div></div>
<div class="body">
<p>${r.text}</p>
<div class="pricebox"><div class="big">${r.price}</div>
${r.lines.map(l=>`<div class="li"><span>${l[0]}</span><b>${l[1]}</b></div>`).join('')}
</div>
<div class="note">${r.foot}</div>
<div class="links">${links}</div>
</div>`;
el.appendChild(d);
});
}
/* ---------- regions + cards ---------- */
function avgFor(id){
let sum=0,n=0,per={};
for(const v of ['till','lea','astrid']){
const s=STATE.votes[v]&&STATE.votes[v][id];
if(s){sum+=s;n++;per[v]=s;}
}
return {avg:n?sum/n:0,n,per};
}
function cardHTML(o){
const imgs=o.imgs.map((s,i)=>`<img src="${s}" class="${i===0?'on':''}" loading="lazy" alt="${o.name}" onerror="this.remove()">`).join('');
const dots=o.imgs.length>1?`<div class="dots">${o.imgs.map((_,i)=>`<span class="dot ${i===0?'on':''}"></span>`).join('')}</div>`:'';
const nav=o.imgs.length>1?'<div class="nav l"></div><div class="nav r"></div>':'';
const rate=o.rate?`<span class="crate"><b>${o.rate}</b> ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}</span>`:(o.bew?`<span class="crate">${o.bew} Bewertungen</span>`:'');
const linkLabel=o.region==='kreuzfahrt'?'Ahoi-Schiff →':'Auf Check24 →';
const link2=o.url2?`<a class="btn ghost" href="${o.url2}" target="_blank" rel="noopener" style="font-size:11.5px;padding:7px 10px">AIDA.de</a>`:'';
return `<div class="card ${o.fav?'fav':''}" id="${o.id}">
<div class="ph">${imgs}<span class="rtag" style="background:${REG[o.region].mk}">${REG[o.region].name}</span>${o.fav?'<span class="favtag">★ Empfehlung</span>':''}${nav}${dots}</div>
<div class="bd">
<h3>${o.name}</h3>
<div class="loc">📍 ${o.loc} · ${'★'.repeat(o.stars)||'🚢'}</div>
<div class="stars-row">${rate}</div>
<div class="chips">${o.chips.map(c=>`<span class="chip">${c}</span>`).join('')}</div>
<div class="why">${o.why.map(w=>`<div><span class="${w[0]}">${w[0]==='ok'?'✓':'✕'}</span> ${w[1]}</div>`).join('')}</div>
<div class="foot"><span class="price">${o.price.startsWith('ab')||o.price.startsWith('~')?o.price:('<span style=\"color:var(--mut)\">'+o.price+'</span>')}</span>
<span style="display:flex;gap:6px">${link2}<a class="btn" href="${o.url}" target="_blank" rel="noopener">${linkLabel}</a></span></div>
<div class="vote" data-opt="${o.id}"></div>
</div>
</div>`;
}
function renderRegions(){
const host=document.getElementById('regions');host.innerHTML='';
['kroatien','kanaren','madeira','kreuzfahrt'].forEach(rk=>{
const items=OPTIONS.filter(o=>o.region===rk);
if(!items.length)return;
const sec=document.createElement('section');
sec.innerHTML=`<div class="region-h"><span class="flag">${REG[rk].flag}</span><h2>${REG[rk].name}</h2><span class="cnt">${items.length} ${items.length>1?'Optionen':'Option'}</span></div>
<div class="grid">${items.map(cardHTML).join('')}</div>`;
host.appendChild(sec);
});
// carousels
document.querySelectorAll('.ph').forEach(ph=>{
const ims=ph.querySelectorAll('img');if(ims.length<2)return;
const dts=ph.querySelectorAll('.dot');let i=0;
const go=d=>{ims[i].classList.remove('on');dts[i]&&dts[i].classList.remove('on');
i=(i+d+ims.length)%ims.length;ims[i].classList.add('on');dts[i]&&dts[i].classList.add('on');};
const l=ph.querySelector('.nav.l'),r=ph.querySelector('.nav.r');
if(l)l.onclick=()=>go(-1);if(r)r.onclick=()=>go(1);
});
renderVotes();
}
/* ---------- voting widgets ---------- */
function renderVotes(){
document.querySelectorAll('.vote').forEach(box=>{
const id=box.dataset.opt;const {avg,n,per}=avgFor(id);
const mine=me_&&STATE.votes[me_]&&STATE.votes[me_][id]||0;
let stars='';
for(let s=1;s<=5;s++) stars+=`<span class="s ${s<=mine?'fill':''}" data-s="${s}"></span>`;
const minis=['till','lea','astrid'].map(v=>{
const sv=per[v];
return `<span class="vm ${v} ${sv?'':'dim'}" title="${VOTER_LABEL[v]}${sv?': '+sv+'★':' noch nicht'}">${VOTER_LABEL[v][0]}${sv?`<span class="n">${sv}</span>`:''}</span>`;
}).join('');
box.innerHTML=`<div class="lead">${me_?('Deine Bewertung als <b>'+VOTER_LABEL[me_]+'</b>:'):'Wähle oben, wer du bist, dann bewerten:'}</div>
<div class="starpick" ${me_?'':'style="opacity:.4;pointer-events:none"'}>${stars}</div>
<div class="voteinfo"><span class="avg">Schnitt: <b>${avg?avg.toFixed(1):''}</b>${n?` <span style="color:var(--mut)">(${n}/3)</span>`:''}</span><span class="voters-mini">${minis}</span></div>`;
box.querySelectorAll('.starpick .s').forEach(st=>{
st.onclick=()=>{const val=+st.dataset.s;const cur=mine;sendVote(id, val===cur?0:val);};
});
});
renderRanking();
}
/* ---------- ranking ---------- */
function renderRanking(){
const arr=OPTIONS.map(o=>({o,...avgFor(o.id)})).filter(x=>x.n>0).sort((a,b)=>b.avg-a.avg||b.n-a.n);
const el=document.getElementById('ranklist');
if(!arr.length){el.innerHTML='<div class="note" style="padding:6px 2px">Noch keine Bewertungen sobald Till, Lea oder Astrid Sterne vergeben, erscheint hier das Ranking.</div>';return;}
el.innerHTML=arr.map((x,idx)=>`<div class="rrow ${idx===0?'top1':''}">
<div class="pos">${idx===0?'🥇':idx===1?'🥈':idx===2?'🥉':(idx+1)}</div>
<div class="nm">${x.o.name.replace(' · Mediterrane Schätze mit Korsika','')}<small>${x.o.loc}</small></div>
<div class="bar"><i style="width:${(x.avg/5*100).toFixed(0)}%"></i></div>
<div class="sc">${x.avg.toFixed(1)}<small> ★ · ${x.n}/3</small></div>
</div>`).join('');
}
/* ---------- server sync ---------- */
async function loadState(){
try{const r=await fetch('/api/state');STATE=await r.json();}catch(e){STATE={votes:{}};}
renderVotes();
}
async function sendVote(option,stars){
if(!me_)return;
// optimistic
STATE.votes[me_]=STATE.votes[me_]||{};
if(stars===0)delete STATE.votes[me_][option];else STATE.votes[me_][option]=stars;
renderVotes();flashSaved();
try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){}
}
function flashSaved(){const s=document.getElementById('saved');s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1400);}
document.getElementById('resetBtn').onclick=async()=>{
if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return;
try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved();}catch(e){}
};
function renderAll(){renderWho();renderRecos();renderRegions();}
/* ---------- map ---------- */
function initMap(){
const map=L.map('map',{scrollWheelZoom:false}).setView([41,6],5);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{attribution:'© OpenStreetMap, © CARTO',maxZoom:19}).addTo(map);
const pts=[];
// cruise route line
L.polyline(ROUTE.map(r=>r.c),{color:'#2f7fd6',weight:3,opacity:.75,dashArray:'6 6'}).addTo(map);
ROUTE.slice(0,5).forEach(r=>{
L.circleMarker(r.c,{radius:4,color:'#2f7fd6',fillColor:'#2f7fd6',fillOpacity:.9,weight:1})
.addTo(map).bindTooltip(r.n,{direction:'top'});
pts.push(r.c);
});
OPTIONS.forEach(o=>{
const col=REG[o.region].mk;
const icon=L.divIcon({className:'',html:`<div style="width:${o.fav?20:15}px;height:${o.fav?20:15}px;border-radius:50%;background:${col};border:3px solid ${o.fav?'#f6b73c':'#fff'};box-shadow:0 1px 4px rgba(0,0,0,.4)"></div>`,iconSize:[20,20],iconAnchor:[10,10]});
const m=L.marker(o.geo,{icon}).addTo(map);
const lbl=o.region==='kreuzfahrt'?'Zur Route →':'Zum Angebot →';
m.bindPopup(`<div style="min-width:170px"><b>${o.name}</b><br>${o.loc}<br>${o.rate?('⭐ '+o.rate+' '+o.rlabel+'<br>'):''}<a href="${o.url}" target="_blank">${lbl}</a></div>`);
pts.push(o.geo);
});
map.fitBounds(pts,{padding:[35,35],maxZoom:6});
setTimeout(()=>map.invalidateSize(),300);
}
renderAll();
initMap();
loadState();
setInterval(loadState, 12000); // live-ish sync between the three voters