Files

297 lines
22 KiB
JavaScript
Raw Permalink 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.
const IMG = u => '/img?u=' + encodeURIComponent(u); // route images through same-origin proxy
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('"+IMG(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="${IMG(s)}" class="${i===0?'on':''}" loading="lazy" alt="${o.name}" onerror="this.closest('.ph')&&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(msg){const s=document.getElementById('saved');s.textContent=msg||'✓ gespeichert';s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1800);}
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('✓ Alle Bewertungen gelöscht');}
catch(e){alert('Zurücksetzen fehlgeschlagen bitte nochmal versuchen.');}
};
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