- 6 nivells de dificultat (flashcards, tria, uneix, escriu, mapa, mapa cec) - Registre de jugadors sense contrasenya (nom + emoji avatar) - Backend Node.js + Express + PostgreSQL (pg) - Mapa SVG interactiu amb dades GeoJSON reals (ICGC) - Filtre de comarques per jugador (muntanya, BCN, GI, LL, T, totes) - Estadistiques per nivell guardades a PostgreSQL - Panel d'administrador amb PIN - Manual integrat per a nens de 10-12 anys - Mode offline (fallback sense backend) - Deploy: Docker + Nginx + Let's Encrypt a Oracle Cloud ARM
1463 lines
79 KiB
HTML
1463 lines
79 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ca">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Les Comarques de Muntanya 🏔️</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@400;600;700;800&display=swap');
|
||
:root {
|
||
--orange:#F4A535; --dark-orange:#C97D10; --green:#4DBD6E;
|
||
--red:#F05C5C; --blue:#5B9CF4; --purple:#A87FE8; --text:#2A2A2A;
|
||
--shadow:0 4px 18px rgba(0,0,0,.13);
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0;}
|
||
body{font-family:'Lexend','Arial Rounded MT Bold',Arial,sans-serif;
|
||
background:linear-gradient(160deg,#FFF3DC 0%,#FFE9B8 100%);
|
||
min-height:100vh;color:var(--text);font-size:20px;letter-spacing:.03em;line-height:1.6;}
|
||
|
||
/* ── Header ── */
|
||
header{background:var(--orange);padding:13px 18px;display:flex;align-items:center;
|
||
justify-content:space-between;box-shadow:var(--shadow);position:sticky;top:0;z-index:50;}
|
||
header h1{font-size:1.25rem;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px;}
|
||
.hdr-right{display:flex;align-items:center;gap:12px;}
|
||
#header-stars{font-size:1.1rem;color:#fff;font-weight:700;}
|
||
#hdr-player{font-size:1.5rem;cursor:pointer;line-height:1;
|
||
background:rgba(255,255,255,.2);border-radius:50%;width:36px;height:36px;
|
||
display:flex;align-items:center;justify-content:center;}
|
||
#hdr-player:hover{background:rgba(255,255,255,.35);}
|
||
|
||
/* ── Screens ── */
|
||
.screen{display:none;padding:18px 14px;max-width:860px;margin:0 auto;}
|
||
.screen.active{display:block;}
|
||
|
||
/* ── WELCOME ── */
|
||
.welcome-title{text-align:center;font-size:1.7rem;font-weight:800;
|
||
color:var(--dark-orange);margin:18px 0 6px;}
|
||
.welcome-sub{text-align:center;font-size:.95rem;color:#888;margin-bottom:22px;}
|
||
.players-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:14px;margin-bottom:24px;}
|
||
.player-card{background:#fff;border-radius:18px;padding:18px 12px;text-align:center;
|
||
cursor:pointer;box-shadow:var(--shadow);border:3px solid transparent;
|
||
transition:transform .15s,border-color .15s;}
|
||
.player-card:hover{transform:translateY(-4px);border-color:var(--orange);}
|
||
.player-card:active{transform:scale(.96);}
|
||
.player-card .p-avatar{font-size:3rem;display:block;margin-bottom:6px;}
|
||
.player-card .p-name{font-weight:700;font-size:.95rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||
.player-card .p-stars{font-size:.8rem;color:#aaa;margin-top:2px;}
|
||
.welcome-actions{display:flex;flex-direction:column;gap:10px;align-items:center;}
|
||
.admin-link{font-size:.8rem;color:#bbb;cursor:pointer;margin-top:8px;text-decoration:underline;}
|
||
.admin-link:hover{color:#888;}
|
||
|
||
/* ── REGISTER ── */
|
||
.reg-title{font-size:1.5rem;font-weight:800;color:var(--dark-orange);margin-bottom:18px;text-align:center;}
|
||
.avatar-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:14px 0 20px;}
|
||
.avatar-btn{font-size:2.2rem;background:#fff;border:3px solid #ddd;border-radius:14px;
|
||
padding:10px;cursor:pointer;transition:border-color .15s,transform .1s;aspect-ratio:1;
|
||
display:flex;align-items:center;justify-content:center;}
|
||
.avatar-btn:hover{border-color:var(--orange);transform:scale(1.06);}
|
||
.avatar-btn.selected{border-color:var(--orange);background:#FFF3DC;transform:scale(1.1);}
|
||
|
||
/* ── HOME ── */
|
||
.home-title{text-align:center;font-size:1.8rem;font-weight:800;color:var(--dark-orange);margin:16px 0 4px;}
|
||
.home-sub{text-align:center;font-size:.9rem;color:#888;margin-bottom:22px;}
|
||
.level-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px;}
|
||
.level-card{background:#fff;border-radius:18px;padding:18px 14px;text-align:center;
|
||
cursor:pointer;box-shadow:var(--shadow);border:3px solid transparent;
|
||
transition:transform .15s,border-color .15s;}
|
||
.level-card:hover{transform:translateY(-3px);border-color:var(--orange);}
|
||
.level-card:active{transform:scale(.97);}
|
||
.level-card .emoji{font-size:2.3rem;display:block;margin-bottom:7px;}
|
||
.level-card .lvl-name{font-weight:700;font-size:.92rem;}
|
||
.level-card .lvl-desc{font-size:.75rem;color:#999;margin-top:3px;}
|
||
.map-card{grid-column:span 2;background:linear-gradient(135deg,#1A5C38,#2E9A5C);color:#fff;}
|
||
.map-card .lvl-name,.map-card .lvl-desc{color:rgba(255,255,255,.92);}
|
||
.map-card:hover{border-color:#fff;}
|
||
.home-actions{display:flex;gap:10px;justify-content:center;margin-top:18px;flex-wrap:wrap;}
|
||
|
||
/* ── STATS ── */
|
||
.stats-hero{background:var(--orange);border-radius:20px;padding:22px;text-align:center;color:#fff;margin-bottom:18px;}
|
||
.stats-avatar{font-size:4rem;display:block;margin-bottom:6px;}
|
||
.stats-name{font-size:1.5rem;font-weight:800;}
|
||
.stats-stars{font-size:1.1rem;margin-top:4px;opacity:.9;}
|
||
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:18px;}
|
||
.stat-box{background:#fff;border-radius:14px;padding:16px;text-align:center;box-shadow:var(--shadow);}
|
||
.stat-box .stat-val{font-size:2rem;font-weight:800;color:var(--dark-orange);}
|
||
.stat-box .stat-lbl{font-size:.78rem;color:#aaa;margin-top:2px;}
|
||
.level-bars{background:#fff;border-radius:14px;padding:18px;box-shadow:var(--shadow);margin-bottom:18px;}
|
||
.level-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;}
|
||
.level-bar-row:last-child{margin-bottom:0;}
|
||
.lbr-icon{font-size:1.3rem;width:28px;flex-shrink:0;text-align:center;}
|
||
.lbr-name{font-size:.82rem;font-weight:700;width:70px;flex-shrink:0;color:#666;}
|
||
.lbr-track{flex:1;background:#eee;border-radius:50px;height:14px;overflow:hidden;}
|
||
.lbr-fill{height:100%;border-radius:50px;transition:width .6s ease;}
|
||
.lbr-pct{font-size:.8rem;font-weight:700;width:36px;text-align:right;flex-shrink:0;}
|
||
.recent-list{background:#fff;border-radius:14px;padding:16px;box-shadow:var(--shadow);}
|
||
.recent-title{font-weight:700;font-size:.85rem;color:#aaa;letter-spacing:.1em;margin-bottom:12px;}
|
||
.recent-item{display:flex;align-items:center;gap:10px;padding:8px 0;
|
||
border-bottom:1px solid #f0f0f0;}
|
||
.recent-item:last-child{border-bottom:none;}
|
||
.ri-lvl{font-size:1.3rem;width:28px;text-align:center;}
|
||
.ri-score{flex:1;font-size:.9rem;font-weight:600;}
|
||
.ri-stars{font-size:.9rem;}
|
||
.ri-date{font-size:.72rem;color:#bbb;}
|
||
|
||
/* ── ADMIN ── */
|
||
.admin-pin-wrap{max-width:320px;margin:0 auto 24px;}
|
||
/* ── Manual ── */
|
||
#screen-help{padding-bottom:40px;}
|
||
.help-hero{text-align:center;padding:20px 0 10px;font-size:3.5rem;line-height:1;}
|
||
.help-hero-title{font-size:1.6rem;font-weight:900;color:var(--green);margin:6px 0 4px;}
|
||
.help-hero-sub{font-size:.95rem;color:#666;margin-bottom:20px;}
|
||
.help-section{margin:18px 0;}
|
||
.help-section-title{font-size:1.05rem;font-weight:800;color:#fff;background:var(--orange);
|
||
border-radius:12px;padding:8px 16px;margin-bottom:10px;display:flex;align-items:center;gap:8px;}
|
||
.help-card{background:#fff;border-radius:16px;padding:16px;margin-bottom:10px;
|
||
box-shadow:0 2px 10px rgba(0,0,0,.07);border-left:5px solid var(--orange);display:flex;gap:14px;align-items:flex-start;}
|
||
.help-card.green{border-left-color:var(--green);}
|
||
.help-card.blue{border-left-color:#1a6bb5;}
|
||
.help-card.purple{border-left-color:#6a1e8a;}
|
||
.help-card.red{border-left-color:#e03030;}
|
||
.help-icon{font-size:2.4rem;flex-shrink:0;line-height:1;margin-top:2px;}
|
||
.help-text strong{display:block;font-size:.95rem;margin-bottom:3px;}
|
||
.help-text p{font-size:.85rem;color:#555;margin:0;line-height:1.5;}
|
||
.help-tip{background:linear-gradient(135deg,#fffbe6,#fff3cc);border-radius:14px;
|
||
padding:14px 16px;margin:10px 0;font-size:.88rem;color:#7a5a00;border:1.5px dashed #f0c040;}
|
||
.help-tip .tip-icon{font-size:1.5rem;display:block;margin-bottom:4px;}
|
||
.help-stars-demo{display:flex;gap:16px;justify-content:center;margin:10px 0;flex-wrap:wrap;}
|
||
.help-star-item{text-align:center;background:#fff;border-radius:14px;padding:12px 16px;
|
||
box-shadow:0 2px 8px rgba(0,0,0,.08);min-width:80px;}
|
||
.help-star-item .stars{font-size:1.3rem;}
|
||
.help-star-item .star-desc{font-size:.75rem;color:#888;margin-top:4px;}
|
||
.help-comarca-demo{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:10px 0;}
|
||
.help-comarca-pill{background:#f0f4ff;border-radius:50px;padding:5px 13px;font-size:.8rem;
|
||
font-weight:600;color:#2255aa;border:1.5px solid #c0d0ff;}
|
||
/* ── Fi Manual ── */
|
||
.group-filter-grid{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0;}
|
||
.group-chip{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:50px;border:2px solid #ddd;cursor:pointer;font-size:.9rem;font-weight:600;transition:all .2s;user-select:none;background:#fff;}
|
||
.group-chip.active{border-color:var(--orange);background:var(--orange);color:#fff;}
|
||
.group-chip input{display:none;}
|
||
.filter-preview{font-size:.82rem;color:#888;margin-top:6px;min-height:20px;}
|
||
.filter-save-btn{margin-top:14px;}
|
||
.pin-input{width:100%;font-size:2rem;letter-spacing:.5em;text-align:center;
|
||
padding:14px;border-radius:14px;border:3px solid #ddd;font-family:inherit;
|
||
font-weight:700;outline:none;transition:border-color .2s;}
|
||
.pin-input:focus{border-color:var(--orange);}
|
||
.admin-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
|
||
table.admin-tbl{width:100%;border-collapse:collapse;font-size:.85rem;}
|
||
table.admin-tbl th{background:var(--orange);color:#fff;padding:10px 8px;
|
||
text-align:center;font-weight:700;white-space:nowrap;}
|
||
table.admin-tbl th:first-child{text-align:left;border-radius:10px 0 0 0;}
|
||
table.admin-tbl th:last-child{border-radius:0 10px 0 0;}
|
||
table.admin-tbl td{padding:10px 8px;text-align:center;border-bottom:1px solid #f0f0f0;}
|
||
table.admin-tbl td:first-child{text-align:left;}
|
||
table.admin-tbl tr:nth-child(even) td{background:#FAFAFA;}
|
||
.pct-cell{border-radius:8px;padding:4px 8px;font-weight:700;font-size:.8rem;}
|
||
.pct-green{background:#D4F7DF;color:#1a7a3c;}
|
||
.pct-orange{background:#FFF3DC;color:#C97D10;}
|
||
.pct-red{background:#FFE0E0;color:#a02020;}
|
||
.pct-gray{background:#F0F0F0;color:#aaa;}
|
||
|
||
/* ── MAP ── */
|
||
#map-screen{padding-bottom:30px;}
|
||
.map-mode-tabs{display:flex;gap:8px;margin-bottom:14px;}
|
||
.map-tab{flex:1;padding:10px;border-radius:11px;font-family:inherit;font-size:.85rem;
|
||
font-weight:700;border:2px solid #ddd;background:#fff;cursor:pointer;transition:all .15s;}
|
||
.map-tab.active{background:var(--green);color:#fff;border-color:var(--green);}
|
||
.map-q-box{background:#fff;border-radius:15px;padding:14px 18px;
|
||
box-shadow:var(--shadow);margin-bottom:14px;text-align:center;}
|
||
#map-svg-container{width:100%;background:#D8EEF8;border-radius:16px;
|
||
overflow:hidden;box-shadow:var(--shadow);}
|
||
#map-svg{width:100%;height:auto;display:block;cursor:pointer;}
|
||
.comarca-bg{fill:#C5DEB0;stroke:#a8c890;stroke-width:.8;transition:fill .2s;}
|
||
.comarca-mountain{stroke:#fff;stroke-width:1.4;cursor:pointer;
|
||
transition:fill .25s,filter .2s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.18));}
|
||
.comarca-mountain:hover{filter:drop-shadow(0 2px 8px rgba(0,0,0,.35)) brightness(1.08);}
|
||
.comarca-mountain.wrong-shake{animation:wShake .4s ease;}
|
||
@keyframes wShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
|
||
.comarca-label{font-family:'Lexend',Arial,sans-serif;font-size:8.5px;font-weight:700;
|
||
fill:#fff;text-anchor:middle;dominant-baseline:middle;pointer-events:none;
|
||
paint-order:stroke fill;stroke:rgba(0,0,0,.45);stroke-width:2.5px;}
|
||
.comarca-label.small{font-size:6.5px;}
|
||
.capital-dot{fill:#fff;stroke:rgba(0,0,0,.4);stroke-width:.8;pointer-events:none;}
|
||
.map-score-row{display:flex;justify-content:space-between;font-size:.83rem;color:#aaa;margin-bottom:8px;}
|
||
.map-score-row span:last-child{font-weight:700;color:var(--green);}
|
||
|
||
/* ── Shared ── */
|
||
.card{background:#fff;border-radius:18px;padding:22px 18px;box-shadow:var(--shadow);margin-bottom:16px;}
|
||
.prog-wrap{background:#ddd;border-radius:50px;height:12px;margin:8px 0 14px;overflow:hidden;}
|
||
.prog-bar{background:linear-gradient(90deg,var(--orange),#f4c535);height:100%;border-radius:50px;transition:width .4s ease;}
|
||
.q-label{font-size:.75rem;font-weight:700;color:#ccc;letter-spacing:.12em;margin-bottom:3px;}
|
||
.q-text{font-size:1.5rem;font-weight:800;color:var(--dark-orange);line-height:1.2;}
|
||
.options-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-top:16px;}
|
||
.opt-btn{padding:14px 11px;border-radius:13px;font-family:inherit;font-size:.95rem;
|
||
font-weight:600;border:3px solid #ddd;background:#fff;cursor:pointer;
|
||
transition:background .1s,border-color .1s;min-height:54px;line-height:1.3;}
|
||
.opt-btn:hover:not(:disabled){border-color:var(--orange);background:#FFF3DC;}
|
||
.opt-btn.correct{background:#D4F7DF;border-color:var(--green);color:#1a7a3c;}
|
||
.opt-btn.wrong{background:#FFE0E0;border-color:var(--red);color:#a02020;}
|
||
.fc-wrap{perspective:900px;width:100%;height:210px;cursor:pointer;margin:18px auto;}
|
||
.fc-inner{width:100%;height:100%;transform-style:preserve-3d;transition:transform .5s;position:relative;}
|
||
.fc-inner.flipped{transform:rotateY(180deg);}
|
||
.fc-face{position:absolute;inset:0;border-radius:20px;display:flex;flex-direction:column;
|
||
align-items:center;justify-content:center;backface-visibility:hidden;padding:22px;}
|
||
.fc-front{background:var(--orange);color:#fff;}
|
||
.fc-back{background:var(--blue);color:#fff;transform:rotateY(180deg);}
|
||
.fc-lbl{font-size:.75rem;opacity:.7;font-weight:600;margin-bottom:4px;}
|
||
.fc-main{font-size:1.8rem;font-weight:800;text-align:center;}
|
||
.fc-hint{text-align:center;color:#bbb;font-size:.82rem;margin-top:5px;}
|
||
.match-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:12px;}
|
||
.m-item{padding:12px 13px;border-radius:11px;background:#fff;border:3px solid #ddd;
|
||
cursor:pointer;font-weight:600;font-size:.9rem;text-align:center;
|
||
transition:border-color .15s,background .15s;min-height:48px;
|
||
display:flex;align-items:center;justify-content:center;}
|
||
.m-item:hover:not(.matched){border-color:var(--orange);background:#FFF3DC;}
|
||
.m-item.selected{border-color:var(--purple);background:#F0E8FF;}
|
||
.m-item.matched{border-color:var(--green);background:#D4F7DF;color:#1a7a3c;cursor:default;}
|
||
.m-item.bad{animation:badFlash .4s;}
|
||
@keyframes badFlash{0%,100%{border-color:#ddd;background:#fff;}50%{border-color:var(--red);background:#FFE0E0;}}
|
||
.btn{display:inline-block;padding:12px 26px;border-radius:50px;font-family:inherit;
|
||
font-size:.95rem;font-weight:700;border:none;cursor:pointer;transition:transform .12s,box-shadow .12s;}
|
||
.btn:active{transform:scale(.96);}
|
||
.btn-primary{background:var(--orange);color:#fff;box-shadow:0 4px 0 var(--dark-orange);}
|
||
.btn-primary:hover{box-shadow:0 6px 0 var(--dark-orange);transform:translateY(-2px);}
|
||
.btn-green{background:var(--green);color:#fff;box-shadow:0 4px 0 #2d8a4e;}
|
||
.btn-back{background:#eee;color:#666;padding:9px 16px;font-size:.88rem;margin-bottom:14px;}
|
||
.info-box{background:linear-gradient(135deg,#FFF3DC,#FFE8B2);border-radius:14px;
|
||
padding:14px 16px;margin-top:14px;border-left:5px solid var(--orange);}
|
||
.info-box p{font-size:.82rem;color:#777;}
|
||
.result-emoji{font-size:4.2rem;text-align:center;display:block;margin:12px 0;}
|
||
.result-title{font-size:1.7rem;font-weight:800;text-align:center;color:var(--dark-orange);}
|
||
.result-score{text-align:center;font-size:1.1rem;margin:9px 0 20px;color:#777;}
|
||
.stars-earned{text-align:center;font-size:2.2rem;letter-spacing:4px;margin-bottom:18px;}
|
||
.mtn-deco{text-align:center;font-size:2.6rem;margin:10px 0;animation:float 3s ease-in-out infinite;}
|
||
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
|
||
#feedback{position:fixed;inset:0;display:flex;flex-direction:column;
|
||
align-items:center;justify-content:center;font-size:4.2rem;
|
||
pointer-events:none;z-index:999;opacity:0;transition:opacity .28s;}
|
||
#feedback.show{opacity:1;}
|
||
#feedback .fb-msg{font-size:1.6rem;font-weight:800;margin-top:10px;text-shadow:0 2px 8px rgba(0,0,0,.2);}
|
||
#confetti-canvas{position:fixed;inset:0;pointer-events:none;z-index:998;}
|
||
@media(max-width:520px){
|
||
.level-grid,.options-grid,.match-grid,.stats-grid{grid-template-columns:1fr;}
|
||
.map-card{grid-column:span 1;}
|
||
.players-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));}
|
||
.avatar-grid{grid-template-columns:repeat(4,1fr);}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="confetti-canvas"></canvas>
|
||
|
||
<header>
|
||
<h1>🏔️ Comarques</h1>
|
||
<div class="hdr-right">
|
||
<div id="header-stars">⭐ 0</div>
|
||
<div id="hdr-player" title="Les meves estadístiques" onclick="goToStats()" style="display:none;">❓</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="feedback">
|
||
<span id="fb-emoji"></span>
|
||
<span class="fb-msg" id="fb-msg"></span>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ WELCOME ═══════════════════ -->
|
||
<div id="screen-welcome" class="screen">
|
||
<div class="mtn-deco">🏔️⛰️🗻</div>
|
||
<div class="welcome-title">Qui juga avui?</div>
|
||
<div class="welcome-sub">Toca el teu nom per entrar 👇</div>
|
||
<div class="players-grid" id="players-grid"></div>
|
||
<div class="welcome-actions">
|
||
<button class="btn btn-primary" onclick="showScreen('screen-register')">➕ Nou jugador</button>
|
||
<button class="btn" onclick="showScreen('screen-help')" style="background:#1a6bb5;color:#fff;">📖 Com es juga?</button>
|
||
<div class="admin-link" onclick="showScreen('screen-admin')">⚙️ Accés administrador</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ REGISTER ═══════════════════ -->
|
||
<div id="screen-register" class="screen">
|
||
<button class="btn btn-back" onclick="showScreen('screen-welcome')">← Tornar</button>
|
||
<div class="card">
|
||
<div class="reg-title">Crea el teu perfil! 🎉</div>
|
||
<div class="q-label">COM ET DIUS?</div>
|
||
<input type="text" id="reg-name" maxlength="20" placeholder="El teu nom..."
|
||
style="width:100%;padding:13px 16px;border-radius:12px;border:3px solid #ddd;
|
||
font-family:inherit;font-size:1.2rem;font-weight:700;outline:none;
|
||
margin-top:6px;transition:border-color .2s;"
|
||
oninput="validateReg()">
|
||
<div id="reg-name-err" style="color:var(--red);font-size:.8rem;min-height:20px;margin-top:4px;"></div>
|
||
|
||
<div class="q-label" style="margin-top:16px;">TRIA EL TEU AVATAR</div>
|
||
<div class="avatar-grid" id="avatar-grid"></div>
|
||
|
||
<button class="btn btn-primary" id="reg-btn" onclick="registerPlayer()" disabled
|
||
style="width:100%;margin-top:6px;">
|
||
Jugar! 🚀
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ HOME ═══════════════════ -->
|
||
<div id="screen-home" class="screen">
|
||
<div class="mtn-deco">🏔️⛰️🗻</div>
|
||
<div class="home-title" id="home-greeting">Aprèn les Comarques!</div>
|
||
<div class="home-sub">Tria un nivell per començar 👇</div>
|
||
<div class="level-grid">
|
||
<div class="level-card" onclick="startLevel(1)">
|
||
<span class="emoji">👀</span>
|
||
<div class="lvl-name">Nivell 1 · Descobreix</div>
|
||
<div class="lvl-desc">Targetes comarca i capital</div>
|
||
</div>
|
||
<div class="level-card" onclick="startLevel(2)">
|
||
<span class="emoji">🧩</span>
|
||
<div class="lvl-name">Nivell 2 · Tria</div>
|
||
<div class="lvl-desc">Tria la capital correcta</div>
|
||
</div>
|
||
<div class="level-card" onclick="startLevel(3)">
|
||
<span class="emoji">🔗</span>
|
||
<div class="lvl-name">Nivell 3 · Uneix</div>
|
||
<div class="lvl-desc">Uneix comarca amb capital</div>
|
||
</div>
|
||
<div class="level-card" onclick="startLevel(4)">
|
||
<span class="emoji">✏️</span>
|
||
<div class="lvl-name">Nivell 4 · Escriu</div>
|
||
<div class="lvl-desc">Escriu la capital de memòria</div>
|
||
</div>
|
||
<div class="level-card map-card" onclick="startLevel(5)" style="grid-column:span 1">
|
||
<span class="emoji">🗺️</span>
|
||
<div class="lvl-name">Nivell 5 · El Gran Mapa</div>
|
||
<div class="lvl-desc">Toca la comarca al mapa real!</div>
|
||
</div>
|
||
<div class="level-card map-card" onclick="startLevel(6)" style="grid-column:span 1;background:linear-gradient(135deg,#2c1654,#6a1e8a);">
|
||
<span class="emoji">🔮</span>
|
||
<div class="lvl-name">Nivell 6 · Mapa Cec</div>
|
||
<div class="lvl-desc">Sense noms al mapa. El repte màxim!</div>
|
||
</div>
|
||
</div>
|
||
<div class="home-actions">
|
||
<button class="btn btn-green" onclick="goToStats()" id="btn-stats" style="display:none;">
|
||
📊 Les meves estadístiques
|
||
</button>
|
||
<button class="btn" onclick="goToFilter()" id="btn-filter" style="display:none;background:#6a1e8a;color:#fff;">
|
||
⚙️ Les meves comarques
|
||
</button>
|
||
<button class="btn btn-back" onclick="goWelcome()" id="btn-change" style="display:none;">
|
||
🔄 Canviar jugador
|
||
</button>
|
||
</div>
|
||
<div class="info-box">
|
||
<p>💡 <strong>Consell:</strong> Comença pel Nivell 1. Cada resposta correcta = una ⭐</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ LEVEL 1 ═══════════════════ -->
|
||
<div id="screen-l1" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label">NIVELL 1 · DESCOBREIX</div>
|
||
<div style="font-size:.95rem;font-weight:600;margin-bottom:4px;">Toca la targeta per veure la capital 👆</div>
|
||
<div class="prog-wrap"><div class="prog-bar" id="l1-prog"></div></div>
|
||
<span id="l1-ctr" style="color:#ccc;font-size:.82rem;"></span>
|
||
<div class="fc-wrap" onclick="flipCard()">
|
||
<div class="fc-inner" id="fc-inner">
|
||
<div class="fc-face fc-front">
|
||
<div class="fc-lbl">COMARCA</div>
|
||
<div class="fc-main" id="fc-comarca">—</div>
|
||
</div>
|
||
<div class="fc-face fc-back">
|
||
<div class="fc-lbl">CAPITAL</div>
|
||
<div class="fc-main" id="fc-capital">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="fc-hint" id="fc-hint">Toca per veure la capital</div>
|
||
<div style="display:flex;gap:10px;justify-content:center;margin-top:16px;flex-wrap:wrap;">
|
||
<button class="btn btn-back" onclick="prevCard()">⬅ Anterior</button>
|
||
<button class="btn btn-primary" onclick="nextCard()">Següent ➡</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ LEVEL 2 ═══════════════════ -->
|
||
<div id="screen-l2" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label">NIVELL 2 · TRIA LA CAPITAL</div>
|
||
<div class="prog-wrap"><div class="prog-bar" id="l2-prog"></div></div>
|
||
<span id="l2-ctr" style="color:#ccc;font-size:.82rem;"></span>
|
||
<div style="margin:14px 0;">
|
||
<div class="q-label">La capital de...</div>
|
||
<div class="q-text" id="l2-q">—</div>
|
||
</div>
|
||
<div class="options-grid" id="l2-opts"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ LEVEL 3 ═══════════════════ -->
|
||
<div id="screen-l3" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label">NIVELL 3 · UNEIX COMARCA ↔ CAPITAL</div>
|
||
<div class="prog-wrap"><div class="prog-bar" id="l3-prog"></div></div>
|
||
<div style="display:flex;justify-content:space-between;font-size:.78rem;color:#ccc;margin-bottom:6px;">
|
||
<span>Comarques ←</span><span>→ Capitals</span>
|
||
</div>
|
||
<div class="match-grid" id="l3-grid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ LEVEL 4 ═══════════════════ -->
|
||
<div id="screen-l4" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label">NIVELL 4 · ESCRIU LA CAPITAL</div>
|
||
<div class="prog-wrap"><div class="prog-bar" id="l4-prog"></div></div>
|
||
<span id="l4-ctr" style="color:#ccc;font-size:.82rem;"></span>
|
||
<div style="margin:14px 0;">
|
||
<div class="q-label">Quina és la capital de...</div>
|
||
<div class="q-text" id="l4-q">—</div>
|
||
</div>
|
||
<input type="text" id="l4-input" placeholder="Escriu aquí..."
|
||
style="width:100%;padding:13px 16px;border-radius:11px;border:3px solid #ddd;
|
||
font-family:inherit;font-size:1.15rem;font-weight:600;outline:none;transition:border-color .2s;"
|
||
onkeydown="if(event.key==='Enter')checkL4()">
|
||
<div id="l4-fb" style="min-height:24px;font-size:.9rem;font-weight:700;margin-top:9px;"></div>
|
||
<div style="display:flex;gap:9px;flex-wrap:wrap;margin-top:12px;">
|
||
<button class="btn btn-primary" onclick="checkL4()">Comprovar ✅</button>
|
||
<button class="btn btn-back" onclick="skipL4()">Saltar ⏭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ MAP ═══════════════════ -->
|
||
<div id="map-screen" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div class="map-mode-tabs">
|
||
<button class="map-tab active" id="tab-name" onclick="setMapMode('name')">🏷️ Troba pel nom</button>
|
||
<button class="map-tab" id="tab-cap" onclick="setMapMode('capital')">📍 Troba per la capital</button>
|
||
</div>
|
||
<div class="map-q-box">
|
||
<div class="q-label" id="map-qlabel">TOCA LA COMARCA...</div>
|
||
<div class="q-text" id="map-qtext" style="font-size:1.65rem;margin:4px 0;">—</div>
|
||
<div id="map-qhint" style="font-size:.9rem;color:#aaa;min-height:22px;"></div>
|
||
</div>
|
||
<div class="prog-wrap"><div class="prog-bar" id="map-prog"></div></div>
|
||
<div class="map-score-row">
|
||
<span id="map-ctr"></span>
|
||
<span id="map-score-live"></span>
|
||
</div>
|
||
<div id="map-svg-container">
|
||
<svg id="map-svg" viewBox="0 0 820 600" xmlns="http://www.w3.org/2000/svg"></svg>
|
||
</div>
|
||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;justify-content:center;font-size:.8rem;color:#888;">
|
||
<span>🟫 Comarques de muntanya · 🟩 Resta de Catalunya</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ RESULT ═══════════════════ -->
|
||
<div id="screen-result" class="screen">
|
||
<div class="card" style="text-align:center;">
|
||
<span class="result-emoji" id="result-emoji">🎉</span>
|
||
<div class="result-title" id="result-title"></div>
|
||
<div class="result-score" id="result-score"></div>
|
||
<div class="stars-earned" id="result-stars"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
|
||
<button class="btn btn-primary" onclick="repeatLevel()">🔄 Repetir</button>
|
||
<button class="btn btn-back" onclick="goHome()">🏠 Inici</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ STATS ═══════════════════ -->
|
||
<div id="screen-stats" class="screen">
|
||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||
<div id="stats-content"><div style="text-align:center;padding:40px;color:#aaa;">Carregant…</div></div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ HELP / MANUAL ═══════════════════ -->
|
||
<div id="screen-help" class="screen">
|
||
<button class="btn btn-back" onclick="showScreen('screen-welcome')">← Tornar</button>
|
||
|
||
<div class="help-hero">🏔️🗺️⛰️</div>
|
||
<div class="help-hero-title">Com es juga?</div>
|
||
<div class="help-hero-sub">Aprèn les comarques de Catalunya jugant! 🎮</div>
|
||
|
||
<!-- ── Pas 1: Registre ── -->
|
||
<div class="help-section">
|
||
<div class="help-section-title" style="background:#1a6bb5;">👤 Primer de tot...</div>
|
||
<div class="help-card blue">
|
||
<div class="help-icon">➕</div>
|
||
<div class="help-text">
|
||
<strong>Crea el teu perfil</strong>
|
||
<p>Toca <b>«Nou jugador»</b>, escriu el teu nom i tria un emoji que t'agradi com a avatar. Cada jugador té les seves pròpies estreles i estadístiques!</p>
|
||
</div>
|
||
</div>
|
||
<div class="help-card blue">
|
||
<div class="help-icon">👆</div>
|
||
<div class="help-text">
|
||
<strong>Entra al joc</strong>
|
||
<p>La propera vegada, toca el teu nom a la pantalla inicial i ja estàs dins! No cal cap contrasenya 🎉</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Pas 2: Nivells ── -->
|
||
<div class="help-section">
|
||
<div class="help-section-title">🎯 Els 6 nivells</div>
|
||
|
||
<div class="help-card green">
|
||
<div class="help-icon">👀</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 1 · Descobreix</strong>
|
||
<p>Veus targetes amb el nom de cada comarca i la seva capital. Toca la targeta per girar-la i descobrir la capital! Perfecte per començar a memoritzar.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-card green">
|
||
<div class="help-icon">🧩</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 2 · Tria</strong>
|
||
<p>Et diuen el nom d'una comarca i has de triar la capital correcta entre 4 opcions. Toca la resposta que creus que és correcta!</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-card green">
|
||
<div class="help-icon">🔗</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 3 · Uneix</strong>
|
||
<p>A l'esquerra hi ha comarques i a la dreta capitals barrejades. Toca una comarca i després toca la seva capital per unir-les. Veuràs una línia que les connecta!</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-card">
|
||
<div class="help-icon">✏️</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 4 · Escriu</strong>
|
||
<p>Has d'escriure el nom de la capital de memòria, sense ajuda! No et preocupis pels accents, el joc és flexible amb l'ortografia.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-card" style="border-left-color:#1a7a3c;">
|
||
<div class="help-icon">🗺️</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 5 · El Gran Mapa</strong>
|
||
<p>Apareix el mapa real de Catalunya amb les comarques acolorides. El joc et diu quin nom o capital buscar, i tu has de tocar la comarca correcta al mapa. Pots triar entre dos modes:</p>
|
||
<p>🏷️ <b>Troba pel nom</b> — et donen el nom, tu toques el lloc<br>
|
||
📍 <b>Troba per la capital</b> — et donen la capital, tu trobes la comarca</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-card purple">
|
||
<div class="help-icon">🔮</div>
|
||
<div class="help-text">
|
||
<strong>Nivell 6 · Mapa Cec</strong>
|
||
<p>Com el Nivell 5, però el mapa no té cap nom escrit! Has d'identificar les comarques només per la seva forma i posició. El repte màxim per als experts! 🧠</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="help-tip">
|
||
<span class="tip-icon">💡</span>
|
||
<b>Consell:</b> Comença sempre pel Nivell 1 per aprendre, i quan te'ls sàpigues de memòria prova el Nivell 6. Cada nivell que passes et fa més fort!
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Estreles ── -->
|
||
<div class="help-section">
|
||
<div class="help-section-title" style="background:#d4a017;">⭐ Les estreles</div>
|
||
<div class="help-card" style="border-left-color:#d4a017;">
|
||
<div class="help-icon">🏆</div>
|
||
<div class="help-text">
|
||
<strong>Guanya estreles en cada partida</strong>
|
||
<p>Al final de cada nivell reps entre 1 i 3 estreles segons com ho has fet. S'acumulen totes!</p>
|
||
</div>
|
||
</div>
|
||
<div class="help-stars-demo">
|
||
<div class="help-star-item">
|
||
<div class="stars">⭐</div>
|
||
<div class="star-desc">Bon intent!<br>Has practicat 💪</div>
|
||
</div>
|
||
<div class="help-star-item">
|
||
<div class="stars">⭐⭐</div>
|
||
<div class="star-desc">Molt bé!<br>Ja ho domines 🌟</div>
|
||
</div>
|
||
<div class="help-star-item">
|
||
<div class="stars">⭐⭐⭐</div>
|
||
<div class="star-desc">Perfecte!<br>Ets un expert! 🏆</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Comarques ── -->
|
||
<div class="help-section">
|
||
<div class="help-section-title" style="background:#6a1e8a;">⚙️ Tria les teves comarques</div>
|
||
<div class="help-card purple">
|
||
<div class="help-icon">🗺️</div>
|
||
<div class="help-text">
|
||
<strong>Personalitza el joc!</strong>
|
||
<p>Un cop seleccionat el teu perfil, toca el botó <b>«⚙️ Les meves comarques»</b> per triar quines comarques vols practicar. Pots combinar grups!</p>
|
||
</div>
|
||
</div>
|
||
<div class="help-comarca-demo">
|
||
<span class="help-comarca-pill">⛰️ Muntanya</span>
|
||
<span class="help-comarca-pill">🏙️ Barcelona</span>
|
||
<span class="help-comarca-pill">🌊 Girona</span>
|
||
<span class="help-comarca-pill">🏔️ Lleida</span>
|
||
<span class="help-comarca-pill">☀️ Tarragona</span>
|
||
<span class="help-comarca-pill">🗺️ Totes (42!)</span>
|
||
</div>
|
||
<div class="help-tip">
|
||
<span class="tip-icon">🎓</span>
|
||
<b>Per als experts:</b> Activa <b>«Totes»</b> per practicar les 42 comarques de Catalunya! Perfecte quan ja domines les de muntanya.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Stats ── -->
|
||
<div class="help-section">
|
||
<div class="help-section-title" style="background:#1a7a3c;">📊 Les meves estadístiques</div>
|
||
<div class="help-card green">
|
||
<div class="help-icon">📈</div>
|
||
<div class="help-text">
|
||
<strong>Segueix el teu progrés</strong>
|
||
<p>Toca <b>«📊 Les meves estadístiques»</b> per veure quants encerts tens a cada nivell. Les barres canvien de color:</p>
|
||
<p>🟢 <b>Verd</b> = 80% o més — Excel·lent!<br>
|
||
🟠 <b>Taronja</b> = 50–79% — Molt bé, segueix practicant<br>
|
||
🔴 <b>Vermell</b> = menys del 50% — Necessites practicar més</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Final ── -->
|
||
<div style="text-align:center;padding:20px 0 10px;">
|
||
<div style="font-size:2.5rem;">🚀🌟🏔️</div>
|
||
<div style="font-size:1rem;font-weight:700;color:var(--green);margin:8px 0 4px;">Ja estàs llest per jugar!</div>
|
||
<div style="font-size:.88rem;color:#888;">Recorda: la pràctica fa al mestre 💪</div>
|
||
<button class="btn btn-primary" onclick="showScreen('screen-welcome')" style="margin-top:16px;">
|
||
🎮 Anar a jugar!
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ FILTER ═══════════════════ -->
|
||
<div id="screen-filter" class="screen">
|
||
<button class="btn btn-back" onclick="showScreen('screen-home')">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label" style="margin-bottom:6px;">⚙️ LES MEVES COMARQUES</div>
|
||
<div style="font-size:.88rem;color:#666;margin-bottom:4px;" id="filter-player-name"></div>
|
||
<div style="font-size:.85rem;color:#888;margin-bottom:12px;">
|
||
Tria quines comarques vols practicar. Pots combinar grups.
|
||
</div>
|
||
<div class="group-filter-grid" id="group-chips-player"></div>
|
||
<div class="filter-preview" id="filter-preview-player">10 comarques seleccionades</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ ADMIN ═══════════════════ -->
|
||
<div id="screen-admin" class="screen">
|
||
<button class="btn btn-back" onclick="goWelcome()">← Tornar</button>
|
||
<div class="card">
|
||
<div class="q-label">ACCÉS ADMINISTRADOR</div>
|
||
<div style="font-size:1.1rem;font-weight:700;margin-bottom:14px;">Introdueix el PIN de 4 dígits</div>
|
||
<div class="admin-pin-wrap">
|
||
<input type="password" inputmode="numeric" maxlength="4" id="admin-pin"
|
||
class="pin-input" placeholder="····"
|
||
oninput="if(this.value.length===4)loadAdmin()">
|
||
</div>
|
||
<div id="admin-err" style="color:var(--red);text-align:center;font-size:.9rem;min-height:22px;"></div>
|
||
</div>
|
||
<div id="admin-content"></div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ SCRIPTS ═══════════════════ -->
|
||
<script src="comarca-paths.js"></script>
|
||
<script>
|
||
/* ══════════════════════════════════════════════════════
|
||
DADES
|
||
══════════════════════════════════════════════════════ */
|
||
// Full catalogue — 41 comarques with province and mountain flag
|
||
// province: 'B'=Barcelona · 'G'=Girona · 'L'=Lleida · 'T'=Tarragona
|
||
const ALL_COMARQUES = [
|
||
// ── Lleida ──────────────────────────────────────────────────────────────
|
||
{ name:"Val d'Aran", capital:"Vielha", emoji:"🏔️", province:"L", mountain:true },
|
||
{ name:"Pallars Sobirà", capital:"Sort", emoji:"⛰️", province:"L", mountain:true },
|
||
{ name:"Alta Ribagorça", capital:"el Pont de Suert", emoji:"🌊", province:"L", mountain:true },
|
||
{ name:"Pallars Jussà", capital:"Tremp", emoji:"🦅", province:"L", mountain:true },
|
||
{ name:"Alt Urgell", capital:"la Seu d'Urgell", emoji:"⛪", province:"L", mountain:true },
|
||
{ name:"Solsonès", capital:"Solsona", emoji:"🏰", province:"L", mountain:true },
|
||
{ name:"Noguera", capital:"Balaguer", emoji:"🌾", province:"L", mountain:false },
|
||
{ name:"Segria", capital:"Lleida", emoji:"🏛️", province:"L", mountain:false },
|
||
{ name:"Urgell", capital:"Tàrrega", emoji:"🌻", province:"L", mountain:false },
|
||
{ name:"Garrigues", capital:"les Borges Blanques", emoji:"🫒", province:"L", mountain:false },
|
||
{ name:"Pla d'Urgell", capital:"Mollerussa", emoji:"🌽", province:"L", mountain:false },
|
||
{ name:"Segarra", capital:"Cervera", emoji:"🌿", province:"L", mountain:false },
|
||
// ── Girona ──────────────────────────────────────────────────────────────
|
||
{ name:"Cerdanya", capital:"Puigcerdà", emoji:"🌄", province:"G", mountain:true },
|
||
{ name:"Ripollès", capital:"Ripoll", emoji:"🌲", province:"G", mountain:true },
|
||
{ name:"Garrotxa", capital:"Olot", emoji:"🌋", province:"G", mountain:true },
|
||
{ name:"Alt Empordà", capital:"Figueres", emoji:"🌬️", province:"G", mountain:false },
|
||
{ name:"Baix Empordà", capital:"la Bisbal d'Empordà", emoji:"🏖️", province:"G", mountain:false },
|
||
{ name:"Gironès", capital:"Girona", emoji:"🦁", province:"G", mountain:false },
|
||
{ name:"Pla de l'Estany", capital:"Banyoles", emoji:"🦆", province:"G", mountain:false },
|
||
{ name:"La Selva", capital:"Santa Coloma de Farners", emoji:"🌿", province:"G", mountain:false },
|
||
// ── Barcelona ────────────────────────────────────────────────────────────
|
||
{ name:"Berguedà", capital:"Berga", emoji:"🏕️", province:"B", mountain:true },
|
||
{ name:"Osona", capital:"Vic", emoji:"🐄", province:"B", mountain:false },
|
||
{ name:"Bages", capital:"Manresa", emoji:"⛏️", province:"B", mountain:false },
|
||
{ name:"Moianès", capital:"Moià", emoji:"🪨", province:"B", mountain:false },
|
||
{ name:"Anoia", capital:"Igualada", emoji:"🏭", province:"B", mountain:false },
|
||
{ name:"Vallès Oriental", capital:"Granollers", emoji:"🌳", province:"B", mountain:false },
|
||
{ name:"Vallès Occidental", capital:"Sabadell", emoji:"🏙️", province:"B", mountain:false },
|
||
{ name:"Barcelonès", capital:"Barcelona", emoji:"🗼", province:"B", mountain:false },
|
||
{ name:"Baix Llobregat", capital:"Sant Feliu de Llobregat", emoji:"🏘️", province:"B", mountain:false },
|
||
{ name:"Maresme", capital:"Mataró", emoji:"⛵", province:"B", mountain:false },
|
||
{ name:"Garraf", capital:"Vilanova i la Geltrú", emoji:"🌊", province:"B", mountain:false },
|
||
{ name:"Alt Penedès", capital:"Vilafranca del Penedès", emoji:"🍇", province:"B", mountain:false },
|
||
// ── Tarragona ────────────────────────────────────────────────────────────
|
||
{ name:"Conca de Barberà", capital:"Montblanc", emoji:"🏰", province:"T", mountain:false },
|
||
{ name:"Priorat", capital:"Falset", emoji:"🍷", province:"T", mountain:false },
|
||
{ name:"Ribera d'Ebre", capital:"Móra d'Ebre", emoji:"🌊", province:"T", mountain:false },
|
||
{ name:"Terra Alta", capital:"Gandesa", emoji:"🌄", province:"T", mountain:false },
|
||
{ name:"Montsià", capital:"Amposta", emoji:"🦩", province:"T", mountain:false },
|
||
{ name:"Baix Ebre", capital:"Tortosa", emoji:"🏛️", province:"T", mountain:false },
|
||
{ name:"Tarragonès", capital:"Tarragona", emoji:"🏛️", province:"T", mountain:false },
|
||
{ name:"Baix Camp", capital:"Reus", emoji:"🎨", province:"T", mountain:false },
|
||
{ name:"Alt Camp", capital:"Valls", emoji:"🧅", province:"T", mountain:false },
|
||
{ name:"Baix Penedès", capital:"el Vendrell", emoji:"🎸", province:"T", mountain:false },
|
||
];
|
||
|
||
// ── Group filter — persisted in localStorage, scoped per player ───────────────
|
||
// activeGroups is a Set of: 'mountain' | 'B' | 'G' | 'L' | 'T' | 'all'
|
||
// Default: only mountain comarques (original behaviour)
|
||
function filterKey(playerId) {
|
||
return playerId ? `activeGroups_${playerId}` : 'activeGroups_default';
|
||
}
|
||
function loadActiveGroups(playerId) {
|
||
try {
|
||
const saved = JSON.parse(localStorage.getItem(filterKey(playerId)));
|
||
if (Array.isArray(saved) && saved.length) return new Set(saved);
|
||
} catch {}
|
||
return new Set(['mountain']);
|
||
}
|
||
function saveActiveGroups(set, playerId) {
|
||
localStorage.setItem(filterKey(playerId), JSON.stringify([...set]));
|
||
}
|
||
let activeGroups = loadActiveGroups(null);
|
||
|
||
// Derive the active COMARQUES subset from the current filter
|
||
function getActiveComarques() {
|
||
const all = activeGroups.has('all');
|
||
return ALL_COMARQUES.filter(c => {
|
||
if (all) return true;
|
||
if (activeGroups.has('mountain') && c.mountain) return true;
|
||
if (activeGroups.has(c.province)) return true;
|
||
return false;
|
||
});
|
||
}
|
||
|
||
// Legacy alias — always reflects the current filter
|
||
let COMARQUES = getActiveComarques();
|
||
const AVATARS = ['🦁','🐯','🐻','🦊','🐼','🐨','🐸','🦋','🌟','🚀','🦄','🐬','🐧','🌈','🎠','🐉'];
|
||
const MTN_COLORS = {
|
||
"Val d'Aran":"#A0522D","Pallars Sobirà":"#B8621A","Alta Ribagorça":"#C47322",
|
||
"Pallars Jussà":"#D4851F","Alt Urgell":"#E09828","Cerdanya":"#E8A82E",
|
||
"Ripollès":"#C86820","Garrotxa":"#B05818","Berguedà":"#D07B25","Solsonès":"#E8B835",
|
||
};
|
||
const LEVEL_ICONS = {1:'👀',2:'🧩',3:'🔗',4:'✏️',5:'🗺️'};
|
||
const LEVEL_NAMES = {1:'Descobreix',2:'Tria',3:'Uneix',4:'Escriu',5:'El Mapa'};
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
ESTAT
|
||
══════════════════════════════════════════════════════ */
|
||
let currentPlayer = null;
|
||
let offlineMode = false;
|
||
let totalStars = 0;
|
||
let sessionStart = null;
|
||
|
||
let currentLevel=1, currentQ=0, score=0, questions=[];
|
||
let fcFlipped=false, fcIndex=0;
|
||
let mapMode='name', mapTarget=null, mapAnswered=false, mapBuilt=false, mapBlind=false;
|
||
let l3Round=0, l3Sel=null, l3BTotal=0, l3BDone=0;
|
||
const L3B=5;
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
API
|
||
══════════════════════════════════════════════════════ */
|
||
async function api(method, path, body) {
|
||
try {
|
||
const opts = { method, headers: {} };
|
||
if (body) { opts.headers['Content-Type']='application/json'; opts.body=JSON.stringify(body); }
|
||
const r = await fetch(path, opts);
|
||
if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status });
|
||
return r.json();
|
||
} catch(e) { return null; }
|
||
}
|
||
const apiGet = path => api('GET', path);
|
||
const apiPost = (path, body)=> api('POST', path, body);
|
||
|
||
async function apiAdmin(pin) {
|
||
try {
|
||
const r = await fetch('/api/admin/stats', { headers: {'x-admin-pin': pin} });
|
||
if (r.status === 401) return { error: 'PIN incorrecte' };
|
||
if (!r.ok) return null;
|
||
return r.json();
|
||
} catch { return null; }
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
UTILS
|
||
══════════════════════════════════════════════════════ */
|
||
const shuffle=a=>{let b=[...a];for(let i=b.length-1;i>0;i--){const j=~~(Math.random()*(i+1));[b[i],b[j]]=[b[j],b[i]];}return b;};
|
||
const normalize=s=>s.trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z\s]/g,'');
|
||
|
||
function showScreen(id) {
|
||
document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
|
||
document.getElementById(id).classList.add('active');
|
||
window.scrollTo(0,0);
|
||
}
|
||
|
||
function updateHeader() {
|
||
document.getElementById('header-stars').textContent = '⭐ ' + totalStars;
|
||
const ph = document.getElementById('hdr-player');
|
||
if (currentPlayer) {
|
||
ph.textContent = currentPlayer.avatar;
|
||
ph.style.display='flex';
|
||
} else {
|
||
ph.style.display='none';
|
||
}
|
||
}
|
||
|
||
function addStars(n) {
|
||
totalStars += n;
|
||
// localStorage kept only as offline fallback — source of truth is the API
|
||
if (currentPlayer) localStorage.setItem('stars_'+currentPlayer.id, totalStars);
|
||
updateHeader();
|
||
}
|
||
|
||
async function saveSession(level, score, total, stars) {
|
||
if (offlineMode || !currentPlayer) return;
|
||
const duration = sessionStart ? Math.round((Date.now()-sessionStart)/1000) : 0;
|
||
await apiPost('/api/sessions', { player_id: currentPlayer.id, level, score, total, stars, duration_seconds: duration });
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
INIT
|
||
══════════════════════════════════════════════════════ */
|
||
window.addEventListener('load', async () => {
|
||
// Test backend availability
|
||
const health = await apiGet('/api/health');
|
||
offlineMode = !health;
|
||
|
||
if (offlineMode) {
|
||
// No backend → mode directe, stars from localStorage
|
||
totalStars = parseInt(localStorage.getItem('stars') || '0');
|
||
updateHeader();
|
||
showScreen('screen-home');
|
||
return;
|
||
}
|
||
|
||
// Build welcome
|
||
await renderWelcome();
|
||
showScreen('screen-welcome');
|
||
});
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
WELCOME / PLAYER MANAGEMENT
|
||
══════════════════════════════════════════════════════ */
|
||
async function renderWelcome() {
|
||
const players = await apiGet('/api/players') || [];
|
||
const grid = document.getElementById('players-grid');
|
||
grid.innerHTML = '';
|
||
|
||
if (!players.length) {
|
||
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#aaa;font-size:.9rem;padding:20px;">Cap jugador registrat encara.<br>Crea el primer perfil! 👇</div>';
|
||
return;
|
||
}
|
||
|
||
players.forEach(p => {
|
||
const card = document.createElement('div');
|
||
card.className = 'player-card';
|
||
// Star count shown on card — loaded from API after render
|
||
card.innerHTML = `
|
||
<span class="p-avatar">${p.avatar}</span>
|
||
<div class="p-name">${p.name}</div>
|
||
<div class="p-stars" id="card-stars-${p.id}">⭐ …</div>`;
|
||
card.onclick = () => selectPlayer(p);
|
||
// Load real star count from API asynchronously
|
||
apiGet(`/api/players/${p.id}/stats`).then(data => {
|
||
const el = document.getElementById(`card-stars-${p.id}`);
|
||
if (el && data) el.textContent = `⭐ ${data.totalStars}`;
|
||
});
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
async function selectPlayer(p) {
|
||
currentPlayer = p;
|
||
// Load this player's comarca filter
|
||
activeGroups = loadActiveGroups(p.id);
|
||
COMARQUES = getActiveComarques();
|
||
mapBuilt = false;
|
||
// Load star count from API; fall back to localStorage for offline mode
|
||
if (!offlineMode) {
|
||
const data = await apiGet(`/api/players/${p.id}/stats`);
|
||
totalStars = data ? data.totalStars : parseInt(localStorage.getItem('stars_'+p.id) || '0');
|
||
} else {
|
||
totalStars = parseInt(localStorage.getItem('stars_'+p.id) || '0');
|
||
}
|
||
updateHeader();
|
||
document.getElementById('home-greeting').textContent = `Hola, ${p.name}! 👋`;
|
||
document.getElementById('btn-stats').style.display = 'inline-block';
|
||
document.getElementById('btn-filter').style.display = 'inline-block';
|
||
document.getElementById('btn-change').style.display = 'inline-block';
|
||
showScreen('screen-home');
|
||
}
|
||
|
||
function goWelcome() {
|
||
currentPlayer = null;
|
||
updateHeader();
|
||
document.getElementById('btn-stats').style.display = 'none';
|
||
document.getElementById('btn-filter').style.display = 'none';
|
||
document.getElementById('btn-change').style.display = 'none';
|
||
document.getElementById('home-greeting').textContent = 'Aprèn les Comarques!';
|
||
renderWelcome();
|
||
showScreen('screen-welcome');
|
||
}
|
||
|
||
/* ── Register ── */
|
||
(function buildAvatarGrid() {
|
||
const grid = document.getElementById('avatar-grid');
|
||
let selected = null;
|
||
AVATARS.forEach(av => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'avatar-btn';
|
||
btn.textContent = av;
|
||
btn.onclick = () => {
|
||
document.querySelectorAll('.avatar-btn').forEach(b=>b.classList.remove('selected'));
|
||
btn.classList.add('selected');
|
||
selected = av;
|
||
validateReg();
|
||
};
|
||
grid.appendChild(btn);
|
||
});
|
||
// expose selected getter
|
||
window._getSelectedAvatar = () => selected;
|
||
})();
|
||
|
||
function validateReg() {
|
||
const name = document.getElementById('reg-name').value.trim();
|
||
const av = window._getSelectedAvatar();
|
||
document.getElementById('reg-btn').disabled = !(name.length >= 2 && av);
|
||
}
|
||
|
||
async function registerPlayer() {
|
||
const name = document.getElementById('reg-name').value.trim();
|
||
const avatar = window._getSelectedAvatar();
|
||
if (!name || !avatar) return;
|
||
|
||
const result = await apiPost('/api/players', { name, avatar });
|
||
if (!result) {
|
||
document.getElementById('reg-name-err').textContent = 'Error de connexió. Torna-ho a provar.';
|
||
return;
|
||
}
|
||
if (result.error) {
|
||
document.getElementById('reg-name-err').textContent = result.error;
|
||
return;
|
||
}
|
||
document.getElementById('reg-name-err').textContent = '';
|
||
selectPlayer(result);
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
STATS SCREEN
|
||
══════════════════════════════════════════════════════ */
|
||
function goToStats() {
|
||
if (!currentPlayer && !offlineMode) { showScreen('screen-welcome'); return; }
|
||
showScreen('screen-stats');
|
||
loadStats();
|
||
}
|
||
|
||
async function loadStats() {
|
||
if (offlineMode || !currentPlayer) {
|
||
document.getElementById('stats-content').innerHTML =
|
||
'<div class="card" style="text-align:center;color:#aaa;">Estadístiques no disponibles en mode sense connexió.</div>';
|
||
return;
|
||
}
|
||
|
||
const data = await apiGet(`/api/players/${currentPlayer.id}/stats`);
|
||
if (!data) {
|
||
document.getElementById('stats-content').innerHTML =
|
||
'<div class="card" style="text-align:center;color:var(--red);">Error en carregar les estadístiques.</div>';
|
||
return;
|
||
}
|
||
|
||
const ls = data.levelStats || {};
|
||
const recentRows = (data.recent || []).map(s => {
|
||
const d = new Date(s.date);
|
||
const dateStr = `${d.getDate()}/${d.getMonth()+1} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||
const pct = Math.round(s.score/s.total*100);
|
||
return `<div class="recent-item">
|
||
<span class="ri-lvl">${LEVEL_ICONS[s.level]}</span>
|
||
<span class="ri-score">${LEVEL_NAMES[s.level]} · ${pct}%</span>
|
||
<span class="ri-stars">${'⭐'.repeat(s.stars)}</span>
|
||
<span class="ri-date">${dateStr}</span>
|
||
</div>`;
|
||
}).join('') || '<div style="color:#aaa;font-size:.85rem;text-align:center;padding:12px;">Cap sessió registrada</div>';
|
||
|
||
const barRows = [1,2,3,4,5].map(lvl => {
|
||
const s = ls[lvl];
|
||
const pct = s?.bestPct ?? null;
|
||
const color = pct===null ? '#ddd' : pct>=80 ? '#4DBD6E' : pct>=50 ? '#F4A535' : '#F05C5C';
|
||
const pctStr = pct===null ? '—' : pct+'%';
|
||
return `<div class="level-bar-row">
|
||
<span class="lbr-icon">${LEVEL_ICONS[lvl]}</span>
|
||
<span class="lbr-name">${LEVEL_NAMES[lvl]}</span>
|
||
<div class="lbr-track">
|
||
<div class="lbr-fill" style="width:${pct??0}%;background:${color};"></div>
|
||
</div>
|
||
<span class="lbr-pct" style="color:${color};">${pctStr}</span>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
document.getElementById('stats-content').innerHTML = `
|
||
<div class="stats-hero">
|
||
<span class="stats-avatar">${data.player.avatar}</span>
|
||
<div class="stats-name">${data.player.name}</div>
|
||
<div class="stats-stars">⭐ ${data.totalStars} estreles acumulades</div>
|
||
</div>
|
||
<div class="stats-grid">
|
||
<div class="stat-box">
|
||
<div class="stat-val">${data.totalSessions}</div>
|
||
<div class="stat-lbl">Partides jugades</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-val">${data.totalStars}</div>
|
||
<div class="stat-lbl">Estreles totals</div>
|
||
</div>
|
||
</div>
|
||
<div class="level-bars">
|
||
<div class="q-label" style="margin-bottom:14px;">MILLOR RESULTAT PER NIVELL</div>
|
||
${barRows}
|
||
</div>
|
||
${data.recent?.length ? `
|
||
<div class="recent-list">
|
||
<div class="recent-title">ÚLTIMES PARTIDES</div>
|
||
${recentRows}
|
||
</div>` : ''}`;
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
ADMIN — GROUP FILTER CONFIG
|
||
══════════════════════════════════════════════════════ */
|
||
const GROUP_DEFS = [
|
||
{ key:'mountain', label:'⛰️ Muntanya', desc:'Les 10 comarques de muntanya' },
|
||
{ key:'B', label:'🏙️ Barcelona', desc:'12 comarques de la demarcació' },
|
||
{ key:'G', label:'🌊 Girona', desc:'8 comarques de la demarcació' },
|
||
{ key:'L', label:'🏔️ Lleida', desc:'12 comarques de la demarcació' },
|
||
{ key:'T', label:'☀️ Tarragona', desc:'10 comarques de la demarcació' },
|
||
{ key:'all', label:'🗺️ Totes', desc:'Les 42 comarques de Catalunya' },
|
||
];
|
||
|
||
function renderAdminConfig() {
|
||
const chips = GROUP_DEFS.map(g => `
|
||
<div class="group-chip ${activeGroups.has(g.key)?'active':''}"
|
||
id="chip-${g.key}" onclick="toggleGroup('${g.key}')" title="${g.desc}">
|
||
${g.label}
|
||
</div>`).join('');
|
||
|
||
const count = getActiveComarques().length;
|
||
return `
|
||
<div class="card" style="margin-bottom:0;">
|
||
<div class="q-label" style="margin-bottom:10px;">⚙️ CONFIGURACIÓ DEL JOC</div>
|
||
<div style="font-size:.88rem;color:#666;margin-bottom:4px;">
|
||
Selecciona quines comarques s'han de practicar. Pots combinar grups.
|
||
</div>
|
||
<div class="group-filter-grid" id="group-chips">${chips}</div>
|
||
<div class="filter-preview" id="filter-preview">
|
||
${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function toggleGroup(key) {
|
||
// 'all' is exclusive — selecting it deselects others; selecting others deselects 'all'
|
||
if (key === 'all') {
|
||
activeGroups = new Set(['all']);
|
||
} else {
|
||
activeGroups.delete('all');
|
||
if (activeGroups.has(key)) {
|
||
activeGroups.delete(key);
|
||
if (activeGroups.size === 0) activeGroups.add('mountain'); // at least one
|
||
} else {
|
||
activeGroups.add(key);
|
||
}
|
||
}
|
||
saveActiveGroups(activeGroups, currentPlayer?.id ?? null);
|
||
syncChips();
|
||
// Refresh COMARQUES and force map rebuild on next level start
|
||
COMARQUES = getActiveComarques();
|
||
mapBuilt = false; // force SVG rebuild with new active set
|
||
}
|
||
|
||
function syncChips() {
|
||
// Sync both the admin chips and the player chips
|
||
['chip-', 'pchip-'].forEach(prefix => {
|
||
GROUP_DEFS.forEach(g => {
|
||
const el = document.getElementById(prefix + g.key);
|
||
if (!el) return;
|
||
el.classList.toggle('active', activeGroups.has(g.key));
|
||
});
|
||
});
|
||
const count = getActiveComarques().length;
|
||
const text = `${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}`;
|
||
['filter-preview', 'filter-preview-player'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = text;
|
||
});
|
||
}
|
||
|
||
function goToFilter() {
|
||
if (!currentPlayer) return;
|
||
document.getElementById('filter-player-name').textContent =
|
||
`${currentPlayer.avatar} ${currentPlayer.name}`;
|
||
// Render player chips with prefix 'pchip-'
|
||
const chips = GROUP_DEFS.map(g => `
|
||
<div class="group-chip ${activeGroups.has(g.key)?'active':''}"
|
||
id="pchip-${g.key}" onclick="toggleGroup('${g.key}')" title="${g.desc}">
|
||
${g.label}
|
||
</div>`).join('');
|
||
document.getElementById('group-chips-player').innerHTML = chips;
|
||
const count = getActiveComarques().length;
|
||
document.getElementById('filter-preview-player').textContent =
|
||
`${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}`;
|
||
showScreen('screen-filter');
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
ADMIN SCREEN
|
||
══════════════════════════════════════════════════════ */
|
||
async function loadAdmin() {
|
||
const pin = document.getElementById('admin-pin').value;
|
||
const data = await apiAdmin(pin);
|
||
const err = document.getElementById('admin-err');
|
||
|
||
if (!data) { err.textContent='Error de connexió.'; return; }
|
||
if (data.error) { err.textContent=data.error; return; }
|
||
err.textContent = '';
|
||
|
||
const rows = data.map(p => {
|
||
const cells = [1,2,3,4,5,6].map(lvl => {
|
||
const s = p.levelStats[lvl];
|
||
if (!s) return `<td><span class="pct-cell pct-gray">—</span></td>`;
|
||
const pct = s.bestPct;
|
||
const cls = pct>=80?'pct-green':pct>=50?'pct-orange':'pct-red';
|
||
return `<td><span class="pct-cell ${cls}">${pct}%</span></td>`;
|
||
}).join('');
|
||
const lastSeen = p.lastActivity
|
||
? new Date(p.lastActivity).toLocaleDateString('ca-ES')
|
||
: '—';
|
||
return `<tr>
|
||
<td><span style="font-size:1.2rem">${p.avatar}</span> <strong>${p.name}</strong></td>
|
||
<td>${p.totalSessions}</td>
|
||
<td>⭐ ${p.totalStars}</td>
|
||
${cells}
|
||
<td style="font-size:.75rem;color:#aaa;">${lastSeen}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
document.getElementById('admin-content').innerHTML = `
|
||
${renderAdminConfig()}
|
||
<div class="admin-table-wrap" style="margin-top:24px;">
|
||
<table class="admin-tbl">
|
||
<thead><tr>
|
||
<th>Jugador</th><th>Sessions</th><th>⭐</th>
|
||
<th>👀 N1</th><th>🧩 N2</th><th>🔗 N3</th><th>✏️ N4</th><th>🗺️ N5</th><th>🔮 N6</th>
|
||
<th>Darrera activitat</th>
|
||
</tr></thead>
|
||
<tbody>${rows || '<tr><td colspan="10" style="color:#aaa;text-align:center;padding:20px;">Sense jugadors</td></tr>'}</tbody>
|
||
</table>
|
||
</div>`;
|
||
// Restore chip active state after render
|
||
syncChips();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
FEEDBACK & SOUNDS
|
||
══════════════════════════════════════════════════════ */
|
||
const OKS=['Molt bé! 🎉','Excel·lent! 🌟','Perfecte! 👏','Genial! 💪','Fantàstic! 🚀','Ets una crack! 🏆','Increïble! 🌈'];
|
||
const NOS=['Quasi! Intenta-ho 💪','No passa res! 🙂','Poc a poc! 🐢','Ja ho tindràs! 😊'];
|
||
|
||
function showFeedback(ok) {
|
||
const el=document.getElementById('feedback');
|
||
document.getElementById('fb-emoji').textContent=ok?['🎊','🌟','🎉','✨','👑'][~~(Math.random()*5)]:'😅';
|
||
document.getElementById('fb-msg').textContent=ok?OKS[~~(Math.random()*OKS.length)]:NOS[~~(Math.random()*NOS.length)];
|
||
el.style.background=ok?'rgba(77,189,110,.9)':'rgba(240,92,92,.8)';
|
||
el.classList.add('show'); playSound(ok);
|
||
setTimeout(()=>el.classList.remove('show'),1100);
|
||
}
|
||
|
||
function playSound(ok){
|
||
try{const ctx=new(window.AudioContext||window.webkitAudioContext)();
|
||
const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);
|
||
if(ok){o.frequency.setValueAtTime(523,ctx.currentTime);o.frequency.setValueAtTime(659,ctx.currentTime+.1);o.frequency.setValueAtTime(784,ctx.currentTime+.2);}
|
||
else{o.frequency.setValueAtTime(300,ctx.currentTime);o.frequency.setValueAtTime(200,ctx.currentTime+.18);}
|
||
g.gain.setValueAtTime(.28,ctx.currentTime);g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.45);
|
||
o.start();o.stop(ctx.currentTime+.45);}catch(e){}
|
||
}
|
||
|
||
function launchConfetti(){
|
||
const c=document.getElementById('confetti-canvas'),cx=c.getContext('2d');
|
||
c.width=window.innerWidth;c.height=window.innerHeight;
|
||
const p=Array.from({length:130},()=>({x:Math.random()*c.width,y:-20,r:5+Math.random()*8,alpha:1,
|
||
color:['#F4A535','#5BC97A','#5B9CF4','#A87FE8','#F05C5C','#F4D535'][~~(Math.random()*6)],
|
||
vx:(Math.random()-.5)*4,vy:2+Math.random()*4}));
|
||
let fr;function draw(){cx.clearRect(0,0,c.width,c.height);let alive=false;
|
||
p.forEach(q=>{q.x+=q.vx;q.y+=q.vy;q.alpha-=.009;if(q.alpha>0)alive=true;
|
||
cx.globalAlpha=Math.max(0,q.alpha);cx.fillStyle=q.color;cx.beginPath();cx.arc(q.x,q.y,q.r,0,Math.PI*2);cx.fill();});
|
||
cx.globalAlpha=1;if(alive)fr=requestAnimationFrame(draw);else cx.clearRect(0,0,c.width,c.height);}
|
||
cancelAnimationFrame(fr);draw();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
RESULT
|
||
══════════════════════════════════════════════════════ */
|
||
async function showResult(level) {
|
||
const pct=Math.round(score/questions.length*100);
|
||
let emoji,title,stars;
|
||
if(pct===100){emoji='🏆';title='PERFECTE! Ets una campiona!';stars=3;}
|
||
else if(pct>=70){emoji='🌟';title='Molt bé! Segueix així!';stars=2;}
|
||
else{emoji='💪';title='Bon intent! Practica una mica més';stars=1;}
|
||
document.getElementById('result-emoji').textContent=emoji;
|
||
document.getElementById('result-title').textContent=title;
|
||
document.getElementById('result-score').textContent=`${score} de ${questions.length} correctes (${pct}%)`;
|
||
document.getElementById('result-stars').textContent='⭐'.repeat(stars);
|
||
addStars(stars);
|
||
await saveSession(level, score, questions.length, stars);
|
||
if(pct===100)launchConfetti();
|
||
showScreen('screen-result');
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
NAVEGACIÓ
|
||
══════════════════════════════════════════════════════ */
|
||
function goHome(){ showScreen('screen-home'); }
|
||
|
||
function startLevel(n){
|
||
currentLevel=n; currentQ=0; score=0;
|
||
COMARQUES = getActiveComarques(); // refresh from current filter
|
||
questions=shuffle(COMARQUES);
|
||
sessionStart=Date.now();
|
||
if(n===1)initL1();else if(n===2)initL2();else if(n===3)initL3();
|
||
else if(n===4)initL4();else if(n===5){mapBlind=false;initMap();}
|
||
else{mapBlind=true;initMap();}
|
||
}
|
||
function repeatLevel(){ startLevel(currentLevel); }
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
L1 FLASHCARDS
|
||
══════════════════════════════════════════════════════ */
|
||
function initL1(){fcIndex=0;showScreen('screen-l1');renderCard();}
|
||
function renderCard(){
|
||
const c=questions[fcIndex];
|
||
document.getElementById('fc-inner').classList.remove('flipped');fcFlipped=false;
|
||
document.getElementById('fc-comarca').textContent=c.emoji+' '+c.name;
|
||
document.getElementById('fc-capital').textContent='📍 '+c.capital;
|
||
document.getElementById('fc-hint').textContent='Toca per veure la capital';
|
||
document.getElementById('l1-prog').style.width=((fcIndex+1)/questions.length*100)+'%';
|
||
document.getElementById('l1-ctr').textContent=`${fcIndex+1} / ${questions.length}`;
|
||
}
|
||
function flipCard(){fcFlipped=!fcFlipped;document.getElementById('fc-inner').classList.toggle('flipped',fcFlipped);document.getElementById('fc-hint').textContent=fcFlipped?'Toca per amagar':'Toca per veure la capital';}
|
||
function nextCard(){
|
||
if(fcIndex<questions.length-1){fcIndex++;renderCard();}
|
||
else{
|
||
addStars(1);
|
||
saveSession(1,questions.length,questions.length,1);
|
||
document.getElementById('result-emoji').textContent='📚';
|
||
document.getElementById('result-title').textContent='Has vist totes les comarques!';
|
||
document.getElementById('result-score').textContent='Ara prova el Nivell 2 per practicar!';
|
||
document.getElementById('result-stars').textContent='⭐';
|
||
showScreen('screen-result');
|
||
}
|
||
}
|
||
function prevCard(){if(fcIndex>0){fcIndex--;renderCard();}}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
L2 MULTI-CHOICE
|
||
══════════════════════════════════════════════════════ */
|
||
function initL2(){showScreen('screen-l2');renderL2();}
|
||
function renderL2(){
|
||
if(currentQ>=questions.length){showResult(2);return;}
|
||
const q=questions[currentQ];
|
||
document.getElementById('l2-prog').style.width=(currentQ/questions.length*100)+'%';
|
||
document.getElementById('l2-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
|
||
document.getElementById('l2-q').textContent=q.emoji+' '+q.name;
|
||
const wrong=shuffle(COMARQUES.filter(c=>c.name!==q.name)).slice(0,3);
|
||
const opts=shuffle([q,...wrong]);
|
||
const grid=document.getElementById('l2-opts');grid.innerHTML='';
|
||
opts.forEach(opt=>{
|
||
const btn=document.createElement('button');btn.className='opt-btn';btn.textContent=opt.capital;
|
||
btn.onclick=()=>handleL2(btn,opt.capital===q.capital,q.capital);grid.appendChild(btn);});
|
||
}
|
||
function handleL2(btn,ok,right){
|
||
document.querySelectorAll('.opt-btn').forEach(b=>b.disabled=true);
|
||
btn.classList.add(ok?'correct':'wrong');
|
||
if(!ok)document.querySelectorAll('.opt-btn').forEach(b=>{if(b.textContent===right)b.classList.add('correct');});
|
||
if(ok)score++;showFeedback(ok);
|
||
setTimeout(()=>{currentQ++;renderL2();},1400);
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
L3 MATCH
|
||
══════════════════════════════════════════════════════ */
|
||
function initL3(){l3Round=0;score=0;showScreen('screen-l3');renderL3();}
|
||
function renderL3(){
|
||
l3Sel=null;const batch=questions.slice(l3Round*L3B,(l3Round+1)*L3B);
|
||
if(!batch.length){showResult(3);return;}
|
||
document.getElementById('l3-prog').style.width=(l3Round*L3B/questions.length*100)+'%';
|
||
const left=shuffle(batch.map(c=>({text:c.emoji+' '+c.name,key:c.name,type:'C'})));
|
||
const right=shuffle(batch.map(c=>({text:'📍 '+c.capital,key:c.name,type:'K'})));
|
||
const all=[];for(let i=0;i<batch.length;i++){all.push(left[i],right[i]);}
|
||
const grid=document.getElementById('l3-grid');grid.innerHTML='';
|
||
all.forEach(item=>{const d=document.createElement('div');d.className='m-item';d.textContent=item.text;
|
||
d.dataset.key=item.key;d.dataset.type=item.type;d.onclick=()=>handleL3(d);grid.appendChild(d);});
|
||
l3BTotal=batch.length;l3BDone=0;
|
||
}
|
||
function handleL3(div){
|
||
if(div.classList.contains('matched'))return;
|
||
if(!l3Sel){document.querySelectorAll('.m-item.selected').forEach(e=>e.classList.remove('selected'));div.classList.add('selected');l3Sel=div;return;}
|
||
if(l3Sel===div){div.classList.remove('selected');l3Sel=null;return;}
|
||
const ok=l3Sel.dataset.key===div.dataset.key&&l3Sel.dataset.type!==div.dataset.type;
|
||
if(ok){l3Sel.classList.remove('selected');l3Sel.classList.add('matched');div.classList.add('matched');l3Sel=null;l3BDone++;score++;playSound(true);
|
||
if(l3BDone===l3BTotal)setTimeout(()=>{l3Round++;renderL3();if(l3Round*L3B>=questions.length)showResult(3);},600);}
|
||
else{l3Sel.classList.remove('selected');[l3Sel,div].forEach(e=>{e.classList.add('bad');setTimeout(()=>e.classList.remove('bad'),400);});playSound(false);l3Sel=null;}
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
L4 TYPE
|
||
══════════════════════════════════════════════════════ */
|
||
function initL4(){showScreen('screen-l4');renderL4();}
|
||
function renderL4(){
|
||
if(currentQ>=questions.length){showResult(4);return;}
|
||
const q=questions[currentQ];
|
||
document.getElementById('l4-prog').style.width=(currentQ/questions.length*100)+'%';
|
||
document.getElementById('l4-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
|
||
document.getElementById('l4-q').textContent=q.emoji+' '+q.name;
|
||
document.getElementById('l4-input').value='';
|
||
document.getElementById('l4-input').style.borderColor='#ddd';
|
||
document.getElementById('l4-fb').textContent='';
|
||
document.getElementById('l4-input').focus();
|
||
}
|
||
function checkL4(){
|
||
const q=questions[currentQ],val=document.getElementById('l4-input').value;
|
||
const ok=normalize(val)===normalize(q.capital)||normalize(val)===normalize(q.capital.replace(/^(el |la |l')/i,''));
|
||
const inp=document.getElementById('l4-input'),fb=document.getElementById('l4-fb');
|
||
if(ok){inp.style.borderColor='#5BC97A';fb.style.color='#1a7a3c';fb.textContent='✅ Correcte! La capital és '+q.capital;score++;showFeedback(true);setTimeout(()=>{currentQ++;renderL4();},1400);}
|
||
else if(!val.trim()){fb.style.color='#aaa';fb.textContent='Escriu alguna cosa!';}
|
||
else{inp.style.borderColor='#F05C5C';fb.style.color='#a02020';fb.textContent='❌ La capital és '+q.capital;showFeedback(false);setTimeout(()=>{currentQ++;renderL4();},2000);}
|
||
}
|
||
function skipL4(){const q=questions[currentQ];document.getElementById('l4-fb').style.color='#999';document.getElementById('l4-fb').textContent='➡ '+q.name+' → '+q.capital;setTimeout(()=>{currentQ++;renderL4();},1900);}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
L5 MAP
|
||
══════════════════════════════════════════════════════ */
|
||
function buildMap(){
|
||
// Always rebuild when switching between normal and blind mode
|
||
const needRebuild = mapBuilt && (document.getElementById('map-svg').dataset.blind !== String(mapBlind));
|
||
if(mapBuilt && !needRebuild)return;
|
||
mapBuilt=true;
|
||
document.getElementById('map-svg').dataset.blind = String(mapBlind);
|
||
const svg=document.getElementById('map-svg'); svg.innerHTML='';
|
||
const sea=document.createElementNS('http://www.w3.org/2000/svg','rect');
|
||
sea.setAttribute('width','820');sea.setAttribute('height','600');sea.setAttribute('fill','#C8E8F5');svg.appendChild(sea);
|
||
// Active set — comarques that are clickable in this session
|
||
const activeSet = new Set(COMARQUES.map(c => c.name));
|
||
for(const [name,info] of Object.entries(COMARCA_PATHS)){
|
||
const path=document.createElementNS('http://www.w3.org/2000/svg','path');
|
||
path.setAttribute('d',info.d);
|
||
if(activeSet.has(name)){
|
||
// Clickable comarca — use MTN_COLORS if available, else a neutral green-blue
|
||
const fill=MTN_COLORS[name]||'#4A90A4';
|
||
path.setAttribute('fill',fill);path.setAttribute('class','comarca-mountain');
|
||
path.setAttribute('data-name',name);path.style.setProperty('--mfill',fill);
|
||
path.setAttribute('id','mp_'+info.id);
|
||
path.addEventListener('click',()=>handleMapClick(path));
|
||
const title=document.createElementNS('http://www.w3.org/2000/svg','title');title.textContent=name;path.appendChild(title);
|
||
}else{path.setAttribute('class','comarca-bg');}
|
||
svg.appendChild(path);
|
||
}
|
||
// Level 6 (blind mode): no labels, no capital dots — identify by shape only
|
||
if(!mapBlind){
|
||
for(const c of COMARQUES){
|
||
const info=COMARCA_PATHS[c.name]; if(!info)continue;
|
||
const small=c.name.length>12;
|
||
const words=c.name.split(' ');
|
||
if(words.length>2&&small){addLabel(svg,info.cx,info.cy-5,words.slice(0,~~(words.length/2+.5)).join(' '),true);addLabel(svg,info.cx,info.cy+7,words.slice(~~(words.length/2+.5)).join(' '),true);}
|
||
else{addLabel(svg,info.cx,info.cy,c.name,small);}
|
||
const dot=document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||
dot.setAttribute('cx',info.cx);dot.setAttribute('cy',info.cy+14);dot.setAttribute('r','2.5');dot.setAttribute('class','capital-dot');svg.appendChild(dot);
|
||
}
|
||
}
|
||
const g=document.createElementNS('http://www.w3.org/2000/svg','g');g.setAttribute('transform','translate(785,35)');
|
||
g.innerHTML=`<circle cx="0" cy="0" r="22" fill="rgba(255,255,255,.9)" stroke="#aaa" stroke-width="1"/>
|
||
<text x="0" y="-12" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">N</text>
|
||
<text x="0" y="14" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">S</text>
|
||
<text x="-13" y="2" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">O</text>
|
||
<text x="13" y="2" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">E</text>
|
||
<polygon points="0,-16 3,-5 0,-8 -3,-5" fill="#E33"/><polygon points="0,16 3,5 0,8 -3,5" fill="#555"/>`;
|
||
svg.appendChild(g);
|
||
addTextSVG(svg,420,18,'ANDORRA · FRANÇA',7,'#999','middle');
|
||
addTextSVG(svg,36,310,'ARAGÓ',7,'#999','middle','rotate(-90,36,310)');
|
||
addTextSVG(svg,790,430,'Mar Mediterrani',7,'#6BAFD6','middle','rotate(90,790,430)');
|
||
addTextSVG(svg,320,575,'COMUNITAT VALENCIANA',7,'#999','middle');
|
||
}
|
||
function addLabel(svg,x,y,text,small){
|
||
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
t.setAttribute('x',x);t.setAttribute('y',y);t.setAttribute('class','comarca-label'+(small?' small':''));t.textContent=text;svg.appendChild(t);
|
||
}
|
||
function addTextSVG(svg,x,y,text,size,fill,anchor,transform){
|
||
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
t.setAttribute('x',x);t.setAttribute('y',y);t.setAttribute('font-size',size);t.setAttribute('fill',fill);
|
||
t.setAttribute('text-anchor',anchor||'middle');t.setAttribute('font-family','Lexend,Arial');t.setAttribute('dominant-baseline','middle');
|
||
if(transform)t.setAttribute('transform',transform);t.textContent=text;svg.appendChild(t);
|
||
}
|
||
function resetMapPaths(){
|
||
document.querySelectorAll('.comarca-mountain').forEach(p=>{const name=p.getAttribute('data-name');p.style.fill=MTN_COLORS[name]||'#C97D10';p.classList.remove('wrong-shake');p.style.filter='';p.style.opacity='1';});
|
||
}
|
||
function initMap(){
|
||
currentQ=0;score=0;mapAnswered=false;
|
||
// Both levels show mode tabs — level 6 just removes labels from the map
|
||
const tabs=document.querySelector('.map-mode-tabs');
|
||
tabs.style.display='flex';
|
||
showScreen('map-screen');
|
||
buildMap();
|
||
resetMapPaths();
|
||
nextMapQ();
|
||
}
|
||
function setMapMode(mode){mapMode=mode;document.getElementById('tab-name').classList.toggle('active',mode==='name');document.getElementById('tab-cap').classList.toggle('active',mode==='capital');initMap();}
|
||
function nextMapQ(){
|
||
if(currentQ>=questions.length){showResult(5);return;}
|
||
mapAnswered=false;mapTarget=questions[currentQ];
|
||
document.getElementById('map-prog').style.width=(currentQ/questions.length*100)+'%';
|
||
document.getElementById('map-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
|
||
document.getElementById('map-score-live').textContent=`✅ ${score} correctes`;
|
||
if(mapMode==='name'){document.getElementById('map-qlabel').textContent='TOCA LA COMARCA...';document.getElementById('map-qtext').textContent=mapTarget.emoji+' '+mapTarget.name;}
|
||
else{document.getElementById('map-qlabel').textContent='QUINA COMARCA TÉ LA CAPITAL...';document.getElementById('map-qtext').textContent='📍 '+mapTarget.capital;}
|
||
document.getElementById('map-qhint').textContent='';resetMapPaths();
|
||
}
|
||
function handleMapClick(path){
|
||
if(mapAnswered)return;
|
||
const clicked=path.getAttribute('data-name');const ok=clicked===mapTarget.name;mapAnswered=true;
|
||
if(ok){
|
||
path.style.fill='#4DBD6E';path.style.filter='drop-shadow(0 0 12px #4DBD6E)';
|
||
score++;showFeedback(true);document.getElementById('map-qhint').textContent='✅ Capital: '+mapTarget.capital;
|
||
setTimeout(()=>{path.style.fill=MTN_COLORS[clicked]||'#C97D10';path.style.filter='';currentQ++;nextMapQ();},1600);
|
||
}else{
|
||
path.classList.add('wrong-shake');path.style.fill='#F05C5C';showFeedback(false);
|
||
const cp=document.getElementById('mp_'+COMARCA_PATHS[mapTarget.name]?.id);
|
||
if(cp){cp.style.fill='#4DBD6E';cp.style.filter='drop-shadow(0 0 10px rgba(77,189,110,.9))';}
|
||
document.getElementById('map-qhint').textContent='❌ Era: '+mapTarget.name+' · Capital: '+mapTarget.capital;
|
||
setTimeout(()=>{path.classList.remove('wrong-shake');currentQ++;nextMapQ();},2400);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|