// 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;
})();