Initial MatheLeicht Astro app

This commit is contained in:
2026-04-19 20:41:10 +00:00
parent 0de87e40e6
commit 709ae12546
10 changed files with 1361 additions and 14 deletions
+12
View File
@@ -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>
+400
View File
@@ -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>
+130
View File
@@ -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;
+13
View File
@@ -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',
};
+57
View File
@@ -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>
+21
View File
@@ -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>
+37
View File
@@ -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>
+35
View File
@@ -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
View File
@@ -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>
+582
View File
@@ -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;
}
}