lingo

<!--
  Mini “Duolingo” para Blogger (ES -> IS)
  Pega TODO este bloque en una entrada/página de Blogger en modo HTML.
  No requiere librerías externas. Guarda el progreso en localStorage.
--><div id="is-edu-app" class="is-app">
  <header class="is-header" role="banner" aria-label="Cabecera de la app">
    <div class="brand">
      <span class="logo" aria-hidden="true">🇮🇸</span>
      <h1>Aprende Islandés</h1>
    </div>
    <nav class="stats" aria-label="Estadísticas del usuario">
      <div class="stat" title="Puntos de experiencia">
        <span class="icon" aria-hidden="true">⭐</span>
        <span id="xp">0</span> XP
      </div>
      <div class="stat" title="Racha diaria">
        <span class="icon" aria-hidden="true">🔥</span>
        <span id="streak">0</span> días
      </div>
      <div class="stat" title="Vidas">
        <span class="icon" aria-hidden="true">💚</span>
        <span id="lives">5</span>
      </div>
    </nav>
  </header> <section class="tabs" role="tablist" aria-label="Secciones de aprendizaje">
    <button class="tab active" data-tab="home" role="tab" aria-selected="true">Inicio</button>
    <button class="tab" data-tab="lessons" role="tab" aria-selected="false">Lecciones</button>
    <button class="tab" data-tab="vocab" role="tab" aria-selected="false">Vocabulario</button>
    <button class="tab" data-tab="practice" role="tab" aria-selected="false">Práctica</button>
    <button class="tab" data-tab="achievements" role="tab" aria-selected="false">Logros</button>
    <button class="tab" data-tab="settings" role="tab" aria-selected="false">Ajustes</button>
  </section> <main class="views">
    <!-- INICIO -->
    <section id="view-home" class="view active" aria-labelledby="Inicio">
      <div class="hero">
        <h2>Velkomin! 👋</h2>
        <p>Curso gamificado para hispanohablantes. Lecciones cortas, audio nativo (TTS), racha diaria y XP.</p>
        <button id="btn-start" class="btn primary">Empezar la primera lección</button>
      </div>
      <div class="grid two">
        <article class="card">
          <h3>Objetivo diario</h3>
          <p>Completa al menos <strong>1 lección</strong> o gana <strong>30 XP</strong> para mantener la racha.</p>
          <div class="progress">
            <div id="daily-progress" class="bar"></div>
          </div>
        </article>
        <article class="card">
          <h3>Consejo rápido</h3>
          <p>Haz clic en 🔊 para escuchar el islandés. Repite en voz alta para mejorar tu pronunciación.</p>
        </article>
      </div>
    </section><!-- LECCIONES -->
<section id="view-lessons" class="view" aria-labelledby="Lecciones">
  <h2>Ruta de aprendizaje (A1)</h2>
  <div id="lessons-list" class="lesson-list"></div>
</section>

<!-- VOCABULARIO -->
<section id="view-vocab" class="view" aria-labelledby="Vocabulario">
  <h2>Vocabulario</h2>
  <p>Palabras y frases de las lecciones completadas. Pulsa 🔊 para escuchar.</p>
  <div id="vocab-table" class="vocab"></div>
</section>

<!-- PRÁCTICA -->
<section id="view-practice" class="view" aria-labelledby="Práctica">
  <h2>Práctica rápida</h2>
  <p>Repaso de 5 preguntas aleatorias según lo aprendido.</p>
  <div id="practice-area"></div>
  <button id="btn-practice" class="btn primary">Iniciar práctica</button>
</section>

<!-- LOGROS -->
<section id="view-achievements" class="view" aria-labelledby="Logros">
  <h2>Logros</h2>
  <ul id="achievements-list" class="achievements"></ul>
</section>

<!-- AJUSTES -->
<section id="view-settings" class="view" aria-labelledby="Ajustes">
  <h2>Ajustes</h2>
  <label class="row">
    <span>Audio TTS islandés (si está disponible):</span>
    <button id="btn-tts-test" class="btn">Probar 🔊</button>
  </label>
  <label class="row">
    <span>Reiniciar progreso (local):</span>
    <button id="btn-reset" class="btn danger">Borrar todo</button>
  </label>
</section>

  </main> <!-- Modal de Lección --> <dialog id="lesson-modal" class="modal" aria-label="Lección">
    <form method="dialog" class="modal-box">
      <header class="modal-header">
        <h3 id="lesson-title">Lección</h3>
        <button class="btn ghost" value="cancel" aria-label="Cerrar">✖</button>
      </header>
      <div id="lesson-content" class="modal-content"></div>
      <footer class="modal-actions">
        <button id="lesson-prev" class="btn">⟨ Anterior</button>
        <button id="lesson-next" class="btn primary">Siguiente ⟩</button>
      </footer>
    </form>
  </dialog> <!-- Toast --> <div id="toast" class="toast" role="status" aria-live="polite"></div>
</div><style>
  :root{
    --bg:#0b1220; /* fondo oscuro elegante */
    --panel:#111a2e;
    --card:#162342;
    --accent:#6ee7ff;
    --accent-2:#7c4dff;
    --ok:#22c55e;
    --warn:#f59e0b;
    --err:#ef4444;
    --text:#e6f0ff;
    --muted:#a0b3d1;
    --shadow:0 12px 28px rgba(0,0,0,.35);
    --radius:18px;
  }
  .is-app{font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; color:var(--text); background:linear-gradient(160deg,#0b1220 0%,#0d1630 50%,#0a1126 100%); padding:16px; border-radius:var(--radius); box-shadow:var(--shadow)}
  .is-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 8px 16px 8px}
  .brand{display:flex;align-items:center;gap:10px}
  .brand .logo{font-size:28px}
  .brand h1{margin:0;font-size:20px;letter-spacing:.3px}
  .stats{display:flex;gap:10px}
  .stat{background:var(--card);padding:8px 12px;border-radius:999px;display:flex;align-items:center;gap:6px;color:var(--muted);border:1px solid rgba(255,255,255,.06)}
  .stat .icon{filter:drop-shadow(0 1px 0 rgba(0,0,0,.6))}

  .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:8px;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06)}
  .tab{background:transparent;color:var(--muted);border:1px solid rgba(255,255,255,.08);padding:8px 12px;border-radius:999px;cursor:pointer}
  .tab.active{background:linear-gradient(135deg, var(--accent), var(--accent-2));color:#0b1220;border:none;font-weight:600}

  .views{padding:12px}
  .view{display:none}
  .view.active{display:block}
  .hero{background:var(--panel);border:1px solid rgba(255,255,255,.06);padding:16px;border-radius:var(--radius)}
  .grid.two{display:grid;grid-template-columns:1fr;gap:12px;margin-top:12px}
  @media(min-width:700px){.grid.two{grid-template-columns:1fr 1fr}}
  .card{background:var(--card);border:1px solid rgba(255,255,255,.08);padding:16px;border-radius:var(--radius)}
  .progress{height:10px;background:#0b1330;border-radius:999px;overflow:hidden;border:1px solid rgba(255,255,255,.08)}
  .progress .bar{height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width .5s ease}

  .lesson-list{display:grid;grid-template-columns:1fr;gap:12px}
  @media(min-width:900px){.lesson-list{grid-template-columns:repeat(2,1fr)}}
  .lesson{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--panel);border:1px solid rgba(255,255,255,.06);padding:14px;border-radius:var(--radius)}
  .lesson .meta{display:flex;align-items:center;gap:10px}
  .lesson .badge{font-size:20px}
  .lesson .title{font-weight:600}
  .lesson .desc{color:var(--muted);font-size:14px}
  .lesson .actions .btn{margin-left:8px}

  .vocab table{width:100%;border-collapse:separate;border-spacing:0;border:1px solid rgba(255,255,255,.06);border-radius:12px;overflow:hidden}
  .vocab th,.vocab td{padding:10px;border-bottom:1px solid rgba(255,255,255,.06)}
  .vocab th{background:var(--panel);text-align:left;color:var(--muted)}
  .vocab tr:hover td{background:rgba(255,255,255,.03)}

  .btn{background:#1b2b52;color:var(--text);border:1px solid rgba(255,255,255,.12);padding:10px 14px;border-radius:12px;cursor:pointer;transition:transform .05s ease, filter .2s}
  .btn:active{transform:translateY(1px)}
  .btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#0b1220;border:none;font-weight:700}
  .btn.ghost{background:transparent;border:none;color:var(--muted)}
  .btn.danger{background:#3a1531;border:1px solid #7f1d1d}

  .modal{border:none;border-radius:16px;padding:0;background:transparent}
  .modal::backdrop{background:rgba(5,10,30,.6)}
  .modal-box{background:var(--panel);border:1px solid rgba(255,255,255,.08);border-radius:16px;max-width:820px;width:92vw;box-shadow:var(--shadow)}
  .modal-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08)}
  .modal-content{padding:16px}
  .modal-actions{display:flex;justify-content:space-between;padding:12px 14px;border-top:1px solid rgba(255,255,255,.08)}

  .q-card{background:var(--card);border:1px solid rgba(255,255,255,.08);padding:14px;border-radius:14px;margin-bottom:10px}
  .choices{display:grid;grid-template-columns:1fr;gap:8px;margin-top:8px}
  @media(min-width:600px){.choices{grid-template-columns:1fr 1fr}}
  .choice{padding:10px;border:1px solid rgba(255,255,255,.12);border-radius:10px;background:#0f1a34;cursor:pointer}
  .choice.correct{border-color:#14532d;background:#0d2518}
  .choice.wrong{border-color:#7f1d1d;background:#2a0f13}

  .toast{position:fixed;inset:auto 16px 16px auto;background:#0f1a34;border:1px solid rgba(255,255,255,.12);padding:10px 12px;border-radius:12px;opacity:0;transform:translateY(8px);transition:all .25s ease;pointer-events:none}
  .toast.show{opacity:1;transform:translateY(0)}

  .row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 0}
</style><script>
(function(){
  const APP_KEY = 'islandic_es_app_v1';
  const state = loadState() || {
    xp: 0,
    lives: 5,
    streak: 0,
    lastVisit: null,
    unlockedLessons: { 'A1-1': true },
    completedLessons: {},
    vocabKnown: {}, // { token: {es, is, ipa?, seen: n, correct: n} }
    settings: { tts: true }
  };

  const $$ = (sel, root=document) => root.querySelector(sel);
  const $$$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));

  // --- CONTENIDO INICIAL (puedes editar/añadir) ---
  const COURSE = [
    {
      id: 'A1-1', level: 'A1', icon: '👋', title: 'Saludos',
      desc: 'Presentaciones básicas y cortesía.',
      items: [
        {es:'Hola', is:'Halló', type:'pair'},
        {es:'Adiós', is:'Bless', type:'pair'},
        {es:'Por favor', is:'Vinsamlegast', type:'pair'},
        {es:'Gracias', is:'Takk', type:'pair'},
        {es:'Sí', is:'Já', type:'pair'},
        {es:'No', is:'Nei', type:'pair'},
      ],
      quiz: [
        {kind:'mc', q:'¿Cómo se dice «Gracias» en islandés?', answers:['Takk','Halló','Bless','Vinsamlegast'], ok:0, speak:'Takk'},
        {kind:'pair', q:'Empareja saludo con su traducción.', pairs:[['Hola','Halló'],['Adiós','Bless']]},
        {kind:'type', q:'Escribe en islandés: «Por favor»', ok:'Vinsamlegast'},
      ]
    },
    {
      id: 'A1-2', level: 'A1', icon: '🔢', title: 'Números 1–10',
      desc: 'Contar del 1 al 10.',
      req: ['A1-1'],
      items: [
        {es:'uno', is:'einn'}, {es:'dos', is:'tveir'}, {es:'tres', is:'þrír'},
        {es:'cuatro', is:'fjórir'}, {es:'cinco', is:'fimm'}, {es:'seis', is:'sex'},
        {es:'siete', is:'sjö'}, {es:'ocho', is:'átta'}, {es:'nueve', is:'níu'}, {es:'diez', is:'tíu'},
      ],
      quiz: [
        {kind:'mc', q:'¿Cuál es «cuatro» en islandés?', answers:['fimm','fjórir','tíu','níu'], ok:1, speak:'fjórir'},
        {kind:'select', q:'Selecciona la traducción de «tres».', options:['tveir','þrír','sjö','sex'], ok:1},
        {kind:'type', q:'Escribe en islandés: «ocho»', ok:'átta'},
      ]
    },
    {
      id: 'A1-3', level: 'A1', icon: '🎨', title: 'Colores',
      desc: 'Colores comunes.', req:['A1-1'],
      items: [
        {es:'rojo', is:'rauður'}, {es:'azul', is:'blár'}, {es:'verde', is:'grænn'},
        {es:'amarillo', is:'gulur'}, {es:'negro', is:'svartur'}, {es:'blanco', is:'hvítur'},
      ],
      quiz: [
        {kind:'mc', q:'¿Cómo se dice «azul»?', answers:['blár','grænn','gulur','svartur'], ok:0, speak:'blár'},
        {kind:'type', q:'Escribe en islandés: «rojo»', ok:'rauður'},
        {kind:'mc', q:'¿Cuál es «negro»?', answers:['hvítur','svartur','gulur','grænn'], ok:1},
      ]
    }
  ];

  // --- INIT ---
  renderStats();
  updateStreak();
  updateDailyProgress();
  renderLessons();
  renderVocab();
  renderAchievements();
  wireTabs();
  $$('#btn-start').addEventListener('click', ()=> openLesson('A1-1'));
  $$('#btn-practice').addEventListener('click', startPractice);
  $$('#btn-reset').addEventListener('click', resetAll);
  $$('#btn-tts-test').addEventListener('click', ()=> speak('Halló! Þetta er próf.', 'is-IS'));

  // --- CORE FUNCTIONS ---
  function renderStats(){
    $$('#xp').textContent = state.xp|0;
    $$('#streak').textContent = state.streak|0;
    $$('#lives').textContent = state.lives|0;
    saveState();
  }

  function updateStreak(){
    const today = new Date();
    const todayKey = today.toDateString();
    if(!state.lastVisit){ state.lastVisit = todayKey; state.streak = 1; }
    else if(state.lastVisit !== todayKey){
      const prev = new Date(state.lastVisit);
      const diff = (today - prev)/(1000*60*60*24);
      state.streak = (diff <= 2) ? (state.streak+1) : 1; // tolerancia de 1 día
      state.lastVisit = todayKey;
    }
    renderStats();
  }

  function updateDailyProgress(){
    const target = 30; // XP objetivo diario
    const pct = Math.min(100, (state.xp % target) / target * 100);
    $$('#daily-progress').style.width = pct + '%';
  }

  function renderLessons(){
    const list = $$('#lessons-list');
    list.innerHTML = '';
    COURSE.forEach(lesson => {
      const locked = lesson.req && !lesson.req.every(id => state.completedLessons[id]);
      const done = !!state.completedLessons[lesson.id];
      const el = document.createElement('div');
      el.className = 'lesson';
      el.innerHTML = `
        <div class="meta">
          <span class="badge">${lesson.icon}</span>
          <div>
            <div class="title">${lesson.title} <small style="color:var(--muted)">(${lesson.level})</small></div>
            <div class="desc">${lesson.desc}</div>
          </div>
        </div>
        <div class="actions">
          ${locked ? '<span title="Completa requisitos">🔒</span>' : ''}
          ${done ? '<span title="Completado">✅</span>' : ''}
          <button class="btn" ${locked? 'disabled':''} data-open="${lesson.id}">Abrir</button>
        </div>`;
      list.appendChild(el);
    });
    $$$('[data-open]').forEach(b => b.addEventListener('click', e => openLesson(e.currentTarget.getAttribute('data-open'))));
  }

  function openLesson(id){
    const lesson = COURSE.find(l => l.id === id);
    if(!lesson) return;
    const modal = $$('#lesson-modal');
    $$('#lesson-title').textContent = `${lesson.icon} ${lesson.title}`;
    const content = $$('#lesson-content');

    // Construir contenido (teoría + quiz)
    const theory = document.createElement('div');
    theory.innerHTML = `<h4>Vocabulario</h4>`;
    lesson.items.forEach(item => {
      const row = document.createElement('div');
      row.className = 'q-card';
      row.innerHTML = `
        <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
          <div><strong>${item.is}</strong> — <span style="color:var(--muted)">${item.es}</span></div>
          <button class="btn" aria-label="Escuchar ${item.is}">🔊</button>
        </div>`;
      row.querySelector('button').addEventListener('click', ()=> speak(item.is, 'is-IS'));
      theory.appendChild(row);

      // registrar vocab
      const key = tokenKey(item);
      if(!state.vocabKnown[key]) state.vocabKnown[key] = {...item, seen:0, correct:0};
    });

    const quiz = document.createElement('div');
    quiz.innerHTML = `<h4>Quiz</h4>`;
    let qIndex = 0;

    function renderQuestion(){
      quiz.querySelectorAll('.q-card').forEach(n => n.remove());
      const q = lesson.quiz[qIndex];
      if(!q){
        // lección terminada
        state.completedLessons[lesson.id] = true;
        addXP(30);
        showToast('Lección completada: +30 XP 🎉');
        saveState();
        renderLessons();
        renderVocab();
        renderAchievements();
        modal.close();
        return;
      }
      const card = document.createElement('div');
      card.className = 'q-card';
      card.innerHTML = `<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
          <div>${q.q}</div>
          ${q.speak ? '<button class="btn" aria-label="Escuchar">🔊</button>' : ''}
        </div>`;
      if(q.speak){ card.querySelector('button').addEventListener('click', ()=> speak(q.speak,'is-IS')); }

      if(q.kind === 'mc' || q.kind === 'select'){
        const box = document.createElement('div');
        box.className = 'choices';
        q.answers = q.answers || q.options;
        q.answers.forEach((ans, i)=>{
          const b = document.createElement('button');
          b.type = 'button';
          b.className = 'choice';
          b.textContent = ans;
          b.addEventListener('click', ()=> {
            const correct = i === q.ok;
            b.classList.add(correct? 'correct':'wrong');
            if(correct){
              addXP(10); markVocab(ans); showToast('+10 XP ✅'); next();
            } else { loseLife(); showToast('Fallaste ❌'); }
          });
          box.appendChild(b);
        });
        card.appendChild(box);
      }
      else if(q.kind === 'type'){
        const input = document.createElement('input');
        input.className = 'choice';
        input.placeholder = 'Escribe aquí…';
        input.setAttribute('inputmode','latin');
        const okBtn = document.createElement('button');
        okBtn.className = 'btn primary';
        okBtn.textContent = 'Comprobar';
        okBtn.addEventListener('click', ()=>{
          const val = (input.value || '').trim();
          const correct = normalize(val) === normalize(q.ok);
          input.classList.add(correct? 'correct' : 'wrong');
          if(correct){ addXP(10); markVocab(q.ok); showToast('+10 XP ✅'); next(); }
          else { loseLife(); showToast(`Respuesta: ${q.ok}`); }
        });
        card.appendChild(input); card.appendChild(document.createElement('br')); card.appendChild(okBtn);
      }
      else if(q.kind === 'pair'){
        // emparejar simple por clics
        const left = q.pairs.map(p=>p[0]);
        const right = shuffle(q.pairs.map(p=>p[1]));
        const box = document.createElement('div');
        box.className = 'choices';
        const selected = {l:null, r:null};
        const chosen = new Set();
        left.forEach(l => {
          const b = document.createElement('button'); b.type='button'; b.className='choice'; b.textContent=l; b.dataset.side='l';
          b.addEventListener('click', ()=> pick(b)); box.appendChild(b);
        });
        right.forEach(r => {
          const b = document.createElement('button'); b.type='button'; b.className='choice'; b.textContent=r; b.dataset.side='r';
          b.addEventListener('click', ()=> pick(b)); box.appendChild(b);
        });
        function pick(btn){
          const side = btn.dataset.side;
          if(side==='l') selected.l = btn; else selected.r = btn;
          btn.classList.add('selected');
          if(selected.l && selected.r){
            const l = selected.l.textContent; const r = selected.r.textContent;
            const ok = q.pairs.some(p=>p[0]===l && p[1]===r);
            if(ok){
              selected.l.classList.add('correct'); selected.r.classList.add('correct');
              chosen.add(l);
              addXP(5); markVocab(r); showToast('+5 XP ✅');
              if(chosen.size === left.length){ next(); }
            } else {
              selected.l.classList.add('wrong'); selected.r.classList.add('wrong');
              loseLife(); showToast('No coincide ❌');
            }
            selected.l = selected.r = null;
          }
  

Entradas populares de este blog

CALCULADORA C1 Y C2 (TCAE, TF, Aux Advo/a, Advo/a, TER, TEL, TEAP,TEDS, Celador/a-Conductor/a, TEMEII, Cocinero/a, Telefonista)

CALCULADORA Grupo E: Celador/a, Pinche, Limpiador/a, Peón, P. Lavanderia Planchado, etc.

CALCULADORA GRUPOS A1 Y A2: FEA, Medicina, Cuerpo A4, T Social, TFA, TMGFA, Enfermería, Fisioterapia, etc.