Initial MatheLeicht Astro app
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import type { Exercise } from '../data/exercises';
|
||||
|
||||
const { exercise } = Astro.props as { exercise: Exercise };
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<div class={`card-tag ${exercise.accent}`}>{exercise.category}</div>
|
||||
<h3>{exercise.title}</h3>
|
||||
<p>{exercise.short}</p>
|
||||
<a class="exercise-link" href={`/aufgaben/${exercise.slug}`}>Aufgabe öffnen →</a>
|
||||
</article>
|
||||
@@ -0,0 +1,400 @@
|
||||
---
|
||||
import type { Exercise } from '../data/exercises';
|
||||
|
||||
const { exercise } = Astro.props as { exercise: Exercise };
|
||||
const config = JSON.stringify(exercise);
|
||||
---
|
||||
|
||||
<div class="exercise-layout" data-trainer-root data-config={config}>
|
||||
<aside class="panel">
|
||||
<div class="control-label">Bereich</div>
|
||||
<p style="margin-top:0; margin-bottom:1rem;">{exercise.categoryIntro}</p>
|
||||
|
||||
<div class="control-label">Schwierigkeit</div>
|
||||
<div class="button-list" data-levels>
|
||||
{exercise.levels.map((level, index) => (
|
||||
<button class={`option-button ${index === 0 ? 'active' : ''}`} type="button" data-level={level.id}>
|
||||
{level.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="control-label">Modus</div>
|
||||
<div class="button-list" data-modes>
|
||||
<button class="option-button active" type="button" data-mode="mc">Multiple Choice</button>
|
||||
<button class="option-button" type="button" data-mode="input">Freie Eingabe</button>
|
||||
</div>
|
||||
|
||||
<div class="control-label">Fortschritt</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-box"><span class="stat-number" data-stat-correct>0</span><span>Richtig</span></div>
|
||||
<div class="stat-box"><span class="stat-number" data-stat-wrong>0</span><span>Falsch</span></div>
|
||||
<div class="stat-box"><span class="stat-number" data-stat-streak>0</span><span>Serie</span></div>
|
||||
<div class="stat-box"><span class="stat-number" data-stat-points>0</span><span>Punkte</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<div class="progress-wrap"><div class="progress-fill" data-progress></div></div>
|
||||
|
||||
<div class="task-card" data-question-card>
|
||||
<div class="task-counter" data-counter>1 / 10</div>
|
||||
<div class="task-title" data-question>7 <span class="op">+</span> 5 = ?</div>
|
||||
<div class="task-hint" data-hint>Wähle die richtige Antwort</div>
|
||||
|
||||
<div class="answer-grid" data-mc-area></div>
|
||||
|
||||
<div class="input-wrap" data-input-area style="display:none;">
|
||||
<input class="answer-input" data-input type="number" inputmode="numeric" />
|
||||
<button class="submit-btn" data-submit type="button">Antwort prüfen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feedback-bar" data-feedback></div>
|
||||
<button class="next-btn" data-next type="button" style="display:none; margin-top:1rem;">Nächste Aufgabe</button>
|
||||
|
||||
<div class="task-card result-card" data-result>
|
||||
<div class="result-emoji" data-result-emoji>🎉</div>
|
||||
<h2 data-result-title>Gut gemacht!</h2>
|
||||
<p data-result-text>Du hast 0 von 10 Aufgaben richtig gelöst.</p>
|
||||
<div class="button-row" style="justify-content:center;">
|
||||
<button class="submit-btn" data-restart type="button">Nochmal üben</button>
|
||||
<a class="button-secondary" href="/">Zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<canvas class="confetti" data-confetti></canvas>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const trainerRoots = document.querySelectorAll('[data-trainer-root]');
|
||||
trainerRoots.forEach((root) => {
|
||||
const exercise = JSON.parse(root.getAttribute('data-config') || '{}');
|
||||
const SESSION_LENGTH = 10;
|
||||
const SESSION_KEY = `matheleicht_${exercise.slug}_session`;
|
||||
|
||||
const levels = exercise.levels || [];
|
||||
let level = levels[0];
|
||||
let mode = 'mc';
|
||||
let correct = 0;
|
||||
let wrong = 0;
|
||||
let streak = 0;
|
||||
let points = 0;
|
||||
let questionNum = 0;
|
||||
let currentAnswer = 0;
|
||||
let answered = false;
|
||||
|
||||
const $ = (sel) => root.querySelector(sel) || document.querySelector(sel);
|
||||
const mcArea = $('[data-mc-area]');
|
||||
const inputArea = $('[data-input-area]');
|
||||
const input = $('[data-input]');
|
||||
const submit = $('[data-submit]');
|
||||
const feedback = $('[data-feedback]');
|
||||
const next = $('[data-next]');
|
||||
const progress = $('[data-progress]');
|
||||
const counter = $('[data-counter]');
|
||||
const question = $('[data-question]');
|
||||
const hint = $('[data-hint]');
|
||||
const result = $('[data-result]');
|
||||
const resultEmoji = $('[data-result-emoji]');
|
||||
const resultTitle = $('[data-result-title]');
|
||||
const resultText = $('[data-result-text]');
|
||||
const restart = $('[data-restart]');
|
||||
const confetti = document.querySelector('[data-confetti]');
|
||||
const ctx = confetti.getContext('2d');
|
||||
let particles = [];
|
||||
let animating = false;
|
||||
|
||||
function saveSession() {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify({
|
||||
level: level.id, mode, correct, wrong, streak, points, questionNum,
|
||||
}));
|
||||
}
|
||||
|
||||
function restoreSession() {
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem(SESSION_KEY) || '{}');
|
||||
if (data.questionNum >= SESSION_LENGTH) return;
|
||||
const foundLevel = levels.find((item) => item.id === data.level);
|
||||
if (foundLevel) level = foundLevel;
|
||||
mode = data.mode || mode;
|
||||
correct = data.correct || 0;
|
||||
wrong = data.wrong || 0;
|
||||
streak = data.streak || 0;
|
||||
points = data.points || 0;
|
||||
questionNum = data.questionNum || 0;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function shuffle(items) {
|
||||
const list = [...items];
|
||||
for (let i = list.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[list[i], list[j]] = [list[j], list[i]];
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function levelRange() {
|
||||
return [level.min, level.max];
|
||||
}
|
||||
|
||||
function generateQuestion() {
|
||||
const [min, max] = levelRange();
|
||||
switch (exercise.operation) {
|
||||
case 'addition': {
|
||||
const a = randomInt(min, max);
|
||||
const b = randomInt(min, max);
|
||||
return { label: `${a} <span class="op">+</span> ${b} = ?`, answer: a + b };
|
||||
}
|
||||
case 'subtraction': {
|
||||
const a = randomInt(min, max);
|
||||
const b = randomInt(min, a);
|
||||
return { label: `${a} <span class="op">−</span> ${b} = ?`, answer: a - b };
|
||||
}
|
||||
case 'multiplication': {
|
||||
const a = randomInt(min, max);
|
||||
const b = randomInt(min, max);
|
||||
return { label: `${a} <span class="op">×</span> ${b} = ?`, answer: a * b };
|
||||
}
|
||||
case 'division': {
|
||||
const b = randomInt(min, max);
|
||||
const answer = randomInt(min, max);
|
||||
const a = b * answer;
|
||||
return { label: `${a} <span class="op">÷</span> ${b} = ?`, answer };
|
||||
}
|
||||
case 'percentage': {
|
||||
const base = randomInt(min, max);
|
||||
const percents = [5, 10, 20, 25, 50];
|
||||
const percent = percents[randomInt(0, percents.length - 1)];
|
||||
return { label: `${percent}% von ${base} = ?`, answer: Math.round((base * percent) / 100) };
|
||||
}
|
||||
default:
|
||||
return { label: `1 <span class="op">+</span> 1 = ?`, answer: 2 };
|
||||
}
|
||||
}
|
||||
|
||||
function wrongAnswers(answer) {
|
||||
const set = new Set();
|
||||
while (set.size < 3) {
|
||||
const offset = randomInt(1, 10) * (Math.random() > 0.5 ? 1 : -1);
|
||||
const candidate = Math.max(0, answer + offset);
|
||||
if (candidate !== answer) set.add(candidate);
|
||||
}
|
||||
return [...set];
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
root.querySelector('[data-stat-correct]').textContent = correct;
|
||||
root.querySelector('[data-stat-wrong]').textContent = wrong;
|
||||
root.querySelector('[data-stat-streak]').textContent = streak;
|
||||
root.querySelector('[data-stat-points]').textContent = points;
|
||||
}
|
||||
|
||||
function renderQuestion() {
|
||||
if (questionNum >= SESSION_LENGTH) return showResult();
|
||||
answered = false;
|
||||
result.classList.remove('visible');
|
||||
root.querySelector('[data-question-card]').style.display = 'flex';
|
||||
feedback.textContent = '';
|
||||
feedback.className = 'feedback-bar';
|
||||
next.style.display = 'none';
|
||||
progress.style.width = `${(questionNum / SESSION_LENGTH) * 100}%`;
|
||||
counter.textContent = `${questionNum + 1} / ${SESSION_LENGTH}`;
|
||||
|
||||
const q = generateQuestion();
|
||||
currentAnswer = q.answer;
|
||||
question.innerHTML = q.label;
|
||||
|
||||
if (mode === 'mc') {
|
||||
hint.textContent = 'Wähle die richtige Antwort';
|
||||
mcArea.style.display = 'grid';
|
||||
inputArea.style.display = 'none';
|
||||
mcArea.innerHTML = '';
|
||||
const options = shuffle([currentAnswer, ...wrongAnswers(currentAnswer)]);
|
||||
options.forEach((value) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'answer-chip';
|
||||
btn.textContent = String(value);
|
||||
btn.dataset.value = String(value);
|
||||
btn.addEventListener('click', () => checkMC(btn));
|
||||
mcArea.appendChild(btn);
|
||||
});
|
||||
} else {
|
||||
hint.textContent = 'Gib deine Antwort ein';
|
||||
mcArea.style.display = 'none';
|
||||
inputArea.style.display = 'grid';
|
||||
input.value = '';
|
||||
input.disabled = false;
|
||||
submit.disabled = false;
|
||||
input.className = 'answer-input';
|
||||
setTimeout(() => input.focus(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAnswer(isCorrect) {
|
||||
if (answered) return;
|
||||
answered = true;
|
||||
questionNum += 1;
|
||||
if (isCorrect) {
|
||||
correct += 1;
|
||||
streak += 1;
|
||||
points += streak >= 3 ? 15 : 10;
|
||||
feedback.className = 'feedback-bar correct';
|
||||
feedback.textContent = streak >= 3 ? 'Richtig! Starker Lauf 🔥' : 'Richtig! Sehr gut gemacht.';
|
||||
if (streak >= 3) launchConfetti(30);
|
||||
} else {
|
||||
wrong += 1;
|
||||
streak = 0;
|
||||
feedback.className = 'feedback-bar wrong';
|
||||
feedback.innerHTML = `Nicht ganz. Die richtige Antwort war <strong>${currentAnswer}</strong>.`;
|
||||
}
|
||||
updateStats();
|
||||
saveSession();
|
||||
setTimeout(() => {
|
||||
if (questionNum >= SESSION_LENGTH) showResult();
|
||||
else next.style.display = 'inline-flex';
|
||||
}, 900);
|
||||
}
|
||||
|
||||
function checkMC(button) {
|
||||
if (answered) return;
|
||||
const buttons = root.querySelectorAll('.answer-chip');
|
||||
buttons.forEach((item) => {
|
||||
item.classList.add('disabled');
|
||||
});
|
||||
const value = Number(button.dataset.value);
|
||||
const ok = value === currentAnswer;
|
||||
button.classList.add(ok ? 'correct' : 'wrong');
|
||||
if (!ok) {
|
||||
buttons.forEach((item) => {
|
||||
if (Number(item.dataset.value) === currentAnswer) item.classList.add('reveal');
|
||||
});
|
||||
}
|
||||
handleAnswer(ok);
|
||||
}
|
||||
|
||||
function checkInput() {
|
||||
if (answered) return;
|
||||
const value = Number(input.value);
|
||||
if (Number.isNaN(value)) return input.focus();
|
||||
const ok = value === currentAnswer;
|
||||
input.className = `answer-input ${ok ? 'correct' : 'wrong'}`;
|
||||
input.disabled = true;
|
||||
submit.disabled = true;
|
||||
handleAnswer(ok);
|
||||
}
|
||||
|
||||
function showResult() {
|
||||
progress.style.width = '100%';
|
||||
root.querySelector('[data-question-card]').style.display = 'none';
|
||||
result.classList.add('visible');
|
||||
next.style.display = 'none';
|
||||
const percent = Math.round((correct / SESSION_LENGTH) * 100);
|
||||
resultEmoji.textContent = percent >= 90 ? '🏆' : percent >= 70 ? '🎉' : percent >= 50 ? '👍' : '💪';
|
||||
resultTitle.textContent = percent >= 90 ? 'Ausgezeichnet!' : percent >= 70 ? 'Super gemacht!' : percent >= 50 ? 'Gut gemacht!' : 'Weiter üben!';
|
||||
resultText.textContent = `Du hast ${correct} von ${SESSION_LENGTH} Aufgaben richtig gelöst und ${points} Punkte gesammelt.`;
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
if (percent >= 70) launchConfetti(120);
|
||||
}
|
||||
|
||||
function restartSession() {
|
||||
correct = 0;
|
||||
wrong = 0;
|
||||
streak = 0;
|
||||
points = 0;
|
||||
questionNum = 0;
|
||||
updateStats();
|
||||
renderQuestion();
|
||||
}
|
||||
|
||||
function setActiveButton(group, value, key) {
|
||||
group.querySelectorAll('button').forEach((button) => {
|
||||
button.classList.toggle('active', button.dataset[key] === value);
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelector('[data-levels]').addEventListener('click', (event) => {
|
||||
const button = event.target.closest('button[data-level]');
|
||||
if (!button) return;
|
||||
const found = levels.find((item) => item.id === button.dataset.level);
|
||||
if (!found) return;
|
||||
level = found;
|
||||
setActiveButton(root.querySelector('[data-levels]'), found.id, 'level');
|
||||
restartSession();
|
||||
});
|
||||
|
||||
root.querySelector('[data-modes]').addEventListener('click', (event) => {
|
||||
const button = event.target.closest('button[data-mode]');
|
||||
if (!button) return;
|
||||
mode = button.dataset.mode;
|
||||
setActiveButton(root.querySelector('[data-modes]'), mode, 'mode');
|
||||
renderQuestion();
|
||||
saveSession();
|
||||
});
|
||||
|
||||
submit.addEventListener('click', checkInput);
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') checkInput();
|
||||
});
|
||||
next.addEventListener('click', renderQuestion);
|
||||
restart.addEventListener('click', restartSession);
|
||||
|
||||
function launchConfetti(amount) {
|
||||
confetti.width = window.innerWidth;
|
||||
confetti.height = window.innerHeight;
|
||||
for (let i = 0; i < amount; i += 1) {
|
||||
particles.push({
|
||||
x: Math.random() * confetti.width,
|
||||
y: Math.random() * confetti.height * 0.5,
|
||||
r: randomInt(4, 10),
|
||||
c: ['#FF8C65', '#FFCE63', '#B89DDB', '#74C69D'][randomInt(0, 3)],
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: Math.random() * 2 + 1,
|
||||
alpha: 1,
|
||||
});
|
||||
}
|
||||
if (!animating) animateConfetti();
|
||||
}
|
||||
|
||||
function animateConfetti() {
|
||||
animating = true;
|
||||
function frame() {
|
||||
ctx.clearRect(0, 0, confetti.width, confetti.height);
|
||||
particles = particles.filter((particle) => particle.alpha > 0.02);
|
||||
particles.forEach((particle) => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = particle.alpha;
|
||||
ctx.fillStyle = particle.c;
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.alpha -= 0.018;
|
||||
});
|
||||
if (particles.length) requestAnimationFrame(frame);
|
||||
else {
|
||||
animating = false;
|
||||
ctx.clearRect(0, 0, confetti.width, confetti.height);
|
||||
}
|
||||
}
|
||||
frame();
|
||||
}
|
||||
|
||||
restoreSession();
|
||||
setActiveButton(root.querySelector('[data-levels]'), level.id, 'level');
|
||||
setActiveButton(root.querySelector('[data-modes]'), mode, 'mode');
|
||||
updateStats();
|
||||
renderQuestion();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
export type Level = {
|
||||
id: string;
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type Exercise = {
|
||||
slug: string;
|
||||
title: string;
|
||||
category: 'Grundschule' | 'Jugendliche' | 'Erwachsene';
|
||||
categoryIntro: string;
|
||||
short: string;
|
||||
description: string;
|
||||
operation: 'addition' | 'subtraction' | 'multiplication' | 'division' | 'percentage';
|
||||
accent: 'coral' | 'lavender' | 'mint' | 'yellow';
|
||||
levels: Level[];
|
||||
};
|
||||
|
||||
export const exercises: Exercise[] = [
|
||||
{
|
||||
slug: 'addieren-bis-20',
|
||||
title: 'Addieren bis 20',
|
||||
category: 'Grundschule',
|
||||
categoryIntro: 'Für erste Plusaufgaben mit kleinen Zahlen und schnellen Erfolgserlebnissen.',
|
||||
short: 'Einfache Plusaufgaben für Kinder in der Grundschule.',
|
||||
description: 'Ideal für einen lockeren Einstieg: kleine Summen, klares Feedback und kurze Übungsrunden.',
|
||||
operation: 'addition',
|
||||
accent: 'coral',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 1, max: 10 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 1, max: 20 },
|
||||
{ id: 'knifflig', label: 'Knifflig', min: 5, max: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'subtrahieren-bis-100',
|
||||
title: 'Subtrahieren bis 100',
|
||||
category: 'Grundschule',
|
||||
categoryIntro: 'Minusaufgaben Schritt für Schritt mit passenden Zahlenbereichen.',
|
||||
short: 'Subtraktion üben – vom kleinen Rechnen bis zu größeren Zahlen.',
|
||||
description: 'Geeignet zum Festigen von Minusaufgaben mit verständlichen Schwierigkeitsstufen.',
|
||||
operation: 'subtraction',
|
||||
accent: 'yellow',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 1, max: 20 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 10, max: 50 },
|
||||
{ id: 'stark', label: 'Stark', min: 20, max: 100 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'einmaleins',
|
||||
title: 'Kleines Einmaleins',
|
||||
category: 'Grundschule',
|
||||
categoryIntro: 'Das Einmaleins mit spielerischem Tempo festigen.',
|
||||
short: 'Malaufgaben für die wichtigsten Reihen und sichere Grundlagen.',
|
||||
description: 'Trainiert das schnelle Wiedererkennen von Multiplikationsaufgaben und Ergebnissen.',
|
||||
operation: 'multiplication',
|
||||
accent: 'lavender',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 1, max: 5 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 2, max: 10 },
|
||||
{ id: 'stark', label: 'Stark', min: 4, max: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'dividieren',
|
||||
title: 'Dividieren ohne Rest',
|
||||
category: 'Jugendliche',
|
||||
categoryIntro: 'Geteilt-Aufgaben mit glatten Ergebnissen und klaren Mustern.',
|
||||
short: 'Division üben, ohne sich direkt mit Resten aufzuhalten.',
|
||||
description: 'Eine gute Zwischenstufe für Lernende, die sicher mit Malaufgaben werden wollen.',
|
||||
operation: 'division',
|
||||
accent: 'mint',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 2, max: 5 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 2, max: 10 },
|
||||
{ id: 'stark', label: 'Stark', min: 3, max: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'kopfrechnen',
|
||||
title: 'Kopfrechnen Mix',
|
||||
category: 'Jugendliche',
|
||||
categoryIntro: 'Schnelle Aufgaben für ein besseres Gefühl für Zahlen.',
|
||||
short: 'Gemischte Rechenaufgaben für mehr Sicherheit im Alltag und in der Schule.',
|
||||
description: 'Abwechslung aus Plus, Minus und Mal – direkt im Browser, ohne Anmeldung.',
|
||||
operation: 'addition',
|
||||
accent: 'coral',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 5, max: 30 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 20, max: 80 },
|
||||
{ id: 'stark', label: 'Stark', min: 50, max: 150 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'prozente-im-alltag',
|
||||
title: 'Prozente im Alltag',
|
||||
category: 'Erwachsene',
|
||||
categoryIntro: 'Rabatte, Trinkgeld und einfache Prozentrechnungen sicher beherrschen.',
|
||||
short: 'Praktische Prozentaufgaben für Alltag, Beruf und Einkauf.',
|
||||
description: 'Trainiert typische Prozentrechnungen mit verständlichen Zahlen und direktem Feedback.',
|
||||
operation: 'percentage',
|
||||
accent: 'lavender',
|
||||
levels: [
|
||||
{ id: 'leicht', label: 'Leicht', min: 10, max: 100 },
|
||||
{ id: 'mittel', label: 'Mittel', min: 20, max: 250 },
|
||||
{ id: 'stark', label: 'Stark', min: 50, max: 500 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getExercise(slug: string) {
|
||||
return exercises.find((exercise) => exercise.slug === slug);
|
||||
}
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
title: 'Grundschule',
|
||||
intro: 'Spielerische Aufgaben für Kinder mit klarer Struktur, großen Zahlen und einfachem Feedback.',
|
||||
},
|
||||
{
|
||||
title: 'Jugendliche',
|
||||
intro: 'Mehr Tempo, mehr Sicherheit und abwechslungsreiche Aufgaben für Schule und Alltag.',
|
||||
},
|
||||
{
|
||||
title: 'Erwachsene',
|
||||
intro: 'Praktisches Rechnen für Alltagssituationen – ohne unnötigen Ballast und ohne Login.',
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,13 @@
|
||||
export const site = {
|
||||
name: 'MatheLeicht',
|
||||
url: 'https://matheleicht.de',
|
||||
title: 'MatheLeicht – Einfach Mathe üben',
|
||||
description:
|
||||
'MatheLeicht ist eine einfache Lernplattform für Kinder, Jugendliche und Erwachsene. Rechnen üben ohne Login – direkt im Browser.',
|
||||
owner: 'Justin Michael Eckenweber',
|
||||
email: 'hey@unverpackt-finder.de',
|
||||
phone: '028160031948',
|
||||
address1: 'Rühlestraße 5',
|
||||
address2: '45147 Essen',
|
||||
country: 'Deutschland',
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import { site } from '../data/site';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title = site.title, description = site.description } = Astro.props;
|
||||
const canonical = new URL(Astro.url.pathname, site.url).toString();
|
||||
---
|
||||
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<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=Nunito:wght@400;600;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:site_name" content={site.name} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="site-nav">
|
||||
<a href="/" class="nav-logo">Mathe<span class="dot">Leicht</span></a>
|
||||
<div class="nav-links">
|
||||
<a href="/#kategorien">Kategorien</a>
|
||||
<a href="/#aufgaben">Aufgaben</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
<a href="/#aufgaben" class="nav-cta">Jetzt starten</a>
|
||||
</nav>
|
||||
<slot />
|
||||
<footer>
|
||||
<div class="container footer-inner">
|
||||
<div>
|
||||
<strong style="font-family: 'Nunito', system-ui, sans-serif;">MatheLeicht</strong><br />
|
||||
<span style="color: var(--ink-mid);">Mathe üben ohne Login – direkt im Browser.</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="/impressum">Impressum</a>
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ExerciseTrainer from '../../components/ExerciseTrainer.astro';
|
||||
import { exercises } from '../../data/exercises';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return exercises.map((exercise) => ({ params: { slug: exercise.slug }, props: { exercise } }));
|
||||
}
|
||||
|
||||
const { exercise } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${exercise.title} – MatheLeicht`} description={exercise.short}>
|
||||
<main class="container" style="padding-top: 2rem; padding-bottom: 3rem;">
|
||||
<a href="/" class="button-inline" style="margin-bottom: 1rem;">← Zurück zur Übersicht</a>
|
||||
<div class="section-badge">{exercise.category}</div>
|
||||
<h1 style="font-family:'Nunito', system-ui, sans-serif; font-size: clamp(2.2rem,4vw,3.4rem); margin: 0.2rem 0 0.8rem;">{exercise.title}</h1>
|
||||
<p class="section-intro" style="max-width: 760px; margin-bottom: 1.5rem;">{exercise.description}</p>
|
||||
<ExerciseTrainer exercise={exercise} />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { site } from '../data/site';
|
||||
---
|
||||
|
||||
<BaseLayout title={`Datenschutz – ${site.name}`} description={`Datenschutzhinweise von ${site.name}.`}>
|
||||
<main class="container" style="padding: 3rem 0;">
|
||||
<div class="legal-wrap">
|
||||
<aside class="panel">
|
||||
<div class="control-label">Datenschutz</div>
|
||||
<p>Eine kompakte Datenschutzseite für die Lernplattform MatheLeicht.</p>
|
||||
</aside>
|
||||
<article class="legal-content">
|
||||
<h1 style="font-family:'Nunito', system-ui, sans-serif; font-size: clamp(2.2rem,4vw,3.4rem);">Datenschutz</h1>
|
||||
<p>
|
||||
Beim Besuch dieser Website können technisch notwendige Daten verarbeitet werden, damit die Plattform sicher
|
||||
und stabil ausgeliefert werden kann. Dazu gehören insbesondere IP-Adresse, Zeitpunkt des Zugriffs,
|
||||
aufgerufene Seite, Browserinformationen und technische Logdaten.
|
||||
</p>
|
||||
<p>
|
||||
MatheLeicht benötigt kein Nutzerkonto. Rechenfortschritte innerhalb einzelner Übungen können lokal im Browser
|
||||
gespeichert werden, damit eine laufende Übungsrunde beim Neuladen nicht sofort verloren geht.
|
||||
</p>
|
||||
<p>
|
||||
Verantwortlich für diese Website ist:<br />
|
||||
<strong>{site.owner}</strong><br />
|
||||
{site.address1}<br />
|
||||
{site.address2}<br />
|
||||
{site.country}
|
||||
</p>
|
||||
<p>
|
||||
Kontakt: {site.email} · {site.phone}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { site } from '../data/site';
|
||||
---
|
||||
|
||||
<BaseLayout title={`Impressum – ${site.name}`} description={`Impressum von ${site.name}.`}>
|
||||
<main class="container" style="padding: 3rem 0;">
|
||||
<div class="legal-wrap">
|
||||
<aside class="panel">
|
||||
<div class="control-label">Rechtliches</div>
|
||||
<p>Hier findest du die Anbieterkennzeichnung für MatheLeicht.</p>
|
||||
</aside>
|
||||
<article class="legal-content">
|
||||
<h1 style="font-family:'Nunito', system-ui, sans-serif; font-size: clamp(2.2rem,4vw,3.4rem);">Impressum</h1>
|
||||
<p>
|
||||
<strong>{site.owner}</strong><br />
|
||||
{site.address1}<br />
|
||||
{site.address2}<br />
|
||||
{site.country}
|
||||
</p>
|
||||
<p>
|
||||
E-Mail: {site.email}<br />
|
||||
Telefon: {site.phone}
|
||||
</p>
|
||||
<h2>Verantwortlich für den Inhalt</h2>
|
||||
<p>
|
||||
{site.owner}<br />
|
||||
{site.address1}<br />
|
||||
{site.address2}<br />
|
||||
{site.country}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
+74
-14
@@ -1,17 +1,77 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ExerciseCard from '../components/ExerciseCard.astro';
|
||||
import { categories, exercises } from '../data/exercises';
|
||||
---
|
||||
|
||||
---
|
||||
<BaseLayout>
|
||||
<section class="hero">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
<div class="container hero-grid">
|
||||
<div>
|
||||
<div class="hero-badge">✨ Ohne Anmeldung · Direkt losrechnen</div>
|
||||
<h1>Mathe üben kann auch <em>leicht</em> und freundlich sein.</h1>
|
||||
<p>
|
||||
MatheLeicht ist eine kleine Lernplattform für einfache Rechenaufgaben. Kinder, Jugendliche und Erwachsene
|
||||
können direkt im Browser üben – ohne Login, ohne Ablenkung, mit klaren Schwierigkeitsstufen.
|
||||
</p>
|
||||
<div class="button-row">
|
||||
<a href="#aufgaben" class="button-primary">Aufgaben entdecken</a>
|
||||
<a href="#kategorien" class="button-secondary">Mehr erfahren</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
</body>
|
||||
</html>
|
||||
<div class="hero-card">
|
||||
<div class="mock-label">Beispielaufgabe</div>
|
||||
<div class="mock-question">8 <span class="op">+</span> 7 = ?</div>
|
||||
<div class="mock-answers">
|
||||
<div class="mock-answer">13</div>
|
||||
<div class="mock-answer good">15</div>
|
||||
<div class="mock-answer">17</div>
|
||||
<div class="mock-answer">16</div>
|
||||
</div>
|
||||
<div class="float-badge fb-1">⭐ Sofortiges Feedback</div>
|
||||
<div class="float-badge fb-2">🔥 Kleine Erfolgsmomente</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="container">
|
||||
<section id="kategorien">
|
||||
<div class="section-badge">Kategorien</div>
|
||||
<h2>Für verschiedene Lernstände gemacht</h2>
|
||||
<p class="section-intro">
|
||||
Jede Kategorie ist so gedacht, dass sie sich natürlich anfühlt: einfache Grundlagen für Kinder,
|
||||
alltagstaugliches Üben für Jugendliche und praktische Rechenaufgaben für Erwachsene.
|
||||
</p>
|
||||
|
||||
<div class="info-grid" style="margin-top: 1.4rem;">
|
||||
{categories.map((category) => (
|
||||
<div class="card">
|
||||
<h3>{category.title}</h3>
|
||||
<p>{category.intro}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="aufgaben">
|
||||
<div class="section-badge">Aufgaben</div>
|
||||
<h2>Wähle eine Übung und starte direkt</h2>
|
||||
{categories.map((category) => {
|
||||
const items = exercises.filter((exercise) => exercise.category === category.title);
|
||||
return (
|
||||
<div class="category-block">
|
||||
<h3 style="font-size:1.5rem;">{category.title}</h3>
|
||||
<p class="category-intro">{category.intro}</p>
|
||||
<div class="exercise-grid">
|
||||
{items.map((exercise) => <ExerciseCard exercise={exercise} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Nunito Sans', system-ui, sans-serif;
|
||||
background: #fdf8f0;
|
||||
color: #3b2b1e;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--cream: #fdf8f0;
|
||||
--coral: #ff8c65;
|
||||
--coral-lt: #ffd4c2;
|
||||
--lav: #b89ddb;
|
||||
--lav-lt: #eae3f7;
|
||||
--mint: #74c69d;
|
||||
--mint-lt: #d4f1e4;
|
||||
--yellow: #ffce63;
|
||||
--yellow-lt: #fff3cc;
|
||||
--ink: #3b2b1e;
|
||||
--ink-mid: #7a5f4e;
|
||||
--ink-soft: #b8a090;
|
||||
--card-r: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
nav.site-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 6vw;
|
||||
height: 68px;
|
||||
background: rgba(253, 248, 240, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1.5px solid rgba(255, 140, 101, 0.15);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 1.5rem;
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.nav-logo .dot {
|
||||
color: var(--lav);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink-mid);
|
||||
}
|
||||
|
||||
.nav-links a:hover,
|
||||
.footer-links a:hover,
|
||||
.exercise-link:hover {
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.nav-cta {
|
||||
background: var(--coral);
|
||||
color: white;
|
||||
padding: 10px 18px;
|
||||
border-radius: 999px;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4px 16px rgba(255, 140, 101, 0.32);
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 4.5rem 0 3rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
gap: 3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob-1 { width: 520px; height: 520px; background: var(--coral-lt); top: -80px; right: -60px; }
|
||||
.blob-2 { width: 340px; height: 340px; background: var(--lav-lt); bottom: 10px; left: -70px; }
|
||||
.blob-3 { width: 260px; height: 260px; background: var(--mint-lt); top: 40%; right: 20%; }
|
||||
|
||||
.hero-badge,
|
||||
.section-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--yellow-lt);
|
||||
border: 1.5px solid var(--yellow);
|
||||
color: #9b7a00;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 0.82rem;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: clamp(2.5rem, 4.8vw, 4.2rem);
|
||||
line-height: 1.08;
|
||||
font-weight: 900;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.hero h1 em {
|
||||
font-style: normal;
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.section-intro,
|
||||
.category-intro,
|
||||
.card p,
|
||||
li {
|
||||
color: var(--ink-mid);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.7rem;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
.button-secondary,
|
||||
.button-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: var(--coral);
|
||||
color: white;
|
||||
padding: 14px 26px;
|
||||
box-shadow: 0 6px 24px rgba(255, 140, 101, 0.36);
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
color: var(--ink-mid);
|
||||
}
|
||||
|
||||
.button-inline {
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
background: white;
|
||||
border-radius: 32px;
|
||||
padding: 2rem 2.4rem;
|
||||
box-shadow: 0 20px 60px rgba(59, 43, 30, 0.12);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mock-label {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.mock-question {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: 2.8rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mock-question .op {
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.mock-answers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.mock-answer {
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
border-radius: 18px;
|
||||
background: var(--cream);
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.mock-answer.good {
|
||||
background: var(--mint-lt);
|
||||
border-color: var(--mint);
|
||||
color: #2d8a5a;
|
||||
}
|
||||
|
||||
.float-badge {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 10px 14px;
|
||||
box-shadow: 0 8px 24px rgba(59, 43, 30, 0.12);
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.fb-1 { top: -14px; right: -20px; color: var(--lav); }
|
||||
.fb-2 { bottom: -12px; left: -14px; color: var(--mint); }
|
||||
|
||||
main section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.category-block {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.exercise-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--card-r);
|
||||
padding: 1.4rem;
|
||||
box-shadow: 0 10px 34px rgba(59, 43, 30, 0.07);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 900;
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-flex;
|
||||
padding: 6px 11px;
|
||||
border-radius: 999px;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.card-tag.coral { background: var(--coral-lt); color: var(--coral); }
|
||||
.card-tag.lavender { background: var(--lav-lt); color: var(--lav); }
|
||||
.card-tag.mint { background: var(--mint-lt); color: #37956a; }
|
||||
.card-tag.yellow { background: var(--yellow-lt); color: #9b7a00; }
|
||||
|
||||
.exercise-link {
|
||||
display: inline-flex;
|
||||
margin-top: 1rem;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1.5px solid rgba(255, 140, 101, 0.15);
|
||||
padding: 2rem 0 3rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink-mid);
|
||||
}
|
||||
|
||||
.legal-wrap,
|
||||
.exercise-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
padding: 1.3rem;
|
||||
box-shadow: 0 10px 34px rgba(59, 43, 30, 0.07);
|
||||
}
|
||||
|
||||
.panel h2,
|
||||
.panel h3 {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.button-list {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.option-button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 2px solid transparent;
|
||||
background: #fff;
|
||||
color: var(--ink-mid);
|
||||
border-radius: 14px;
|
||||
padding: 11px 14px;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option-button.active {
|
||||
border-color: var(--coral);
|
||||
background: var(--coral-lt);
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: var(--cream);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 900;
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
background: white;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--coral), var(--yellow));
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: white;
|
||||
border-radius: 32px;
|
||||
padding: 2.2rem;
|
||||
box-shadow: 0 12px 48px rgba(59, 43, 30, 0.08);
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-counter {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-size: clamp(2.4rem, 5vw, 4rem);
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.task-title .op {
|
||||
color: var(--coral);
|
||||
}
|
||||
|
||||
.task-hint {
|
||||
color: var(--ink-soft);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.8rem;
|
||||
}
|
||||
|
||||
.answer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 430px;
|
||||
}
|
||||
|
||||
.answer-chip,
|
||||
.submit-btn,
|
||||
.next-btn {
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.answer-chip {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 2px solid #edeae4;
|
||||
background: var(--cream);
|
||||
font-size: 1.35rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.answer-chip.correct { background: var(--mint-lt); border-color: var(--mint); color: #2d8a5a; }
|
||||
.answer-chip.wrong { background: #ffe8e8; border-color: #ff8080; color: #cc4444; }
|
||||
.answer-chip.reveal { background: var(--mint-lt); border-color: var(--mint); color: #2d8a5a; }
|
||||
.answer-chip.disabled { cursor: default; }
|
||||
|
||||
.input-wrap {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.answer-input {
|
||||
width: 100%;
|
||||
padding: 18px 20px;
|
||||
border-radius: 18px;
|
||||
border: 2px solid #edeae4;
|
||||
background: var(--cream);
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.answer-input.correct { border-color: var(--mint); background: var(--mint-lt); color: #2d8a5a; }
|
||||
.answer-input.wrong { border-color: #ff8080; background: #ffe8e8; color: #cc4444; }
|
||||
|
||||
.submit-btn,
|
||||
.next-btn {
|
||||
border: 0;
|
||||
background: var(--coral);
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
padding: 14px 22px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(255, 140, 101, 0.35);
|
||||
}
|
||||
|
||||
.feedback-bar {
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
border-radius: 18px;
|
||||
padding: 0 1.1rem;
|
||||
font-family: 'Nunito', system-ui, sans-serif;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.feedback-bar.correct { background: var(--mint-lt); color: #2d8a5a; }
|
||||
.feedback-bar.wrong { background: #ffe8e8; color: #cc4444; }
|
||||
|
||||
.result-card {
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-card.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-emoji {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
canvas.confetti {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
background: white;
|
||||
border-radius: 28px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 12px 40px rgba(59, 43, 30, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.hero-grid,
|
||||
.exercise-grid,
|
||||
.info-grid,
|
||||
.legal-wrap,
|
||||
.exercise-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user