feat: light read electrons + progress
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user