// app-lesson.jsx — Lesson player, course detail, and CivifyApp shell // Depends on: app-data.js, app-icons.jsx, app-screens.jsx, logo.jsx (function () { const { useState, useRef, useEffect, useCallback } = React; const D = window.CIVIFY_APP; const Icon = window.Icon; // ── FILL (drag-to-place) exercise ─────────────────────────── function FillExercise({ step, slots, setSlots, locked }) { const blankRefs = useRef([]); const [drag, setDrag] = useState(null); // {chip, x, y, w, h, label} const [over, setOver] = useState(-1); const usedChips = slots.filter((s) => s !== null).map((s) => s.chip); const placeInSlot = (chipIdx, slotIdx) => { setSlots((prev) => { const next = prev.map((s) => (s && s.chip === chipIdx ? null : s)); // remove chip elsewhere next[slotIdx] = { chip: chipIdx, label: step.chips[chipIdx] }; return next; }); }; const clickChip = (chipIdx) => { if (locked) return; setSlots((prev) => { if (prev.some((s) => s && s.chip === chipIdx)) return prev; // already placed const empty = prev.findIndex((s) => s === null); if (empty < 0) return prev; const next = prev.slice(); next[empty] = { chip: chipIdx, label: step.chips[chipIdx] }; return next; }); }; const clearSlot = (slotIdx) => { if (locked) return; setSlots((prev) => prev.map((s, i) => (i === slotIdx ? null : s))); }; // pointer drag const startDrag = (e, chipIdx) => { if (locked || usedChips.includes(chipIdx)) return; const r = e.currentTarget.getBoundingClientRect(); e.currentTarget.setPointerCapture?.(e.pointerId); setDrag({ chip: chipIdx, x: e.clientX, y: e.clientY, w: r.width, h: r.height, label: step.chips[chipIdx] }); }; useEffect(() => { if (!drag) return; const move = (e) => { setDrag((d) => (d ? { ...d, x: e.clientX, y: e.clientY } : d)); let hit = -1; blankRefs.current.forEach((el, i) => { if (!el) return; const r = el.getBoundingClientRect(); if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) hit = i; }); setOver(hit); }; const up = (e) => { let hit = -1; blankRefs.current.forEach((el, i) => { if (!el) return; const r = el.getBoundingClientRect(); if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) hit = i; }); if (hit >= 0) placeInSlot(drag.chip, hit); setDrag(null); setOver(-1); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); return () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); }; }, [drag]); let bi = -1; return (
{step.sentence.map((part, i) => { if (part === '{0}' || part === '{1}') { bi += 1; const idx = bi; const s = slots[idx]; const correct = locked && s && s.label === step.blanks[idx]; return ( (blankRefs.current[idx] = el)} className={'cf-blank' + (s ? ' filled' : '') + (over === idx ? ' over' : '')} style={locked && s ? { borderColor: correct ? '#2FA36B' : '#E25555', background: correct ? '#EAF7F0' : '#FCEBEB', color: correct ? '#2FA36B' : '#E25555' } : null} onClick={() => s && clearSlot(idx)} > {s ? s.label : ''} ); } return {part}; })}
{step.chips.map((w, i) => (
startDrag(e, i)} onClick={() => clickChip(i)} > {w}
))}
{drag && (
{drag.label}
)}
); } // ── LESSON PLAYER ─────────────────────────────────────────── function Lesson({ onClose, onDone }) { const L = D.lesson; const [idx, setIdx] = useState(0); const [checked, setChecked] = useState(false); const [choice, setChoice] = useState(null); const [tf, setTf] = useState(null); const [slots, setSlots] = useState([null, null]); const [done, setDone] = useState(false); const step = L.steps[idx]; const progress = ((idx + (checked ? 1 : 0)) / L.steps.length) * 100; const reset = () => { setChecked(false); setChoice(null); setTf(null); setSlots([null, null]); }; const isCorrect = () => { if (step.type === 'choice') return step.options[choice]?.correct; if (step.type === 'truefalse') return tf === step.answer; if (step.type === 'fill') return slots.every((s, i) => s && s.label === step.blanks[i]); return true; }; const canCheck = () => { if (step.type === 'choice') return choice !== null; if (step.type === 'truefalse') return tf !== null; if (step.type === 'fill') return slots.every((s) => s !== null); return true; }; const advance = () => { if (idx + 1 >= L.steps.length) { setDone(true); return; } setIdx(idx + 1); reset(); }; const onPrimary = () => { if (step.type === 'intro') { advance(); return; } if (!checked) { setChecked(true); return; } advance(); }; if (done) { return (

Lektion klar!

Snyggt jobbat — du behärskar EU:s institutioner.

+{L.reward}Poäng
{D.user.streak + 1}Streak
); } const correct = checked && isCorrect(); const wrong = checked && !isCorrect(); return (
{step.type === 'intro' && ( <>
Introduktion
{step.title}

{step.body}

)} {step.type === 'choice' && ( <>
Flerval
{step.prompt}
{step.options.map((o, i) => { let cls = 'cf-opt'; if (!checked && choice === i) cls += ' sel'; if (checked && o.correct) cls += ' right'; if (checked && choice === i && !o.correct) cls += ' wrong'; return ( ); })} )} {step.type === 'fill' && ( <>
Dra & släpp
{step.prompt}
)} {step.type === 'truefalse' && ( <>
Sant eller falskt
{step.prompt}
{[true, false].map((v) => { let cls = ''; if (!checked && tf === v) cls = 'sel'; if (checked && step.answer === v) cls = 'right'; if (checked && tf === v && step.answer !== v) cls = 'wrong'; return ; })}
)}
{checked && (
{correct ? 'Rätt!' : 'Inte riktigt'} {correct ? step.explain : (step.explainWrong || step.explain)}
)}
); } // ── COURSE DETAIL ─────────────────────────────────────────── function CourseDetail({ course, onBack, onStartLesson }) { return (

{course.title}

{course.subtitle} · {course.lessonsDone}/{course.lessonsTotal} klara

{course.lessons.map((l, i) => { const playable = course.id === 'eu' && l.state === 'current'; return (
{l.state === 'done' ? : l.state === 'locked' ? : i + 1}
{l.t}
{l.state === 'done' ? 'Klar' : l.state === 'current' ? 'Pågår' : 'Låst'}
{playable && }
); })}
); } function shade(hex) { const n = parseInt(hex.slice(1), 16); let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; r = Math.round(r * 0.78); g = Math.round(g * 0.74); b = Math.round(b * 0.7); return `rgb(${r},${g},${b})`; } // ── APP SHELL ─────────────────────────────────────────────── function CivifyApp() { const [tab, setTab] = useState('learn'); const [course, setCourse] = useState(null); const [lesson, setLesson] = useState(false); const Home = window.CivifyHome, Explore = window.CivifyExplore, Profile = window.CivifyProfile; const openCourse = (c) => setCourse(c); const startLesson = () => setLesson(true); return (
{tab === 'learn' && } {tab === 'explore' && } {tab === 'profile' && } {course && !lesson && ( setCourse(null)} onStartLesson={startLesson} /> )} {lesson && ( setLesson(false)} onDone={() => { setLesson(false); setCourse(null); setTab('profile'); }} /> )} {!lesson && (
{[['learn', 'home', 'Lär dig'], ['explore', 'compass', 'Utforska'], ['profile', 'person', 'Profil']].map(([k, ic, lbl]) => ( ))}
)}
); } window.CivifyApp = CivifyApp; })();