feat: app completa recordaLexia (fases 1-5)
App web familiar de rutinas visuales para niños con TDAH: muestra cada día el material del cole y las rutinas de tarde, con gamificación por monedas y tienda de recompensas. Multi-niño y bilingüe ES/CA. Uso doméstico/homelab. Backend (Spring Boot 3.5 / Java 21 / Gradle): - Dominio por capas, PostgreSQL + Liquibase, datos semilla. - API REST con DTOs: /today, toggle con monedas y bonos de bloque/día, monedero, tienda/canje, ajustes y CRUD del panel de padres. - Seguridad ligera por PIN (BCrypt + sesion en memoria), sin Keycloak. - Tests JUnit: generacion del dia, monedas/bonos con reversion, canje, seguridad. Frontend (Angular 19, standalone + signals): - Perfiles, Home (Tablero y Foco), Tienda y panel de padres (5 pestañas). - Tipografia OpenDyslexic conmutable (accesibilidad), i18n ES/CA, TTS y sonido. - Tokens de diseño fieles al handoff (paleta, animaciones, monedas voladoras). Empaquetado: - Docker multi-stage + docker-compose (PostgreSQL + backend + Nginx). - Decisiones de arquitectura documentadas en docs/adr.
This commit is contained in:
BIN
artifacts/App de rutinas visuales para TDAH/.thumbnail
Normal file
BIN
artifacts/App de rutinas visuales para TDAH/.thumbnail
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
630
artifacts/App de rutinas visuales para TDAH/Rutinas TDAH.dc.html
Normal file
630
artifacts/App de rutinas visuales para TDAH/Rutinas TDAH.dc.html
Normal file
@@ -0,0 +1,630 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||
html, body { margin:0; padding:0; height:100%; background:#EAF1F4; font-family:'Nunito',sans-serif; overflow:hidden; }
|
||||
@keyframes pop { 0%{transform:scale(1)} 40%{transform:scale(1.06)} 100%{transform:scale(1)} }
|
||||
@keyframes checkPop { 0%{transform:scale(0)} 60%{transform:scale(1.25)} 100%{transform:scale(1)} }
|
||||
@keyframes floatY { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
|
||||
@keyframes floatYb { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-14px)} }
|
||||
@keyframes walletBump { 0%{transform:scale(1)} 35%{transform:scale(1.22) rotate(-4deg)} 100%{transform:scale(1)} }
|
||||
@keyframes confFall { 0%{transform:translateY(-20vh) rotate(0)} 100%{transform:translateY(110vh) rotate(720deg)} }
|
||||
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-8px)} 40%{transform:translateX(8px)} 60%{transform:translateX(-6px)} 80%{transform:translateX(6px)} }
|
||||
@keyframes celebPop { 0%{transform:scale(.6);opacity:0} 60%{transform:scale(1.05)} 100%{transform:scale(1);opacity:1} }
|
||||
@keyframes ringGlow { 0%,100%{box-shadow:0 0 0 0 rgba(242,166,90,.35)} 50%{box-shadow:0 0 0 14px rgba(242,166,90,0)} }
|
||||
@keyframes slideUp { 0%{transform:translateY(30px);opacity:0} 100%{transform:translateY(0);opacity:1} }
|
||||
::-webkit-scrollbar{ width:10px;height:10px } ::-webkit-scrollbar-thumb{ background:#cfd9e0;border-radius:8px }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<div style="position:fixed;inset:0;font-family:'Nunito',sans-serif;color:#2A3142;overflow:hidden;background:radial-gradient(1100px 700px at 12% -10%, #FBF4E9 0%, transparent 55%), radial-gradient(1000px 700px at 110% 120%, #E2F0EC 0%, transparent 55%), #EFF4F6;">
|
||||
|
||||
<!-- soft organic blobs -->
|
||||
<div style="position:absolute;top:-120px;left:-100px;width:420px;height:420px;border-radius:48% 52% 60% 40%/55% 45% 60% 45%;background:#FCEBD3;opacity:.55;filter:blur(2px);"></div>
|
||||
<div style="position:absolute;bottom:-140px;right:-90px;width:460px;height:460px;border-radius:60% 40% 45% 55%/50% 55% 45% 50%;background:#D7ECE5;opacity:.6;filter:blur(2px);"></div>
|
||||
|
||||
<!-- ============ PROFILES ============ -->
|
||||
<sc-if value="{{ isProfiles }}" hint-placeholder-val="{{ true }}">
|
||||
<div style="position:relative;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:48px;padding:40px;">
|
||||
<div style="text-align:center;animation:slideUp .5s ease both;">
|
||||
<div style="font-size:60px;animation:floatY 3.5s ease-in-out infinite;">🌳</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:46px;letter-spacing:.5px;margin-top:6px;">{{ L.who }}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:36px;flex-wrap:wrap;justify-content:center;">
|
||||
<sc-for list="{{ profiles }}" as="p" hint-placeholder-count="3">
|
||||
<div style="{{ p.cardStyle }}">
|
||||
<button onClick="{{ p.select }}" style="all:unset;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:14px;">
|
||||
<div style="{{ p.ringStyle }}"><span style="font-size:78px;line-height:1;">{{ p.mascot }}</span></div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:30px;letter-spacing:.5px;">{{ p.name }}</div>
|
||||
<div style="display:flex;align-items:center;gap:7px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:6px 16px;font-weight:800;font-size:18px;">🪙 {{ p.coins }}</div>
|
||||
</button>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:14px;background:#F4F7F9;border-radius:999px;padding:6px 8px;">
|
||||
<button onClick="{{ p.ageDown }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;color:#7A879B;box-shadow:0 2px 5px rgba(0,0,0,.06);">−</button>
|
||||
<span style="font-weight:800;font-size:15px;color:#5A6B82;min-width:64px;text-align:center;">{{ p.ageLabel }}</span>
|
||||
<button onClick="{{ p.ageUp }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;color:#7A879B;box-shadow:0 2px 5px rgba(0,0,0,.06);">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<button onClick="{{ goPin }}" style="all:unset;cursor:pointer;display:flex;align-items:center;gap:9px;color:#8C99AB;font-weight:700;font-size:16px;background:rgba(255,255,255,.6);padding:10px 18px;border-radius:999px;">⚙️ {{ L.padres }}</button>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- ============ HOME ============ -->
|
||||
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
|
||||
<div style="position:relative;height:100%;display:flex;flex-direction:column;padding:22px 30px 88px 30px;">
|
||||
|
||||
<!-- header -->
|
||||
<div style="display:flex;align-items:center;gap:20px;flex:none;">
|
||||
<div style="flex:1;">
|
||||
<div style="font-family:'Nunito';font-weight:800;font-size:17px;color:#8C99AB;letter-spacing:.5px;">{{ L.hola }}, {{ profile.name }}! 👋</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:42px;line-height:1;margin-top:2px;">{{ dayName }} <span style="color:#9FB0BD;font-weight:500;font-size:30px;">{{ dayDate }}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- timer -->
|
||||
<div style="display:flex;align-items:center;gap:14px;background:#fff;border-radius:24px;padding:12px 20px 12px 14px;box-shadow:0 8px 22px rgba(40,60,100,.07);animation:ringGlow 2.6s ease-in-out infinite;">
|
||||
<div style="position:relative;width:74px;height:74px;border-radius:50%;background:{{ timerRing }};display:flex;align-items:center;justify-content:center;">
|
||||
<div style="width:56px;height:56px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-size:30px;">🐦</div>
|
||||
</div>
|
||||
<div style="line-height:1.05;">
|
||||
<div style="font-weight:800;font-size:13px;color:#9FB0BD;letter-spacing:.5px;">{{ L.salimos }}</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:30px;color:#E08A3C;">{{ timerMin }} <span style="font-size:16px;color:#C99B6A;">{{ L.min }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- wallet -->
|
||||
<div ref="{{ setWallet }}" style="{{ walletStyle }}">
|
||||
<span style="font-size:26px;">🪙</span>
|
||||
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:30px;">{{ coins }}</span>
|
||||
</div>
|
||||
<button onClick="{{ goStore }}" style="all:unset;cursor:pointer;width:58px;height:58px;border-radius:20px;background:#fff;box-shadow:0 8px 22px rgba(40,60,100,.07);display:flex;align-items:center;justify-content:center;font-size:28px;">🎁</button>
|
||||
</div>
|
||||
|
||||
<!-- global progress -->
|
||||
<div style="display:flex;align-items:center;gap:16px;margin:16px 0 14px;flex:none;">
|
||||
<div style="flex:1;height:18px;background:#E2EAEE;border-radius:999px;overflow:hidden;">
|
||||
<div style="height:100%;width:{{ globalPct }};background:linear-gradient(90deg,#7FBF6B,#5BC0BE);border-radius:999px;transition:width .5s cubic-bezier(.2,.8,.2,1);"></div>
|
||||
</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:20px;color:#5A6B82;white-space:nowrap;">{{ globalDone }}/{{ globalTotal }} {{ L.listo }} ✨</div>
|
||||
</div>
|
||||
|
||||
<!-- exam / homework alert (in-flow banner) -->
|
||||
<sc-if value="{{ hasEvents }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="display:flex;gap:12px;margin-bottom:14px;flex:none;">
|
||||
<sc-for list="{{ events }}" as="e" hint-placeholder-count="2">
|
||||
<div onClick="{{ e.speak }}" style="cursor:pointer;display:flex;align-items:center;gap:12px;background:{{ e.bg }};border:3px solid {{ e.border }};border-radius:18px;padding:10px 18px;box-shadow:0 6px 16px rgba(212,140,40,.12);">
|
||||
<span style="font-size:30px;">{{ e.icon }}</span>
|
||||
<div style="line-height:1.05;">
|
||||
<div style="font-weight:900;font-size:11px;letter-spacing:1px;color:{{ e.border }};">{{ e.kind }}</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:19px;color:#5a4326;">{{ e.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- EMPTY STATE -->
|
||||
<sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;text-align:center;">
|
||||
<div style="font-size:96px;animation:floatYb 3s ease-in-out infinite;">🏖️</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:40px;">{{ L.vacioT }}</div>
|
||||
<div style="font-size:24px;color:#7A879B;font-weight:700;">{{ L.vacioS }}</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- VARIANT A : board -->
|
||||
<sc-if value="{{ showA }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="flex:1;display:flex;gap:22px;min-height:0;">
|
||||
<!-- COLE -->
|
||||
<div style="flex:1.05;display:flex;flex-direction:column;min-height:0;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
|
||||
<span style="font-size:30px;">🎒</span>
|
||||
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:25px;">{{ L.cole }}</span>
|
||||
<span style="margin-left:auto;font-weight:800;color:#7A879B;font-size:16px;background:#fff;padding:5px 13px;border-radius:999px;">{{ coleDone }}/{{ coleTotal }}</span>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;display:grid;grid-template-columns:1fr 1fr;gap:14px;align-content:start;padding-right:4px;">
|
||||
<sc-for list="{{ coleItems }}" as="item" hint-placeholder-count="6">
|
||||
<div ref="{{ item.setRef }}" onClick="{{ item.toggle }}" style="{{ item.cardStyle }}">
|
||||
<div style="{{ item.tileStyle }}">{{ item.icon }}</div>
|
||||
<div style="flex:1;min-width:0;"><div style="{{ item.labelStyle }}">{{ item.label }}</div></div>
|
||||
<button onClick="{{ item.speak }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#F2F6F8;display:flex;align-items:center;justify-content:center;font-size:16px;flex:none;">🔊</button>
|
||||
<div style="{{ item.checkStyle }}">{{ item.check }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TARDE -->
|
||||
<div style="flex:1;display:flex;flex-direction:column;min-height:0;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
|
||||
<span style="font-size:30px;">🌙</span>
|
||||
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:25px;">{{ L.tarde }}</span>
|
||||
<span style="margin-left:auto;font-weight:800;color:#7A879B;font-size:16px;background:#fff;padding:5px 13px;border-radius:999px;">{{ tardeDone }}/{{ tardeTotal }}</span>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:14px;padding-right:4px;">
|
||||
<sc-for list="{{ tardeItems }}" as="item" hint-placeholder-count="5">
|
||||
<div ref="{{ item.setRef }}" onClick="{{ item.toggle }}" style="{{ item.cardStyle }}">
|
||||
<div style="{{ item.tileStyle }}">{{ item.icon }}</div>
|
||||
<div style="flex:1;min-width:0;"><div style="{{ item.labelStyle }}">{{ item.label }}</div></div>
|
||||
<button onClick="{{ item.speak }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#F2F6F8;display:flex;align-items:center;justify-content:center;font-size:16px;flex:none;">🔊</button>
|
||||
<div style="{{ item.checkStyle }}">{{ item.check }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- VARIANT B : focus -->
|
||||
<sc-if value="{{ showB }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:0;gap:22px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;background:{{ focus.tint }};color:{{ focus.color }};padding:9px 22px;border-radius:999px;font-family:'Fredoka',sans-serif;font-weight:700;font-size:22px;">{{ focus.blockIcon }} {{ focus.blockLabel }}</div>
|
||||
<div style="display:flex;align-items:center;gap:38px;">
|
||||
<button onClick="{{ bPrev }}" style="{{ bPrevStyle }}">‹</button>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:20px;animation:celebPop .35s ease both;">
|
||||
<div style="{{ focus.heroTile }}">{{ focus.icon }}</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:46px;text-align:center;text-wrap:balance;max-width:560px;line-height:1.05;">{{ focus.label }}</div>
|
||||
<div style="display:flex;gap:16px;align-items:center;">
|
||||
<button onClick="{{ focus.speak }}" style="all:unset;cursor:pointer;width:62px;height:62px;border-radius:50%;background:#fff;box-shadow:0 6px 18px rgba(40,60,100,.1);display:flex;align-items:center;justify-content:center;font-size:28px;">🔊</button>
|
||||
<button onClick="{{ bDone }}" style="{{ bDoneStyle }}">{{ focus.doneLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick="{{ bNext }}" style="{{ bNextStyle }}">›</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:8px;">
|
||||
<sc-for list="{{ dots }}" as="d" hint-placeholder-count="11">
|
||||
<div style="{{ d.style }}"></div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="font-weight:800;color:#7A879B;font-size:19px;">{{ L.quedan }} {{ remaining }} · {{ L.despues }}: {{ nextLabel }}</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- ============ STORE ============ -->
|
||||
<sc-if value="{{ isStore }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="position:relative;height:100%;display:flex;flex-direction:column;padding:24px 36px 36px;">
|
||||
<div style="display:flex;align-items:center;gap:18px;margin-bottom:22px;flex:none;">
|
||||
<button onClick="{{ goHome }}" style="all:unset;cursor:pointer;width:54px;height:54px;border-radius:18px;background:#fff;box-shadow:0 6px 16px rgba(40,60,100,.08);display:flex;align-items:center;justify-content:center;font-size:24px;">‹</button>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:38px;flex:1;">🎁 {{ L.tienda }}</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:10px 22px;font-family:'Fredoka';font-weight:700;font-size:30px;">🪙 {{ coins }}</div>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;display:grid;grid-template-columns:repeat(3,1fr);gap:20px;align-content:start;">
|
||||
<sc-for list="{{ rewards }}" as="r" hint-placeholder-count="6">
|
||||
<div style="{{ r.cardStyle }}">
|
||||
<div style="{{ r.tileStyle }}">{{ r.icon }}</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:22px;text-align:center;line-height:1.1;text-wrap:balance;min-height:52px;display:flex;align-items:center;">{{ r.name }}</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:5px 16px;font-family:'Fredoka';font-weight:700;font-size:22px;">🪙 {{ r.cost }}</div>
|
||||
<button onClick="{{ r.redeem }}" style="{{ r.btnStyle }}">{{ r.btnLabel }}</button>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<sc-if value="{{ toast.show }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="position:absolute;bottom:34px;left:50%;transform:translateX(-50%);background:#2A3142;color:#fff;padding:16px 28px;border-radius:999px;font-weight:800;font-size:21px;box-shadow:0 12px 30px rgba(0,0,0,.2);animation:slideUp .3s ease both;">{{ toast.msg }}</div>
|
||||
</sc-if>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- ============ PIN ============ -->
|
||||
<sc-if value="{{ isPin }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="position:relative;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;">
|
||||
<button onClick="{{ goProfiles }}" style="all:unset;cursor:pointer;position:absolute;top:26px;left:30px;color:#8C99AB;font-weight:800;font-size:18px;">‹ {{ L.volver }}</button>
|
||||
<div style="font-size:50px;">🔒</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:28px;">{{ L.pin }}</div>
|
||||
<div style="{{ pinDotsStyle }}">
|
||||
<sc-for list="{{ pinDots }}" as="d" hint-placeholder-count="4">
|
||||
<div style="{{ d.style }}"></div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,84px);gap:16px;">
|
||||
<sc-for list="{{ keys }}" as="k" hint-placeholder-count="12">
|
||||
<button onClick="{{ k.press }}" style="{{ k.style }}">{{ k.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="color:#9FB0BD;font-weight:700;font-size:15px;">PIN demo: 1 2 3 4</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- ============ PARENTS ============ -->
|
||||
<sc-if value="{{ isParents }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="position:relative;height:100%;display:flex;flex-direction:column;background:#F4F7F9;">
|
||||
<div style="display:flex;align-items:center;gap:16px;padding:18px 30px;background:#fff;border-bottom:1px solid #E6ECF0;flex:none;">
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:26px;flex:none;">👪 {{ L.padres }}</div>
|
||||
<div style="flex:1;display:flex;gap:8px;justify-content:center;">
|
||||
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="5">
|
||||
<button onClick="{{ t.select }}" style="{{ t.style }}">{{ t.icon }} {{ t.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<button onClick="{{ goHome }}" style="all:unset;cursor:pointer;background:#2A3142;color:#fff;font-weight:800;font-size:15px;padding:11px 20px;border-radius:14px;flex:none;">🔓 {{ L.salir }}</button>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow:auto;padding:26px 30px;">
|
||||
<!-- HORARIO -->
|
||||
<sc-if value="{{ tabHorario }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Horario semanal · material de cada día</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;">
|
||||
<sc-for list="{{ schedule }}" as="day" hint-placeholder-count="5">
|
||||
<div style="background:#fff;border-radius:20px;padding:16px;box-shadow:0 4px 14px rgba(40,60,100,.05);min-height:230px;">
|
||||
<div style="font-family:'Fredoka';font-weight:700;font-size:18px;margin-bottom:12px;text-align:center;color:#5A6B82;">{{ day.name }}</div>
|
||||
<div style="display:flex;flex-direction:column;gap:9px;">
|
||||
<sc-for list="{{ day.acts }}" as="a" hint-placeholder-count="2">
|
||||
<div style="display:flex;align-items:center;gap:9px;background:{{ a.bg }};border-radius:12px;padding:8px 11px;">
|
||||
<span style="font-size:20px;">{{ a.icon }}</span>
|
||||
<span style="font-weight:800;font-size:14px;color:{{ a.color }};">{{ a.name }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
<button style="all:unset;cursor:pointer;text-align:center;border:2px dashed #D3DCE3;border-radius:12px;padding:7px;color:#9FB0BD;font-weight:800;font-size:13px;">+ Añadir</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- MATERIALES -->
|
||||
<sc-if value="{{ tabMateriales }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Actividades y su material</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:18px;">
|
||||
<sc-for list="{{ activities }}" as="act" hint-placeholder-count="4">
|
||||
<div style="background:#fff;border-radius:20px;padding:18px;box-shadow:0 4px 14px rgba(40,60,100,.05);border-left:8px solid {{ act.color }};">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
|
||||
<span style="font-size:26px;">{{ act.icon }}</span>
|
||||
<span style="font-family:'Fredoka';font-weight:700;font-size:21px;">{{ act.name }}</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:9px;">
|
||||
<sc-for list="{{ act.items }}" as="m" hint-placeholder-count="3">
|
||||
<div style="display:flex;align-items:center;gap:7px;background:#F4F7F9;border-radius:999px;padding:7px 14px;font-weight:800;font-size:14px;color:#5A6B82;">{{ m.i }} {{ m.n }}</div>
|
||||
</sc-for>
|
||||
<button style="all:unset;cursor:pointer;background:#F4F7F9;border:2px dashed #D3DCE3;border-radius:999px;padding:6px 14px;color:#9FB0BD;font-weight:800;font-size:14px;">+ material</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:20px;min-height:120px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:20px;">+ Nueva actividad</button>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- EVENTOS -->
|
||||
<sc-if value="{{ tabEventos }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Exámenes y deberes</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;max-width:680px;">
|
||||
<sc-for list="{{ pEvents }}" as="e" hint-placeholder-count="3">
|
||||
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 20px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
|
||||
<div style="width:48px;height:48px;border-radius:14px;background:{{ e.bg }};display:flex;align-items:center;justify-content:center;font-size:26px;">{{ e.icon }}</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-weight:900;font-size:12px;letter-spacing:1px;color:{{ e.color }};">{{ e.kind }}</div>
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:20px;">{{ e.title }}</div>
|
||||
</div>
|
||||
<div style="font-weight:800;color:#7A879B;font-size:16px;">📅 {{ e.date }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:18px;padding:18px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:19px;">+ Añadir examen o deberes</button>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- RUTINAS -->
|
||||
<sc-if value="{{ tabRutinas }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Rutinas de la tarde (por día)</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:18px;">
|
||||
<sc-for list="{{ weekChips }}" as="w" hint-placeholder-count="5">
|
||||
<button onClick="{{ w.select }}" style="{{ w.style }}">{{ w.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:11px;max-width:620px;">
|
||||
<sc-for list="{{ routineList }}" as="r" hint-placeholder-count="5">
|
||||
<div style="display:flex;align-items:center;gap:14px;background:#fff;border-radius:16px;padding:13px 18px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
|
||||
<span style="font-size:26px;">{{ r.icon }}</span>
|
||||
<span style="flex:1;font-family:'Fredoka';font-weight:600;font-size:19px;">{{ r.name }}</span>
|
||||
<span style="cursor:grab;color:#C3CDD6;font-size:20px;">⠿</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:16px;padding:14px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:18px;">+ Añadir rutina</button>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- GAMIFICACIÓN -->
|
||||
<sc-if value="{{ tabJuego }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:18px;">Recompensas y ajustes</div>
|
||||
<div style="display:flex;flex-direction:column;gap:14px;max-width:560px;">
|
||||
<sc-for list="{{ steppers }}" as="s" hint-placeholder-count="3">
|
||||
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 22px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
|
||||
<span style="font-size:30px;">{{ s.icon }}</span>
|
||||
<div style="flex:1;"><div style="font-family:'Fredoka';font-weight:600;font-size:19px;">{{ s.label }}</div><div style="font-size:14px;color:#9FB0BD;font-weight:700;">{{ s.hint }}</div></div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<button onClick="{{ s.dec }}" style="all:unset;cursor:pointer;width:42px;height:42px;border-radius:50%;background:#F4F7F9;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:22px;color:#7A879B;">−</button>
|
||||
<span style="font-family:'Fredoka';font-weight:700;font-size:26px;min-width:56px;text-align:center;">🪙 {{ s.value }}</span>
|
||||
<button onClick="{{ s.inc }}" style="all:unset;cursor:pointer;width:42px;height:42px;border-radius:50%;background:#F4F7F9;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:22px;color:#7A879B;">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
<sc-for list="{{ toggles }}" as="tg" hint-placeholder-count="2">
|
||||
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 22px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
|
||||
<span style="font-size:30px;">{{ tg.icon }}</span>
|
||||
<div style="flex:1;font-family:'Fredoka';font-weight:600;font-size:19px;">{{ tg.label }}</div>
|
||||
<button onClick="{{ tg.toggle }}" style="{{ tg.trackStyle }}"><span style="{{ tg.knobStyle }}"></span></button>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</sc-if>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- ============ BOTTOM DEMO DOCK (prototype controls) ============ -->
|
||||
<sc-if value="{{ showDock }}" hint-placeholder-val="{{ true }}">
|
||||
<div style="position:fixed;bottom:0;left:0;right:0;z-index:40;display:flex;align-items:center;justify-content:center;gap:8px;padding:8px;background:rgba(255,255,255,.78);backdrop-filter:blur(8px);border-top:1px solid rgba(0,0,0,.05);">
|
||||
<button onClick="{{ goProfiles }}" style="{{ dockBtn }}">👤 {{ L.perfiles }}</button>
|
||||
<button onClick="{{ goPin }}" style="{{ dockBtn }}">⚙️ {{ L.padres }}</button>
|
||||
<button onClick="{{ goStore }}" style="{{ dockBtn }}">🎁 {{ L.tienda }}</button>
|
||||
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
|
||||
<button onClick="{{ setVarA }}" style="{{ varAStyle }}">A · Tablero</button>
|
||||
<button onClick="{{ setVarB }}" style="{{ varBStyle }}">B · Foco</button>
|
||||
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
|
||||
<button onClick="{{ toggleEmpty }}" style="{{ dockBtn }}">{{ emptyLabel }}</button>
|
||||
<button onClick="{{ reset }}" style="{{ dockBtn }}">↺ {{ L.reiniciar }}</button>
|
||||
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
|
||||
<button onClick="{{ toggleLang }}" style="{{ langBtn }}">{{ langLabel }}</button>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- flying coins layer -->
|
||||
<div style="position:fixed;inset:0;pointer-events:none;z-index:60;">{{ flyingCoinEls }}</div>
|
||||
|
||||
<!-- CELEBRATION -->
|
||||
<sc-if value="{{ celebrate }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;background:rgba(35,49,66,.45);backdrop-filter:blur(3px);">
|
||||
<div style="position:absolute;inset:0;overflow:hidden;pointer-events:none;">{{ confettiEls }}</div>
|
||||
<div style="position:relative;background:#fff;border-radius:36px;padding:46px 64px;text-align:center;box-shadow:0 30px 80px rgba(0,0,0,.3);animation:celebPop .45s cubic-bezier(.2,.8,.2,1) both;">
|
||||
<div style="font-size:104px;animation:floatYb 2.2s ease-in-out infinite;">🦊🎉</div>
|
||||
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:54px;color:#2A3142;margin-top:8px;">{{ L.todoListo }}</div>
|
||||
<div style="font-size:24px;color:#7A879B;font-weight:700;margin-top:6px;">{{ L.biengrande }}</div>
|
||||
<div style="display:inline-flex;align-items:center;gap:9px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:12px 26px;font-family:'Fredoka';font-weight:700;font-size:28px;margin-top:22px;">🪙 +{{ coinsPerDay }}</div>
|
||||
<div style="margin-top:26px;">
|
||||
<button onClick="{{ dismissCeleb }}" style="all:unset;cursor:pointer;background:linear-gradient(135deg,#7FBF6B,#5BC0BE);color:#fff;font-family:'Fredoka';font-weight:700;font-size:26px;padding:16px 44px;border-radius:999px;box-shadow:0 10px 26px rgba(91,192,190,.4);">{{ L.genial }} 👍</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1280,"height":800}}">
|
||||
class Component extends DCLogic {
|
||||
D = {
|
||||
es:{ who:'¿QUIÉN ENTRA HOY?', cole:'PARA EL COLE', tarde:'ESTA TARDE', salimos:'SALIMOS EN', min:'min', todoListo:'¡TODO LISTO!', biengrande:'¡Lo has hecho genial!', listo:'listo', quedan:'Quedan', despues:'Después', genial:'¡GENIAL!', tienda:'TIENDA DE PREMIOS', canjear:'CANJEAR', faltan:'Te faltan', monedas:'monedas', padres:'Padres', salir:'Salir', pin:'Introduce el PIN', vacioT:'HOY NO HAY COLE', vacioS:'¡Disfruta de la tarde! 🎉', volver:'Volver', perfiles:'Perfiles', reiniciar:'Reiniciar', hola:'Hola', diaVacio:'Día vacío', diaNormal:'Día normal', hecho:'¡HECHO!', anios:'años' },
|
||||
ca:{ who:'QUI ENTRA AVUI?', cole:"PER A L'ESCOLA", tarde:'AQUESTA TARDA', salimos:'SORTIM EN', min:'min', todoListo:'TOT FET!', biengrande:'Ho has fet genial!', listo:'fet', quedan:'En queden', despues:'Després', genial:'GENIAL!', tienda:'BOTIGA DE PREMIS', canjear:'BESCANVIAR', faltan:'Et falten', monedas:'monedes', padres:'Pares', salir:'Sortir', pin:'Introdueix el PIN', vacioT:'AVUI NO HI HA ESCOLA', vacioS:'Gaudeix de la tarda! 🎉', volver:'Tornar', perfiles:'Perfils', reiniciar:'Reiniciar', hola:'Hola', diaVacio:'Dia buit', diaNormal:'Dia normal', hecho:'FET!', anios:'anys' }
|
||||
};
|
||||
|
||||
profilesData = [
|
||||
{ id:'nora', name:'NORA', mascot:'🦊', color:'#F2A65A', coins:42 },
|
||||
{ id:'leo', name:'LEO', mascot:'🐢', color:'#5BC0BE', coins:28 },
|
||||
{ id:'mia', name:'MÍA', mascot:'🦉', color:'#A78BD0', coins:55 },
|
||||
];
|
||||
|
||||
coleData = [
|
||||
{ id:'estuche', icon:'✏️', label:'ESTUCHE', labelCa:'ESTOIG', color:'#F2A65A' },
|
||||
{ id:'mates', icon:'📘', label:'LIBRO DE MATES', labelCa:'LLIBRE DE MATES', color:'#5B8DEF' },
|
||||
{ id:'flauta', icon:'🎵', label:'FLAUTA', labelCa:'FLAUTA', color:'#A78BD0' },
|
||||
{ id:'ropa', icon:'👕', label:'ROPA DE GIMNASIA', labelCa:'ROBA D\u2019EDUCACIÓ FÍSICA', color:'#7FBF6B' },
|
||||
{ id:'zapas', icon:'👟', label:'ZAPATILLAS', labelCa:'SABATILLES', color:'#7FBF6B' },
|
||||
{ id:'almuerzo', icon:'🍎', label:'ALMUERZO', labelCa:'ESMORZAR', color:'#F4C95D' },
|
||||
];
|
||||
tardeData = [
|
||||
{ id:'mochila', icon:'🎒', label:'DESHACER LA MOCHILA', labelCa:'BUIDAR LA MOTXILLA', color:'#5BC0BE' },
|
||||
{ id:'merienda', icon:'🥪', label:'MERENDAR', labelCa:'BERENAR', color:'#F2A65A' },
|
||||
{ id:'deberes', icon:'📝', label:'HACER LOS DEBERES', labelCa:'FER ELS DEURES', color:'#5B8DEF' },
|
||||
{ id:'piano', icon:'🎹', label:'PRACTICAR PIANO', labelCa:'PRACTICAR PIANO', color:'#A78BD0' },
|
||||
{ id:'mesa', icon:'🍽️', label:'RECOGER LA MESA', labelCa:'PARAR TAULA', color:'#7FBF6B' },
|
||||
];
|
||||
|
||||
rewardsData = [
|
||||
{ icon:'🎮', name:'30 MIN DE TABLET', nameCa:'30 MIN DE TAULETA', cost:20, color:'#5B8DEF' },
|
||||
{ icon:'🍿', name:'PELI EN FAMILIA', nameCa:'PEL·LÍCULA', cost:50, color:'#A78BD0' },
|
||||
{ icon:'🛝', name:'TARDE EN EL PARQUE', nameCa:'TARDA AL PARC', cost:40, color:'#7FBF6B' },
|
||||
{ icon:'🍕', name:'ELIJO LA CENA', nameCa:'TRIO EL SOPAR', cost:30, color:'#F2A65A' },
|
||||
{ icon:'🌙', name:'30 MIN MÁS DESPIERTO', nameCa:'30 MIN MÉS DESPERT', cost:60, color:'#5BC0BE' },
|
||||
{ icon:'🦖', name:'SORPRESA DINO', nameCa:'SORPRESA DINO', cost:80, color:'#EC8FA4' },
|
||||
];
|
||||
|
||||
activitiesData = [
|
||||
{ name:'Gimnasia', icon:'🤸', color:'#7FBF6B', items:[{i:'👕',n:'Equipación'},{i:'👟',n:'Zapatillas'},{i:'🧺',n:'Toalla'},{i:'💧',n:'Agua'}] },
|
||||
{ name:'Música', icon:'🎵', color:'#A78BD0', items:[{i:'🎵',n:'Flauta'},{i:'📒',n:'Libreta'}] },
|
||||
{ name:'Matemáticas', icon:'📘', color:'#5B8DEF', items:[{i:'📘',n:'Libro'},{i:'📐',n:'Regla'},{i:'✏️',n:'Estuche'}] },
|
||||
{ name:'Lengua', icon:'📖', color:'#EC8FA4', items:[{i:'📖',n:'Lectura'},{i:'📓',n:'Cuaderno'}] },
|
||||
];
|
||||
|
||||
state = {
|
||||
screen:'profiles', lang:'es', variant:'A', profileId:'nora', coins:42,
|
||||
done:{}, focusIndex:0, celebrate:false, confetti:[], flyingCoins:[], walletPulse:false,
|
||||
emptyDemo:false, pinInput:'', parentsTab:'horario', routineDay:1,
|
||||
coinsPerTask:5, coinsPerBlock:10, coinsPerDay:20, sndOn:true, ttsOn:true,
|
||||
ages:{ nora:7, leo:9, mia:6 }, timer:18*60,
|
||||
};
|
||||
|
||||
componentDidMount(){ this._t = setInterval(()=>{ this.setState(s=>({ timer: s.timer>0 ? s.timer-1 : 0 })); }, 1000); }
|
||||
componentWillUnmount(){ clearInterval(this._t); }
|
||||
|
||||
L(){ return this.D[this.state.lang]; }
|
||||
allTasks(){ return this.coleData.concat(this.tardeData); }
|
||||
cardRefs = {};
|
||||
setCardRef = (id)=> (el)=>{ if(el) this.cardRefs[id]=el; };
|
||||
|
||||
speak(text){ if(!this.state.ttsOn) return; try{ const u=new SpeechSynthesisUtterance(text); u.lang=this.state.lang==='ca'?'ca-ES':'es-ES'; u.rate=0.9; u.pitch=1.1; speechSynthesis.cancel(); speechSynthesis.speak(u);}catch(e){} }
|
||||
ding(){ if(!this.state.sndOn) return; try{ const a=new (window.AudioContext||window.webkitAudioContext)(); const o=a.createOscillator(), g=a.createGain(); o.connect(g); g.connect(a.destination); o.type='sine'; o.frequency.setValueAtTime(660,a.currentTime); o.frequency.setValueAtTime(990,a.currentTime+0.09); g.gain.setValueAtTime(0.12,a.currentTime); g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+0.32); o.start(); o.stop(a.currentTime+0.33);}catch(e){} }
|
||||
|
||||
toggleTask(id){
|
||||
const done={...this.state.done}; const was=done[id]; done[id]=!was;
|
||||
this.setState({done}, ()=>{ if(!was){ this.launchCoin(id); this.ding(); this.maybeCelebrate(); } });
|
||||
}
|
||||
launchCoin(id){
|
||||
const card=this.cardRefs[id], wal=this.walletEl;
|
||||
if(!card||!wal){ this.addCoins(); return; }
|
||||
const c=card.getBoundingClientRect(), w=wal.getBoundingClientRect();
|
||||
const x=c.left+30, y=c.top+30;
|
||||
const dx=(w.left+w.width/2)-x, dy=(w.top+w.height/2)-y;
|
||||
const key='c'+Date.now()+Math.random();
|
||||
this.setState(s=>({flyingCoins:[...s.flyingCoins,{key,x,y,dx,dy}]}));
|
||||
setTimeout(()=>{ this.setState(s=>({flyingCoins:s.flyingCoins.filter(f=>f.key!==key)})); this.addCoins(); }, 760);
|
||||
}
|
||||
addCoins(){ this.setState(s=>({coins:s.coins+s.coinsPerTask, walletPulse:!s.walletPulse})); }
|
||||
maybeCelebrate(){
|
||||
const total=this.allTasks().length;
|
||||
const d=this.allTasks().filter(t=>this.state.done[t.id]).length;
|
||||
if(d===total && total>0 && !this.state.celebrate){
|
||||
setTimeout(()=>this.setState(s=>({celebrate:true, confetti:this.makeConfetti(), coins:s.coins+s.coinsPerDay})), 450);
|
||||
}
|
||||
}
|
||||
makeConfetti(){ const cs=['#5B8DEF','#5BC0BE','#F2A65A','#A78BD0','#7FBF6B','#EC8FA4','#F4C95D']; return Array.from({length:100},(_,i)=>({key:i,left:Math.random()*100,delay:Math.random()*0.7,dur:2.1+Math.random()*1.8,color:cs[i%cs.length],size:7+Math.random()*11})); }
|
||||
dismissCeleb(){ this.setState({celebrate:false, confetti:[]}); }
|
||||
|
||||
// variant B
|
||||
focusTask(){ const t=this.allTasks(); let i=Math.min(this.state.focusIndex, t.length-1); return t[i]; }
|
||||
bDone(){ const t=this.focusTask(); if(!t) return; if(!this.state.done[t.id]) this.toggleTask(t.id); const all=this.allTasks(); let n=this.state.focusIndex+1; while(n<all.length && this.state.done[all[n].id]) n++; if(n<all.length) this.setState({focusIndex:n}); }
|
||||
bNav(d){ const all=this.allTasks(); let i=this.state.focusIndex+d; if(i<0)i=0; if(i>all.length-1)i=all.length-1; this.setState({focusIndex:i}); }
|
||||
|
||||
selectProfile(p){ this.setState({screen:'home', profileId:p.id, coins:p.coins, done:{}, focusIndex:0, celebrate:false, emptyDemo:false}); }
|
||||
changeAge(id,d){ this.setState(s=>{ const ages={...s.ages}; ages[id]=Math.max(4,Math.min(12,(ages[id]||7)+d)); return {ages}; }); }
|
||||
reset(){ this.setState({done:{}, focusIndex:0, celebrate:false, confetti:[], emptyDemo:false}); }
|
||||
|
||||
pressPin(d){ if(d==='del'){ this.setState(s=>({pinInput:s.pinInput.slice(0,-1)})); return; } const v=this.state.pinInput+d; if(v.length>=4){ if(v==='1234'){ this.setState({screen:'parents', pinInput:''}); } else { this.setState({pinInput:''}); } } else { this.setState({pinInput:v}); } }
|
||||
|
||||
redeem(r){ if(this.state.coins>=r.cost){ this.setState(s=>({coins:s.coins-r.cost}), ()=>this.flash('🎉 ¡'+(this.state.lang==='ca'?'Bescanviat':'Canjeado')+'!')); } else { this.flash('🙂 '+this.L().faltan+' '+(r.cost-this.state.coins)); } }
|
||||
flash(msg){ clearTimeout(this._tt); this.setState({toast:{show:true,msg}}); this._tt=setTimeout(()=>this.setState({toast:{show:false,msg}}),1700); }
|
||||
|
||||
step(key,d,min,max){ this.setState(s=>({[key]:Math.max(min,Math.min(max,s[key]+d))})); }
|
||||
|
||||
renderVals(){
|
||||
const s=this.state, L=this.L(), lang=s.lang;
|
||||
const profile=this.profilesData.find(p=>p.id===s.profileId)||this.profilesData[0];
|
||||
const lbl=(it)=> lang==='ca'? it.labelCa : it.label;
|
||||
const tint=(hex)=> hex+'24';
|
||||
|
||||
const baseCard=(it)=>{ const done=!!s.done[it.id];
|
||||
const card='display:flex;align-items:center;gap:16px;background:'+(done?tint(it.color):'#fff')+';border:3px solid '+(done?it.color:'#EEF2F6')+';border-radius:26px;padding:14px 16px;cursor:pointer;box-shadow:0 6px 16px rgba(40,60,100,.06);transition:transform .15s,border-color .25s,background .25s;min-height:92px;'+(done?'animation:pop .35s ease;':'');
|
||||
const tile='width:66px;height:66px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:38px;flex:none;background:'+tint(it.color)+';'+(done?'opacity:.85;':'');
|
||||
const label='font-family:\'Fredoka\',sans-serif;font-weight:600;font-size:21px;letter-spacing:.2px;color:'+(done?'#7A879B':'#2A3142')+';line-height:1.05;text-transform:uppercase;';
|
||||
const check='width:60px;height:60px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:30px;color:#fff;font-weight:900;border:3px solid '+(done?it.color:'#DCE3EA')+';background:'+(done?it.color:'#fff')+';'+(done?'animation:checkPop .35s ease;':'');
|
||||
return {...it, done, label:lbl(it), cardStyle:card, tileStyle:tile, labelStyle:label, checkStyle:check, check:done?'✓':'',
|
||||
setRef:this.setCardRef(it.id), toggle:()=>this.toggleTask(it.id), speak:(e)=>{e&&e.stopPropagation&&e.stopPropagation();this.speak(lbl(it));}};
|
||||
};
|
||||
const coleItems=this.coleData.map(baseCard);
|
||||
const tardeItems=this.tardeData.map(baseCard);
|
||||
const coleDone=coleItems.filter(i=>i.done).length, tardeDone=tardeItems.filter(i=>i.done).length;
|
||||
const gTotal=coleItems.length+tardeItems.length, gDone=coleDone+tardeDone;
|
||||
|
||||
// focus B
|
||||
const all=this.allTasks(); const fi=Math.min(s.focusIndex,all.length-1); const ft=all[fi];
|
||||
const isCole=fi<this.coleData.length; const fdone=!!s.done[ft.id];
|
||||
const heroTile='width:200px;height:200px;border-radius:46px;display:flex;align-items:center;justify-content:center;font-size:120px;background:'+tint(ft.color)+';box-shadow:0 16px 40px '+ft.color+'33;animation:floatYb 3s ease-in-out infinite;';
|
||||
const navBtn=(en)=>'all:unset;cursor:pointer;width:64px;height:64px;border-radius:50%;background:#fff;box-shadow:0 6px 16px rgba(40,60,100,.08);display:flex;align-items:center;justify-content:center;font-size:34px;color:#9FB0BD;'+(en?'':'opacity:.3;pointer-events:none;');
|
||||
const bDoneStyle='all:unset;cursor:pointer;background:'+(fdone?'#7FBF6B':'linear-gradient(135deg,'+ft.color+',#5BC0BE)')+';color:#fff;font-family:\'Fredoka\';font-weight:700;font-size:30px;padding:18px 46px;border-radius:999px;box-shadow:0 12px 28px '+ft.color+'55;';
|
||||
const remaining=all.filter(t=>!s.done[t.id]).length;
|
||||
const nextT=all.slice(fi+1).find(t=>!s.done[t.id]) || all.find(t=>!s.done[t.id]);
|
||||
const dots=all.map((t,i)=>({ style:'width:14px;height:14px;border-radius:50%;background:'+(s.done[t.id]?'#7FBF6B':(i===fi?'#F2A65A':'#D3DCE3'))+';transition:.3s;'+(i===fi?'transform:scale(1.4);':'') }));
|
||||
|
||||
// events
|
||||
const evRaw=[ {icon:'📋',kind:'EXAMEN',kindCa:'EXAMEN',title:lang==='ca'?'Examen de Llengua':'Examen de Lengua',border:'#E08A3C',bg:'#FFF1DE'},
|
||||
{icon:'📎',kind:'DEBERES',kindCa:'DEURES',title:lang==='ca'?'Fitxa de mates':'Ficha de mates',border:'#5B8DEF',bg:'#E9F1FF'} ];
|
||||
const events=evRaw.map(e=>({...e, kind:lang==='ca'?e.kindCa:e.kind, speak:()=>this.speak((lang==='ca'?e.kindCa:e.kind)+'. '+e.title)}));
|
||||
|
||||
// rewards
|
||||
const rewards=this.rewardsData.map(r=>{ const can=s.coins>=r.cost; const name=lang==='ca'?r.nameCa:r.name;
|
||||
return {...r, name, cardStyle:'display:flex;flex-direction:column;align-items:center;gap:12px;background:#fff;border-radius:26px;padding:20px 16px 18px;box-shadow:0 8px 22px rgba(40,60,100,.06);border-top:6px solid '+r.color+';',
|
||||
tileStyle:'width:88px;height:88px;border-radius:26px;display:flex;align-items:center;justify-content:center;font-size:52px;background:'+tint(r.color)+';',
|
||||
btnStyle:'all:unset;cursor:'+(can?'pointer':'default')+';background:'+(can?r.color:'#EEF2F6')+';color:'+(can?'#fff':'#9FB0BD')+';font-family:\'Fredoka\';font-weight:700;font-size:18px;padding:11px 26px;border-radius:999px;white-space:nowrap;',
|
||||
btnLabel:can?L.canjear:(L.faltan+' '+(r.cost-s.coins)),
|
||||
redeem:()=>this.redeem(r)};
|
||||
});
|
||||
|
||||
// pin
|
||||
const pinDots=[0,1,2,3].map(i=>({ style:'width:20px;height:20px;border-radius:50%;background:'+(i<s.pinInput.length?'#5BC0BE':'#D3DCE3')+';' }));
|
||||
const keyDef=['1','2','3','4','5','6','7','8','9','','0','del'];
|
||||
const keys=keyDef.map(k=>({ label:k==='del'?'⌫':k, press:k===''?(()=>{}):(()=>this.pressPin(k)),
|
||||
style:'all:'+(k===''?'unset':'unset')+';cursor:'+(k===''?'default':'pointer')+';width:84px;height:84px;border-radius:24px;background:'+(k===''?'transparent':'#fff')+';'+(k===''?'':'box-shadow:0 5px 14px rgba(40,60,100,.07);')+'display:flex;align-items:center;justify-content:center;font-family:\'Fredoka\';font-weight:600;font-size:34px;color:#2A3142;' }));
|
||||
|
||||
// parents tabs
|
||||
const tabDefs=[ {id:'horario',icon:'📅',label:lang==='ca'?'Horari':'Horario'}, {id:'materiales',icon:'🎒',label:lang==='ca'?'Materials':'Materiales'}, {id:'eventos',icon:'📋',label:lang==='ca'?'Esdeveniments':'Eventos'}, {id:'rutinas',icon:'🌙',label:lang==='ca'?'Rutines':'Rutinas'}, {id:'juego',icon:'🪙',label:lang==='ca'?'Recompenses':'Recompensas'} ];
|
||||
const tabs=tabDefs.map(t=>({...t, select:()=>this.setState({parentsTab:t.id}), style:'all:unset;cursor:pointer;padding:10px 18px;border-radius:14px;font-family:\'Fredoka\';font-weight:600;font-size:16px;background:'+(s.parentsTab===t.id?'#2A3142':'#F4F7F9')+';color:'+(s.parentsTab===t.id?'#fff':'#5A6B82')+';' }));
|
||||
|
||||
const actColor={Gimnasia:'#7FBF6B',Música:'#A78BD0',Matemáticas:'#5B8DEF',Lengua:'#EC8FA4',Plástica:'#F2A65A'};
|
||||
const actIcon={Gimnasia:'🤸',Música:'🎵',Matemáticas:'📘',Lengua:'📖',Plástica:'🎨'};
|
||||
const sched=[['Matemáticas','Lengua'],['Matemáticas','Música','Gimnasia'],['Lengua','Plástica'],['Matemáticas','Música'],['Gimnasia','Lengua']];
|
||||
const dayNames=lang==='ca'?['DILLUNS','DIMARTS','DIMECRES','DIJOUS','DIVENDRES']:['LUNES','MARTES','MIÉRCOLES','JUEVES','VIERNES'];
|
||||
const schedule=sched.map((acts,i)=>({ name:dayNames[i], acts:acts.map(a=>({name:a,icon:actIcon[a]||'📗',color:actColor[a]||'#5A6B82',bg:(actColor[a]||'#5A6B82')+'1f'})) }));
|
||||
const activities=this.activitiesData.map(a=>({...a}));
|
||||
const pEvents=[ {icon:'📋',kind:'EXAMEN',color:'#E08A3C',bg:'#FFF1DE',title:lang==='ca'?'Examen de Llengua':'Examen de Lengua',date:'Mar 17'},
|
||||
{icon:'📎',kind:'DEBERES',color:'#5B8DEF',bg:'#E9F1FF',title:lang==='ca'?'Fitxa de mates':'Ficha de mates',date:'Mar 17'},
|
||||
{icon:'📋',kind:'EXAMEN',color:'#E08A3C',bg:'#FFF1DE',title:lang==='ca'?'Examen de Mates':'Examen de Mates',date:'Jue 19'} ];
|
||||
const weekChips=dayNames.map((d,i)=>({ label:d.slice(0,3), select:()=>this.setState({routineDay:i}), style:'all:unset;cursor:pointer;padding:9px 16px;border-radius:12px;font-weight:800;font-size:14px;background:'+(s.routineDay===i?'#5BC0BE':'#fff')+';color:'+(s.routineDay===i?'#fff':'#5A6B82')+';box-shadow:0 3px 10px rgba(40,60,100,.05);' }));
|
||||
const baseRoutines=[ {icon:'🎒',name:lang==='ca'?'Buidar la motxilla':'Deshacer la mochila'},{icon:'🥪',name:lang==='ca'?'Berenar':'Merendar'},{icon:'📝',name:lang==='ca'?'Fer els deures':'Hacer los deberes'},{icon:'🍽️',name:lang==='ca'?'Parar taula':'Recoger la mesa'} ];
|
||||
const routineList=(s.routineDay===1||s.routineDay===3)? baseRoutines.concat([{icon:'🎹',name:lang==='ca'?'Practicar piano':'Practicar piano'}]) : baseRoutines;
|
||||
const steppers=[ {icon:'✅',label:lang==='ca'?'Monedes per tasca':'Monedas por tarea',hint:lang==='ca'?'En completar cada tasca':'Al completar cada tarea',value:s.coinsPerTask,inc:()=>this.step('coinsPerTask',1,1,20),dec:()=>this.step('coinsPerTask',-1,1,20)},
|
||||
{icon:'🎯',label:lang==='ca'?'Monedes per bloc':'Monedas por bloque',hint:lang==='ca'?'En acabar cole o tarda':'Al terminar cole o tarde',value:s.coinsPerBlock,inc:()=>this.step('coinsPerBlock',1,0,50),dec:()=>this.step('coinsPerBlock',-1,0,50)},
|
||||
{icon:'🏆',label:lang==='ca'?'Bonus dia complet':'Bonus día completo',hint:lang==='ca'?'En acabar-ho tot':'Al completar todo el día',value:s.coinsPerDay,inc:()=>this.step('coinsPerDay',5,0,100),dec:()=>this.step('coinsPerDay',-5,0,100)} ];
|
||||
const tgl=(on)=>'all:unset;cursor:pointer;width:62px;height:34px;border-radius:999px;background:'+(on?'#7FBF6B':'#D3DCE3')+';display:flex;align-items:center;padding:3px;transition:.2s;';
|
||||
const knob=(on)=>'width:28px;height:28px;border-radius:50%;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);transform:translateX('+(on?'28px':'0')+');transition:.2s;';
|
||||
const toggles=[ {icon:'🔊',label:lang==='ca'?'So en marcar':'Sonido al marcar',trackStyle:tgl(s.sndOn),knobStyle:knob(s.sndOn),toggle:()=>this.setState(p=>({sndOn:!p.sndOn}))},
|
||||
{icon:'🗣️',label:lang==='ca'?'Lectura en veu alta':'Lectura en voz alta',trackStyle:tgl(s.ttsOn),knobStyle:knob(s.ttsOn),toggle:()=>this.setState(p=>({ttsOn:!p.ttsOn}))} ];
|
||||
|
||||
// timer ring
|
||||
const frac=s.timer/(20*60); const deg=Math.round(frac*360);
|
||||
const timerRing='conic-gradient(#F2A65A '+deg+'deg, #FFE6C7 0deg)';
|
||||
const timerMin=Math.ceil(s.timer/60);
|
||||
|
||||
// flying coins
|
||||
const flyingCoinEls=s.flyingCoins.map(f=>React.createElement('div',{key:f.key, style:{position:'fixed',left:f.x+'px',top:f.y+'px',fontSize:'40px',zIndex:60,['--dx']:f.dx+'px',['--dy']:f.dy+'px',animation:'none',transform:'translate('+f.dx+'px,'+f.dy+'px) scale(.4)',transition:'transform .72s cubic-bezier(.4,0,.5,1), opacity .72s',opacity:0,willChange:'transform'}, ref:(el)=>{ if(el){ el.style.transform='translate(0,0) scale(1)'; el.style.opacity='1'; requestAnimationFrame(()=>requestAnimationFrame(()=>{ el.style.transform='translate('+f.dx+'px,'+f.dy+'px) scale(.4)'; el.style.opacity='0'; })); } }}, '🪙'));
|
||||
const confettiEls=s.confetti.map(c=>React.createElement('div',{key:c.key, style:{position:'absolute',top:'-5vh',left:c.left+'%',width:c.size+'px',height:(c.size*0.6)+'px',background:c.color,borderRadius:'2px',animation:'confFall '+c.dur+'s linear '+c.delay+'s infinite'}}));
|
||||
|
||||
const dockBtn='all:unset;cursor:pointer;padding:7px 13px;border-radius:11px;background:#fff;color:#5A6B82;font-weight:800;font-size:13px;box-shadow:0 2px 6px rgba(40,60,100,.06);';
|
||||
const segBtn=(on)=>'all:unset;cursor:pointer;padding:7px 13px;border-radius:11px;background:'+(on?'#2A3142':'#fff')+';color:'+(on?'#fff':'#5A6B82')+';font-weight:800;font-size:13px;box-shadow:0 2px 6px rgba(40,60,100,.06);';
|
||||
|
||||
return {
|
||||
L,
|
||||
isProfiles:s.screen==='profiles', isHome:s.screen==='home', isStore:s.screen==='store', isPin:s.screen==='pin', isParents:s.screen==='parents',
|
||||
showDock:s.screen!=='pin',
|
||||
profiles:this.profilesData.map(p=>({...p, ageLabel:(s.ages[p.id]||7)+' '+L.anios,
|
||||
cardStyle:'background:#fff;border-radius:30px;padding:26px 30px;box-shadow:0 12px 30px rgba(40,60,100,.08);display:flex;flex-direction:column;align-items:center;animation:slideUp .5s ease both;',
|
||||
ringStyle:'width:140px;height:140px;border-radius:50%;background:'+p.color+'26;display:flex;align-items:center;justify-content:center;animation:floatY 3.5s ease-in-out infinite;',
|
||||
select:()=>this.selectProfile(p), ageUp:()=>this.changeAge(p.id,1), ageDown:()=>this.changeAge(p.id,-1) })),
|
||||
profile, coins:s.coins,
|
||||
walletStyle:'display:flex;align-items:center;gap:8px;background:#FFF6E0;color:#C7912B;border-radius:20px;padding:10px 20px;'+(s.walletPulse!==undefined?'':'')+(true?'animation:'+(s._wp?'':'')+'walletBump .5s ease;':''),
|
||||
setWallet:(el)=>{this.walletEl=el;},
|
||||
dayName: lang==='ca'?'DIMARTS':'MARTES', dayDate: lang==='ca'?'17 de juny':'17 de junio',
|
||||
timerRing, timerMin,
|
||||
globalDone:gDone, globalTotal:gTotal, globalPct:(gTotal?Math.round(gDone/gTotal*100):0)+'%',
|
||||
coleItems, tardeItems, coleDone, coleTotal:coleItems.length, tardeDone, tardeTotal:tardeItems.length,
|
||||
showEmpty:s.emptyDemo, showA:!s.emptyDemo && s.variant==='A', showB:!s.emptyDemo && s.variant==='B',
|
||||
hasEvents:!s.emptyDemo, events,
|
||||
focus:{...ft, label:lbl(ft), icon:ft.icon, color:ft.color, tint:tint(ft.color), heroTile,
|
||||
blockIcon:isCole?'🎒':'🌙', blockLabel:isCole?L.cole:L.tarde, doneLabel:fdone?'✓ '+L.listo.toUpperCase():L.hecho,
|
||||
speak:()=>this.speak(lbl(ft))},
|
||||
bDone:()=>this.bDone(), bPrev:()=>this.bNav(-1), bNext:()=>this.bNav(1),
|
||||
bPrevStyle:navBtn(fi>0), bNextStyle:navBtn(fi<all.length-1), bDoneStyle,
|
||||
dots, remaining, nextLabel: nextT?lbl(nextT):'🎉',
|
||||
rewards, toast:s.toast||{show:false,msg:''},
|
||||
pinDots, pinDotsStyle:'display:flex;gap:16px;'+(s.pinInput.length===0&&false?'animation:shake .4s;':''), keys,
|
||||
tabs, tabHorario:s.parentsTab==='horario', tabMateriales:s.parentsTab==='materiales', tabEventos:s.parentsTab==='eventos', tabRutinas:s.parentsTab==='rutinas', tabJuego:s.parentsTab==='juego',
|
||||
schedule, activities, pEvents, weekChips, routineList, steppers, toggles,
|
||||
celebrate:s.celebrate, confettiEls, flyingCoinEls, coinsPerDay:s.coinsPerDay, dismissCeleb:()=>this.dismissCeleb(),
|
||||
goHome:()=>this.setState({screen:'home'}), goStore:()=>this.setState({screen:'store'}), goProfiles:()=>this.setState({screen:'profiles'}), goPin:()=>this.setState({screen:'pin',pinInput:''}),
|
||||
setVarA:()=>this.setState({variant:'A'}), setVarB:()=>this.setState({variant:'B'}),
|
||||
varAStyle:segBtn(s.variant==='A'), varBStyle:segBtn(s.variant==='B'),
|
||||
toggleEmpty:()=>this.setState(p=>({emptyDemo:!p.emptyDemo})), emptyLabel:s.emptyDemo?('📋 '+L.diaNormal):('🏖️ '+L.diaVacio),
|
||||
reset:()=>this.reset(), dockBtn,
|
||||
toggleLang:()=>this.setState(p=>({lang:p.lang==='es'?'ca':'es'})), langBtn:dockBtn, langLabel:s.lang==='es'?'🌐 CA':'🌐 ES',
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1513
artifacts/App de rutinas visuales para TDAH/support.js
Normal file
1513
artifacts/App de rutinas visuales para TDAH/support.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user