mirror of
https://github.com/tiennm99/mathmax.git
synced 2026-08-06 10:21:37 +00:00
feat: enable Số học + Đại số topic tiles + landings
This commit is contained in:
@@ -4,8 +4,10 @@ Toán tương tác cho học sinh THCS Việt Nam (lớp 6-9). Số học, Đạ
|
||||
|
||||
## Status
|
||||
|
||||
3 bài hình học đầu tiên đã ra mắt. Số học và Đại số vẫn "Sắp ra mắt".
|
||||
5 bài đã ra mắt — số học, đại số, hình học đều có ít nhất một bài.
|
||||
|
||||
- Lớp 6 — Ước chung lớn nhất (Euclid): `/so-hoc/uoc-chung-lon-nhat/`
|
||||
- Lớp 7 — Hiệu hai bình phương: `/dai-so/hieu-hai-binh-phuong/`
|
||||
- Lớp 7 — Tam giác bằng nhau (SSS): `/hinh-hoc/tam-giac-bang-nhau/`
|
||||
- Lớp 8 — Tam giác đồng dạng: `/hinh-hoc/tam-giac-dong-dang/`
|
||||
- Lớp 9 — Góc nội tiếp: `/hinh-hoc/goc-noi-tiep/`
|
||||
@@ -34,7 +36,8 @@ Live URL: https://tiennm99.github.io/mathmax/
|
||||
- **Static**: SvelteKit + `@sveltejs/adapter-static`, `paths.base = '/mathmax'`, output `build/`.
|
||||
- **Styling**: Tailwind 3 (PostCSS) + Be Vietnam Pro (woff2 qua `@fontsource`). Tick palette `colors.pair.{1,2,3}` được khai báo trong `tailwind.config.js`.
|
||||
- **Language**: JavaScript only (Svelte 5, JSDoc qua `jsconfig.json` với `checkJs: true`).
|
||||
- **Math engine**: `src/lib/geom-engine/` — module thuần (không phụ thuộc DOM): `vec`, `triangle`, `circle`, `ticks`. Vitest unit tests đi kèm.
|
||||
- **Math engines**: `src/lib/geom-engine/` (vec, triangle, circle, ticks) và `src/lib/numtheory-engine/` (gcd, lcm, gcdSteps). Module thuần, không phụ thuộc DOM. Vitest unit tests đi kèm.
|
||||
- **Math typography**: `src/lib/components/tex.svelte` — wrapper KaTeX duy nhất. SSR qua `renderToString`, không cần JS phía client để hiển thị.
|
||||
- **Lessons**: mỗi bài là một `+page.svelte`; copy tiếng Việt colocate trong `src/lib/lessons/<slug>/copy.vi.js`.
|
||||
- **Drag**: Svelte action `use:draggable` (`src/lib/actions/draggable.svelte.js`) — Pointer Events + bàn phím mũi tên cho a11y.
|
||||
- **i18n**: Hiện chỉ có tiếng Việt. Site chrome ở `src/lib/i18n/site.vi.js`. English thêm sau bằng cách tạo `*.en.js` song song.
|
||||
|
||||
@@ -55,3 +55,7 @@ When the trigger fires:
|
||||
## Initial CI note
|
||||
|
||||
Initial CI uses `npm install` (no lockfile committed yet). After first successful run, user commits `package-lock.json` locally and we switch CI to `npm ci` in a follow-up PR.
|
||||
|
||||
## Bundle notes
|
||||
|
||||
- **KaTeX** (`katex@^0.16`) is loaded site-wide via `import 'katex/dist/katex.min.css'` in `+layout.svelte`. CSS + woff2 fonts add ~280KB total to the static asset payload. Math is server-rendered via `katex.renderToString` (in `src/lib/components/tex.svelte`), so KaTeX **JavaScript runs only at build time** — the runtime cost is the CSS + fonts, not the JS module. If Lighthouse perf drops below 90 after content growth, consider scoping CSS imports per-route.
|
||||
|
||||
@@ -26,14 +26,14 @@ export const topics = {
|
||||
'so-hoc': {
|
||||
title: 'Số học',
|
||||
blurb: 'Phép tính, ước-bội, phân số, số nguyên. Trực quan hoá thuật toán và quy luật.',
|
||||
status: 'comingSoon',
|
||||
href: null,
|
||||
status: 'live',
|
||||
href: '/so-hoc/',
|
||||
},
|
||||
'dai-so': {
|
||||
title: 'Đại số',
|
||||
blurb: 'Biểu thức, phương trình, hàm số. Thao tác kéo-thả các đối tượng đại số.',
|
||||
status: 'comingSoon',
|
||||
href: null,
|
||||
status: 'live',
|
||||
href: '/dai-so/',
|
||||
},
|
||||
'hinh-hoc': {
|
||||
title: 'Hình học',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { vi as gcdCopy } from './uoc-chung-lon-nhat/copy.vi.js';
|
||||
import { vi as diffSquaresCopy } from './hieu-hai-binh-phuong/copy.vi.js';
|
||||
import { vi as sssCopy } from './tam-giac-bang-nhau/copy.vi.js';
|
||||
import { vi as similarityCopy } from './tam-giac-dong-dang/copy.vi.js';
|
||||
import { vi as inscribedCopy } from './goc-noi-tiep/copy.vi.js';
|
||||
@@ -7,8 +9,9 @@ import { vi as inscribedCopy } from './goc-noi-tiep/copy.vi.js';
|
||||
* gradeLabel: string, intro: string, [k: string]: any}} LessonCopy
|
||||
*/
|
||||
|
||||
// Order: by topic (số học → đại số → hình học), then by grade ascending.
|
||||
/** @type {LessonCopy[]} */
|
||||
export const lessons = [sssCopy, similarityCopy, inscribedCopy];
|
||||
export const lessons = [gcdCopy, diffSquaresCopy, sssCopy, similarityCopy, inscribedCopy];
|
||||
|
||||
/** @param {string} topic */
|
||||
export function lessonsByTopic(topic) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { t } from '$lib/i18n/index.js';
|
||||
import { lessonsByTopic } from '$lib/lessons/registry.js';
|
||||
|
||||
const copy = t();
|
||||
const topic = copy.topics['dai-so'];
|
||||
const lessons = lessonsByTopic('dai-so');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{topic.title} — {copy.site.title}</title>
|
||||
<meta name="description" content={topic.blurb} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href={base + '/'} class="text-xl font-bold text-indigo-600 tracking-tight">MathMax</a>
|
||||
<nav aria-label="Điều hướng chính"></nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="bg-slate-50 min-h-screen">
|
||||
<section class="max-w-4xl mx-auto px-4 py-12">
|
||||
<nav class="mb-6 text-sm">
|
||||
<a href={base + '/'} class="text-indigo-600 hover:underline">{copy.lessonChrome.backToHub}</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-slate-900 mb-2">{topic.title}</h1>
|
||||
<p class="text-lg text-slate-600">{topic.blurb}</p>
|
||||
</header>
|
||||
|
||||
<ul class="grid md:grid-cols-2 gap-4" aria-label="Danh sách bài học đại số">
|
||||
{#each lessons as lesson (lesson.slug)}
|
||||
<li>
|
||||
<a
|
||||
href={base + `/dai-so/${lesson.slug}/`}
|
||||
class="block bg-white rounded-2xl border border-slate-200 p-5 transition hover:border-indigo-400 hover:shadow-sm"
|
||||
>
|
||||
<div class="flex items-baseline justify-between gap-3 mb-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">{lesson.gradeLabel}</span>
|
||||
<span class="text-xs text-emerald-700">{copy.status.live}</span>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-1">{lesson.title}</h2>
|
||||
<p class="text-sm text-slate-600 leading-relaxed">{lesson.intro}</p>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-6 text-center text-sm text-slate-500">
|
||||
© {new Date().getFullYear()} ·
|
||||
<a href="https://github.com/tiennm99/mathmax" class="text-indigo-600 hover:underline" target="_blank" rel="noopener noreferrer">Mã nguồn</a>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { t } from '$lib/i18n/index.js';
|
||||
import { vi as m } from '$lib/lessons/hieu-hai-binh-phuong/copy.vi.js';
|
||||
import Tex from '$lib/components/tex.svelte';
|
||||
|
||||
const copy = t();
|
||||
const U = 18; // px per math unit
|
||||
const PAD = 24;
|
||||
const PAIR1 = '#D7263D';
|
||||
const PAIR2 = '#1B998B';
|
||||
|
||||
let a = $state(5);
|
||||
let b = $state(2);
|
||||
|
||||
// Clamp b ≤ a — guard prevents feedback loops.
|
||||
$effect(() => {
|
||||
if (b > a) b = a;
|
||||
});
|
||||
|
||||
const aSafe = $derived(Math.max(2, Math.min(10, Math.trunc(Number(a)) || 2)));
|
||||
const bSafe = $derived(Math.max(1, Math.min(aSafe, Math.trunc(Number(b)) || 1)));
|
||||
|
||||
const aSq = $derived(aSafe * aSafe);
|
||||
const bSq = $derived(bSafe * bSafe);
|
||||
const diff = $derived(aSq - bSq);
|
||||
const product = $derived((aSafe + bSafe) * (aSafe - bSafe));
|
||||
|
||||
// Square diagram dimensions.
|
||||
const sqViewW = $derived(10 * U + PAD * 2);
|
||||
const sqViewH = $derived(10 * U + PAD * 2);
|
||||
const sqSize = $derived(aSafe * U);
|
||||
const cutSize = $derived(bSafe * U);
|
||||
const stripH = $derived(bSafe * U); // top-strip height
|
||||
const stripW = $derived((aSafe - bSafe) * U); // top-strip width
|
||||
|
||||
// Rectangle diagram dimensions (a+b) × (a-b).
|
||||
const rectViewW = $derived(20 * U + PAD * 2);
|
||||
const rectViewH = $derived(10 * U + PAD * 2);
|
||||
const rectW = $derived((aSafe + bSafe) * U);
|
||||
const rectH = $derived((aSafe - bSafe) * U);
|
||||
const rectLeftW = $derived(aSafe * U);
|
||||
const rectRightW = $derived(bSafe * U);
|
||||
|
||||
const liveTex = $derived(
|
||||
`${aSafe}^2 - ${bSafe}^2 = (${aSafe} + ${bSafe})(${aSafe} - ${bSafe}) = ${diff}`
|
||||
);
|
||||
const identityTex = 'a^2 - b^2 = (a + b)(a - b)';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.title} — {copy.site.title}</title>
|
||||
<meta name="description" content={m.intro} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href={base + '/'} class="text-xl font-bold text-indigo-600 tracking-tight">MathMax</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="bg-slate-50 min-h-screen">
|
||||
<article class="max-w-3xl mx-auto px-4 py-8">
|
||||
<nav class="mb-4 text-sm">
|
||||
<a href={base + '/dai-so/'} class="text-indigo-600 hover:underline">{copy.lessonChrome.backToTopic}</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-6">
|
||||
<div class="text-sm uppercase tracking-wide text-slate-500">{m.gradeLabel}</div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 mt-1 mb-2">{m.title}</h1>
|
||||
<p class="text-slate-700 leading-relaxed">{m.intro}</p>
|
||||
</header>
|
||||
|
||||
<section class="mb-6 bg-white rounded-lg border border-slate-200 p-5">
|
||||
<p class="text-sm text-slate-500 mb-3">{m.instruction}</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-slate-700">{m.sliderALabel}: <span class="tabular-nums text-indigo-700">{aSafe}</span></span>
|
||||
<input
|
||||
type="range" min="2" max="10" step="1"
|
||||
bind:value={a}
|
||||
aria-valuetext={`a bằng ${aSafe}`}
|
||||
class="mt-1 w-full accent-indigo-600"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-slate-700">{m.sliderBLabel}: <span class="tabular-nums text-indigo-700">{bSafe}</span></span>
|
||||
<input
|
||||
type="range" min="1" max={aSafe} step="1"
|
||||
bind:value={b}
|
||||
aria-valuetext={`b bằng ${bSafe}`}
|
||||
class="mt-1 w-full accent-indigo-600"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-white rounded-lg border border-slate-200 p-3">
|
||||
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 text-center">{m.diagramSquareLabel}</h3>
|
||||
<svg
|
||||
viewBox="0 0 {sqViewW} {sqViewH}"
|
||||
class="block w-full"
|
||||
role="img"
|
||||
aria-label={m.diagramSquareLabel}
|
||||
>
|
||||
<!-- L-shape pieces -->
|
||||
{#if stripW > 0}
|
||||
<rect x={PAD} y={PAD} width={stripW} height={stripH} fill={PAIR1} fill-opacity="0.55" stroke={PAIR1} stroke-width="1" />
|
||||
{/if}
|
||||
{#if rectH > 0}
|
||||
<rect x={PAD} y={PAD + stripH} width={sqSize} height={sqSize - stripH} fill={PAIR2} fill-opacity="0.55" stroke={PAIR2} stroke-width="1" />
|
||||
{/if}
|
||||
<!-- Cut corner outline (white square, dashed) -->
|
||||
<rect x={PAD + stripW} y={PAD} width={cutSize} height={cutSize} fill="white" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 3" />
|
||||
<!-- Outer square outline -->
|
||||
<rect x={PAD} y={PAD} width={sqSize} height={sqSize} fill="none" stroke="#1f2937" stroke-width="1.5" />
|
||||
<!-- Labels -->
|
||||
<text x={PAD + sqSize / 2} y={PAD - 8} text-anchor="middle" font-size="13" fill="#1f2937" font-weight="600">a = {aSafe}</text>
|
||||
<text x={PAD - 8} y={PAD + sqSize / 2} text-anchor="end" dominant-baseline="middle" font-size="13" fill="#1f2937" font-weight="600">a</text>
|
||||
{#if cutSize > 0}
|
||||
<text x={PAD + stripW + cutSize / 2} y={PAD - 4} text-anchor="middle" font-size="11" fill="#64748b">b = {bSafe}</text>
|
||||
<text x={PAD + sqSize + 4} y={PAD + cutSize / 2} text-anchor="start" dominant-baseline="middle" font-size="11" fill="#64748b">b</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-slate-200 p-3">
|
||||
<h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 text-center">{m.diagramRectLabel}</h3>
|
||||
<svg
|
||||
viewBox="0 0 {rectViewW} {rectViewH}"
|
||||
class="block w-full"
|
||||
role="img"
|
||||
aria-label={m.diagramRectLabel}
|
||||
>
|
||||
{#if rectH > 0}
|
||||
<!-- Left part: a × (a-b), originated from L bottom -->
|
||||
<rect x={PAD} y={PAD} width={rectLeftW} height={rectH} fill={PAIR2} fill-opacity="0.55" stroke={PAIR2} stroke-width="1" />
|
||||
<!-- Right part: b × (a-b), originated from L top strip rotated 90° -->
|
||||
<rect x={PAD + rectLeftW} y={PAD} width={rectRightW} height={rectH} fill={PAIR1} fill-opacity="0.55" stroke={PAIR1} stroke-width="1" />
|
||||
<!-- Outer outline -->
|
||||
<rect x={PAD} y={PAD} width={rectW} height={rectH} fill="none" stroke="#1f2937" stroke-width="1.5" />
|
||||
{:else}
|
||||
<!-- Degenerate a = b: zero-height rectangle. Show a flat line w/ note. -->
|
||||
<line x1={PAD} y1={PAD + 5} x2={PAD + rectW} y2={PAD + 5} stroke="#1f2937" stroke-width="1.5" />
|
||||
<text x={PAD + rectW / 2} y={PAD + 30} text-anchor="middle" font-size="12" fill="#64748b" font-style="italic">a = b → a − b = 0 → diện tích = 0</text>
|
||||
{/if}
|
||||
<!-- Labels -->
|
||||
<text x={PAD + rectW / 2} y={PAD - 8} text-anchor="middle" font-size="13" fill="#1f2937" font-weight="600">a + b = {aSafe + bSafe}</text>
|
||||
{#if rectH > 0}
|
||||
<text x={PAD - 8} y={PAD + rectH / 2} text-anchor="end" dominant-baseline="middle" font-size="13" fill="#1f2937" font-weight="600">a − b = {aSafe - bSafe}</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="mb-6 text-sm text-slate-600 italic">{m.rearrangeNote}</p>
|
||||
|
||||
<section class="mb-6 rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-3">{m.numericTitle}</h2>
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm tabular-nums mb-4" aria-live="polite">
|
||||
<div><span class="font-semibold">a²:</span> {aSq}</div>
|
||||
<div><span class="font-semibold">b²:</span> {bSq}</div>
|
||||
<div><span class="font-semibold" style="color:{PAIR1}">a² − b²:</span> {diff}</div>
|
||||
<div><span class="font-semibold" style="color:{PAIR2}">(a + b)(a − b):</span> {product}</div>
|
||||
</div>
|
||||
<div class="text-slate-800">
|
||||
<Tex display math={liveTex} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-2">{m.identityTitle}</h2>
|
||||
<p class="rounded-lg bg-slate-100 p-4 text-slate-800 mb-3">{m.identityStatement}</p>
|
||||
<Tex display math={identityTex} />
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-2">{m.exampleTitle}</h2>
|
||||
<p class="text-slate-700 leading-relaxed">{m.exampleBody}</p>
|
||||
</section>
|
||||
|
||||
<footer class="border-t border-slate-200 pt-4 text-sm text-slate-500">
|
||||
{m.nextTeaser}
|
||||
</footer>
|
||||
</article>
|
||||
</main>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { t } from '$lib/i18n/index.js';
|
||||
import { lessonsByTopic } from '$lib/lessons/registry.js';
|
||||
|
||||
const copy = t();
|
||||
const topic = copy.topics['so-hoc'];
|
||||
const lessons = lessonsByTopic('so-hoc');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{topic.title} — {copy.site.title}</title>
|
||||
<meta name="description" content={topic.blurb} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href={base + '/'} class="text-xl font-bold text-indigo-600 tracking-tight">MathMax</a>
|
||||
<nav aria-label="Điều hướng chính"></nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="bg-slate-50 min-h-screen">
|
||||
<section class="max-w-4xl mx-auto px-4 py-12">
|
||||
<nav class="mb-6 text-sm">
|
||||
<a href={base + '/'} class="text-indigo-600 hover:underline">{copy.lessonChrome.backToHub}</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-slate-900 mb-2">{topic.title}</h1>
|
||||
<p class="text-lg text-slate-600">{topic.blurb}</p>
|
||||
</header>
|
||||
|
||||
<ul class="grid md:grid-cols-2 gap-4" aria-label="Danh sách bài học số học">
|
||||
{#each lessons as lesson (lesson.slug)}
|
||||
<li>
|
||||
<a
|
||||
href={base + `/so-hoc/${lesson.slug}/`}
|
||||
class="block bg-white rounded-2xl border border-slate-200 p-5 transition hover:border-indigo-400 hover:shadow-sm"
|
||||
>
|
||||
<div class="flex items-baseline justify-between gap-3 mb-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">{lesson.gradeLabel}</span>
|
||||
<span class="text-xs text-emerald-700">{copy.status.live}</span>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-1">{lesson.title}</h2>
|
||||
<p class="text-sm text-slate-600 leading-relaxed">{lesson.intro}</p>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-6 text-center text-sm text-slate-500">
|
||||
© {new Date().getFullYear()} ·
|
||||
<a href="https://github.com/tiennm99/mathmax" class="text-indigo-600 hover:underline" target="_blank" rel="noopener noreferrer">Mã nguồn</a>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { t } from '$lib/i18n/index.js';
|
||||
import { vi as m } from '$lib/lessons/uoc-chung-lon-nhat/copy.vi.js';
|
||||
import { gcd, lcm, gcdSteps } from '$lib/numtheory-engine/index.js';
|
||||
import Tex from '$lib/components/tex.svelte';
|
||||
|
||||
const copy = t();
|
||||
|
||||
let a = $state(48);
|
||||
let b = $state(18);
|
||||
let stepIdx = $state(0);
|
||||
|
||||
// Sanitize inputs into the [1, 999] integer range without throwing.
|
||||
const aSafe = $derived(Math.max(1, Math.min(999, Math.trunc(Number(a)) || 1)));
|
||||
const bSafe = $derived(Math.max(1, Math.min(999, Math.trunc(Number(b)) || 1)));
|
||||
|
||||
const steps = $derived(gcdSteps(aSafe, bSafe));
|
||||
const visibleSteps = $derived(steps.slice(0, stepIdx));
|
||||
const finished = $derived(stepIdx >= steps.length);
|
||||
const result = $derived(finished ? gcd(aSafe, bSafe) : null);
|
||||
const lcmValue = $derived(result !== null && result !== 0 ? lcm(aSafe, bSafe) : null);
|
||||
|
||||
// Reset trace whenever inputs change.
|
||||
$effect(() => {
|
||||
aSafe; bSafe;
|
||||
stepIdx = 0;
|
||||
});
|
||||
|
||||
const lcmTex = $derived(
|
||||
lcmValue !== null && result !== null
|
||||
? `\\text{BCNN}(${aSafe}, ${bSafe}) = \\frac{${aSafe} \\cdot ${bSafe}}{${result}} = ${lcmValue}`
|
||||
: ''
|
||||
);
|
||||
const gcdTex = $derived(
|
||||
result !== null ? `\\text{ƯCLN}(${aSafe}, ${bSafe}) = ${result}` : ''
|
||||
);
|
||||
|
||||
function stepOnce() { if (stepIdx < steps.length) stepIdx += 1; }
|
||||
function autoFinish() { stepIdx = steps.length; }
|
||||
function reset() { stepIdx = 0; }
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.title} — {copy.site.title}</title>
|
||||
<meta name="description" content={m.intro} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="border-b border-slate-200 bg-white">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href={base + '/'} class="text-xl font-bold text-indigo-600 tracking-tight">MathMax</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="bg-slate-50 min-h-screen">
|
||||
<article class="max-w-3xl mx-auto px-4 py-8">
|
||||
<nav class="mb-4 text-sm">
|
||||
<a href={base + '/so-hoc/'} class="text-indigo-600 hover:underline">{copy.lessonChrome.backToTopic}</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-6">
|
||||
<div class="text-sm uppercase tracking-wide text-slate-500">{m.gradeLabel}</div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 mt-1 mb-2">{m.title}</h1>
|
||||
<p class="text-slate-700 leading-relaxed">{m.intro}</p>
|
||||
</header>
|
||||
|
||||
<section class="mb-6 bg-white rounded-lg border border-slate-200 p-5">
|
||||
<p class="text-sm text-slate-500 mb-3">{m.instruction}</p>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-slate-700">{m.inputALabel}</span>
|
||||
<input
|
||||
type="number" min="1" max="999" step="1"
|
||||
bind:value={a}
|
||||
class="mt-1 w-full border border-slate-300 rounded-md px-3 py-2 text-lg tabular-nums focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-slate-700">{m.inputBLabel}</span>
|
||||
<input
|
||||
type="number" min="1" max="999" step="1"
|
||||
bind:value={b}
|
||||
class="mt-1 w-full border border-slate-300 rounded-md px-3 py-2 text-lg tabular-nums focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={stepOnce}
|
||||
disabled={finished}
|
||||
class="px-4 py-2 rounded-md bg-indigo-600 text-white font-semibold hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-slate-300 disabled:cursor-not-allowed"
|
||||
>{m.stepBtn}</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={autoFinish}
|
||||
disabled={finished}
|
||||
class="px-4 py-2 rounded-md bg-white border border-indigo-600 text-indigo-700 font-semibold hover:bg-indigo-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>{m.autoBtn}</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={reset}
|
||||
class="px-4 py-2 rounded-md bg-white border border-slate-300 text-slate-700 font-semibold hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
||||
>{m.resetBtn}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-2">{m.rowDescription}</h2>
|
||||
<div aria-live="polite">
|
||||
<table class="w-full border-collapse text-sm tabular-nums">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-slate-300 text-slate-600">
|
||||
<th class="text-left py-2 px-3 font-semibold">#</th>
|
||||
<th class="text-right py-2 px-3 font-semibold">{m.tableHeaderA}</th>
|
||||
<th class="text-right py-2 px-3 font-semibold">{m.tableHeaderQ} · {m.tableHeaderB}</th>
|
||||
<th class="text-right py-2 px-3 font-semibold">+ {m.tableHeaderR}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each visibleSteps as step, i (i)}
|
||||
<tr class="border-b border-slate-200">
|
||||
<td class="py-2 px-3 text-slate-500">{i + 1}</td>
|
||||
<td class="py-2 px-3 text-right font-semibold text-slate-900">{step.a}</td>
|
||||
<td class="py-2 px-3 text-right text-slate-700">= {step.q} · {step.b}</td>
|
||||
<td class="py-2 px-3 text-right text-slate-700">+ {step.r}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if visibleSteps.length === 0}
|
||||
<tr><td colspan="4" class="py-4 px-3 text-center text-slate-400 italic">Nhấn "{m.stepBtn}" để bắt đầu</td></tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if result !== null}
|
||||
<section class="mb-6 rounded-lg border border-emerald-200 bg-emerald-50 p-5" aria-live="polite">
|
||||
<div class="text-emerald-800 font-semibold text-lg mb-2">
|
||||
✓ {m.resultLabel}(<span class="tabular-nums">{aSafe}</span>, <span class="tabular-nums">{bSafe}</span>) = <span class="tabular-nums">{result}</span>
|
||||
</div>
|
||||
<div class="text-slate-800 mb-3">
|
||||
<Tex display math={gcdTex} />
|
||||
</div>
|
||||
{#if lcmValue !== null}
|
||||
<div class="text-slate-800">
|
||||
<div class="text-sm font-semibold text-slate-600 mb-1">{m.lcmLabel}:</div>
|
||||
<Tex display math={lcmTex} />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-2">{m.theoremTitle}</h2>
|
||||
<p class="rounded-lg bg-slate-100 p-4 text-slate-800">{m.theoremStatement}</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-2">{m.exampleTitle}</h2>
|
||||
<p class="text-slate-700 leading-relaxed">{m.exampleBody}</p>
|
||||
</section>
|
||||
|
||||
<footer class="border-t border-slate-200 pt-4 text-sm text-slate-500">
|
||||
{m.nextTeaser}
|
||||
</footer>
|
||||
</article>
|
||||
</main>
|
||||
Reference in New Issue
Block a user