feat: light read electrons + progress

This commit is contained in:
electron-rare
2026-06-10 23:24:37 +02:00
parent f539771c7a
commit 3b34941772
3 changed files with 36 additions and 12 deletions
+13 -4
View File
@@ -1,7 +1,7 @@
---
import type { BookModule } from '../lib/moodle';
interface Props { slug: string; modules: BookModule[]; }
const { slug, modules } = Astro.props;
interface Props { slug: string; modules: BookModule[]; readSet?: Set<string>; }
const { slug, modules, readSet } = Astro.props;
---
<div id="sommaire" class="max-w-content mx-auto grid md:grid-cols-2 gap-6">
{modules.map((m, mi) => (
@@ -12,8 +12,17 @@ const { slug, modules } = Astro.props;
<li>
<a href={`/cours/${slug}/module-${mi + 1}/chapitre-${ci + 1}`}
class="text-sm text-slate-300 hover:text-white transition-colors flex gap-3">
<span class="text-copper/60 font-mono text-xs pt-0.5">{ci + 1}</span>
<span>{ch.title}</span>
{readSet?.has(`${mi + 1}-${ci + 1}`) ? (
<>
<span class="text-copper font-mono text-xs pt-0.5">✓</span>
<span class="text-slate-200">{ch.title}</span>
</>
) : (
<>
<span class="text-copper/60 font-mono text-xs pt-0.5">{ci + 1}</span>
<span>{ch.title}</span>
</>
)}
</a>
</li>
))}
+11 -2
View File
@@ -26,14 +26,22 @@ if (user) {
if (reads.some((r) => !r.moodle_synced))
syncToMoodle({ sub: user.sub, email: user.email, name: user.name }, course.slug).catch(() => {});
}
const readSet = new Set(reads.map((r) => `${r.module_n}-${r.chapitre_n}`));
const hubData = {
slug: course.slug,
modules: modules.map((m, mi) => ({
title: m.title,
chapters: m.chapters.map((ch, ci) => ({ title: ch.title, url: `/cours/${course.slug}/module-${mi + 1}/chapitre-${ci + 1}` })),
chapters: m.chapters.map((ch, ci) => ({
title: ch.title,
url: `/cours/${course.slug}/module-${mi + 1}/chapitre-${ci + 1}`,
read: readSet.has(`${mi + 1}-${ci + 1}`),
})),
})),
};
const total = modules.reduce((s, m) => s + m.chapters.length, 0);
const nbLus = hubData.modules.reduce((s, m) => s + m.chapters.filter((c) => c.read).length, 0);
const pct = total ? Math.round((nbLus / total) * 100) : 0;
---
<Base title={course.title} description={course.blurb}>
<Nav user={user ? { name: user.name } : null} currentPath={Astro.url.pathname} />
@@ -42,9 +50,10 @@ const hubData = {
<p class="text-sm text-copper uppercase tracking-wider mb-2">Formation</p>
<h1 class="text-4xl md:text-5xl font-semibold tracking-tight text-white">{course.title}</h1>
<p class="text-lg text-slate-400 mt-3 max-w-2xl">{course.blurb}</p>
{user && <p class="mt-4 text-copper-soft text-sm">{nbLus} / {total} chapitres lus · {pct} %</p>}
</header>
<div id="atom-mount" class="hidden max-w-content mx-auto" data-hub={JSON.stringify(hubData)}></div>
<Sommaire slug={course.slug} modules={modules} />
<Sommaire slug={course.slug} modules={modules} readSet={readSet} />
</main>
</Base>
+12 -6
View File
@@ -2,12 +2,13 @@ import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
interface HubChapter { title: string; url: string; }
interface HubChapter { title: string; url: string; read?: boolean; }
interface HubModule { title: string; chapters: HubChapter[]; }
interface HubData { slug: string; modules: HubModule[]; }
const COPPER = 0xf97316;
const COPPER_SOFT = 0xfdba74;
const SLATE = 0x64748b;
export function mountAtomHub(mount: HTMLElement, data: HubData): void {
const W = mount.clientWidth;
@@ -42,7 +43,9 @@ export function mountAtomHub(mount: HTMLElement, data: HubData): void {
// Orbits: one tilted ellipse per module, electrons = chapters
const electronMeshes: THREE.Mesh[] = [];
const electronGeo = new THREE.SphereGeometry(0.09, 16, 16);
const electronMat = new THREE.MeshBasicMaterial({ color: COPPER_SOFT });
// Two shared materials (read/unread) — no per-electron clone
const matRead = new THREE.MeshBasicMaterial({ color: COPPER_SOFT });
const matUnread = new THREE.MeshBasicMaterial({ color: SLATE });
data.modules.forEach((mod, mi) => {
const group = new THREE.Group();
@@ -59,20 +62,23 @@ export function mountAtomHub(mount: HTMLElement, data: HubData): void {
);
group.add(orbit);
// Module label anchored on the orbit
// Module label anchored on the orbit (with progress when started)
const nRead = mod.chapters.filter((c) => c.read).length;
const labelEl = document.createElement('div');
labelEl.textContent = mod.title.replace(/^Module (\d+) ?: ?/, 'M$1 · ');
labelEl.textContent = nRead > 0
? `M${mi + 1} · ${mod.title.replace(/^Module \d+ ?: ?/, '')}${nRead}/${mod.chapters.length}`
: mod.title.replace(/^Module (\d+) ?: ?/, 'M$1 · ');
labelEl.style.cssText = 'color:#fdba74;font-size:12px;background:rgba(11,15,23,.7);border:1px solid rgba(249,115,22,.35);border-radius:99px;padding:2px 10px;white-space:nowrap;';
const label = new CSS2DObject(labelEl);
label.position.set(rx, 0, 0);
group.add(label);
mod.chapters.forEach((ch, ci) => {
const e = new THREE.Mesh(electronGeo, electronMat.clone());
const e = new THREE.Mesh(electronGeo, ch.read ? matRead : matUnread);
const angle = (ci / mod.chapters.length) * Math.PI * 2;
e.position.set(Math.cos(angle) * rx, 0, Math.sin(angle) * rz);
e.userData = { url: ch.url, title: ch.title, baseAngle: angle, rx, rz, speed: 0.05 + mi * 0.012 };
const eGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: COPPER, transparent: true, opacity: 0.55, depthWrite: false }));
const eGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: COPPER, transparent: true, opacity: ch.read ? 0.55 : 0.15, depthWrite: false }));
eGlow.scale.setScalar(0.5);
e.add(eGlow);
group.add(e);