feat: made stuff

This commit is contained in:
2026-05-01 14:14:33 +02:00
parent 9a4c6863d1
commit b6586f7715
19 changed files with 1870 additions and 116 deletions
+4
View File
@@ -56,6 +56,10 @@ jobs:
run: |
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
sed 's/--> statement-breakpoint/;/g' databases/migrations/0003_add_session2_and_malus.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
sed 's/--> statement-breakpoint/;/g' databases/migrations/0004_add_stages_and_mobilites.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
- name: Install dependencies
run: npm install --ignore-scripts && deno install
+26
View File
@@ -27,6 +27,32 @@ export default function makeSlug(basePath: string): Route {
}
}
// For multi-segment slugs (e.g. "overview/12345"), try
// partials/<dir>/[param].tsx and inject the param into context.params
if (!page && slug.includes("/")) {
const idx = slug.indexOf("/");
const dir = slug.slice(0, idx);
const param = slug.slice(idx + 1);
// Discover the dynamic segment name from the file system
try {
const entries: string[] = [];
for await (const entry of Deno.readDir(`${basePath}/partials/${dir}`)) {
if (entry.isFile) entries.push(entry.name);
}
const dynFile = entries.find((n) =>
n.startsWith("[") && n.endsWith("].tsx")
);
if (dynFile) {
const paramName = dynFile.slice(1, -5); // "[numEtud].tsx" → "numEtud"
context.params[paramName] = param;
page = (await import(`${basePath}/partials/${dir}/${dynFile}`)).Page;
}
} catch {
// directory doesn't exist or no dynamic file
}
}
if (!page) {
return context.renderNotFound();
}
+1536
View File
File diff suppressed because it is too large Load Diff
@@ -115,7 +115,7 @@ export default function AdminEnseignements() {
return (
<div class="page-content">
<h2 class="page-title">Assignations Enseignant Module / Promo</h2>
<h2 class="page-title">Assignations Enseignant ECUE / Promo</h2>
{error && <p class="state-error">{error}</p>}
@@ -135,7 +135,7 @@ export default function AdminEnseignements() {
onChange={(e) =>
setFilterModule((e.target as HTMLSelectElement).value)}
>
<option value="">Module </option>
<option value="">ECUE </option>
{modules.map((m) => (
<option key={m.id} value={m.id}>{m.id} {m.nom}</option>
))}
@@ -194,7 +194,7 @@ export default function AdminEnseignements() {
</select>
</div>
<div class="form-field">
<label>Module</label>
<label>ECUE</label>
<select
class="filter-select"
value={addModule}
@@ -202,7 +202,7 @@ export default function AdminEnseignements() {
setAddModule((e.target as HTMLSelectElement).value)}
style="min-width: 0; width: 100%"
>
<option value="">Module...</option>
<option value="">ECUE...</option>
{modules.map((m) => (
<option key={m.id} value={m.id}>
{m.id} -- {m.nom}
@@ -251,7 +251,7 @@ export default function AdminEnseignements() {
<thead>
<tr>
<th>Promo</th>
<th>Module</th>
<th>ECUE</th>
<th>Enseignant (User.id)</th>
<th>Actions</th>
</tr>
@@ -319,7 +319,7 @@ export default function AdminEnseignements() {
<div class="info-note">
<p>
Un même module peut être enseigné par plusieurs utilisateurs sur une
Un même ECUE peut être enseigné par plusieurs utilisateurs sur une
même promo.
</p>
<p class="info-note-dim">
@@ -22,7 +22,7 @@ export default function AdminModules() {
fetch("/admin/api/enseignements"),
fetch("/admin/api/users"),
]);
if (!mRes.ok) throw new Error("Impossible de charger les modules");
if (!mRes.ok) throw new Error("Impossible de charger les ECUEs");
setModules(await mRes.json());
if (eRes.ok) setEnseignements(await eRes.json());
if (uRes.ok) setUsers(await uRes.json());
@@ -61,7 +61,7 @@ export default function AdminModules() {
}
async function deleteModule(id: string) {
if (!confirm(`Supprimer le module ${id} ?`)) return;
if (!confirm(`Supprimer l'ECUE ${id} ?`)) return;
try {
const res = await fetch(
`/admin/api/modules/${encodeURIComponent(id)}`,
@@ -102,7 +102,7 @@ export default function AdminModules() {
return (
<div class="page-content">
<h2 class="page-title">Gestion des Modules</h2>
<h2 class="page-title">Gestion des ECUEs</h2>
{error && <p class="state-error">{error}</p>}
@@ -122,7 +122,7 @@ export default function AdminModules() {
}}
style="margin-left: auto"
>
+ Ajouter module
+ Ajouter ECUE
</button>
</div>
@@ -134,7 +134,7 @@ export default function AdminModules() {
<thead>
<tr>
<th>id (code)</th>
<th>Nom du module</th>
<th>Nom de l'ECUE</th>
<th>Enseignants assignes</th>
<th>Actions</th>
</tr>
@@ -144,7 +144,7 @@ export default function AdminModules() {
? (
<tr>
<td colspan={4} class="state-empty">
Aucun module enregistré
Aucun ECUE enregistré
</td>
</tr>
)
@@ -218,13 +218,13 @@ export default function AdminModules() {
</div>
)}
{/* Nouveau module */}
{/* Nouvel ECUE */}
<div
id="new-module-section"
class="edit-section"
style="margin-top: 1.5rem"
>
<p class="edit-section-title">Nouveau module</p>
<p class="edit-section-title">Nouvel ECUE</p>
<div class="form-row">
<input
class="form-input"
@@ -235,7 +235,7 @@ export default function AdminModules() {
/>
<input
class="form-input"
placeholder="Nom du module"
placeholder="Nom de l'ECUE"
value={newNom}
onInput={(e) => setNewNom((e.target as HTMLInputElement).value)}
/>
+9 -9
View File
@@ -104,7 +104,7 @@ export default function AdminUEs() {
idUE: number,
idPromo: string,
) {
if (!confirm("Supprimer ce module de la UE ?")) return;
if (!confirm("Supprimer cet ECUE de la UE ?")) return;
try {
const res = await fetch(
`/admin/api/ue-modules/${encodeURIComponent(idModule)}/${idUE}/${
@@ -121,7 +121,7 @@ export default function AdminUEs() {
async function addUeModule() {
if (!selectedUe || !addModuleId || !addPromoId) {
setAddError("Module et Promo sont requis");
setAddError("ECUE et Promo sont requis");
return;
}
const coeff = parseFloat(addCoeff);
@@ -203,7 +203,7 @@ export default function AdminUEs() {
class="col-dim"
style="font-size: 0.78rem; margin: -0.5rem 0 1rem"
>
UE = Unité d'Enseignement regroupant plusieurs modules
UE = Unité d'Enseignement regroupant plusieurs ECUEs
</p>
{error && <p class="state-error">{error}</p>}
@@ -314,13 +314,13 @@ export default function AdminUEs() {
<div class="panel-box">
<p class="panel-box-title">{selectedUe.nom}</p>
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
Modules assignés (UE_Module)
ECUEs assignés (UE_Module)
</p>
<div class="data-table-wrap" style="margin-bottom: 1rem">
<table class="data-table">
<thead>
<tr>
<th>Module</th>
<th>ECUE</th>
<th>Promo</th>
<th>Coeff</th>
<th>Actions</th>
@@ -331,7 +331,7 @@ export default function AdminUEs() {
? (
<tr>
<td colspan={4} class="state-empty">
Aucun module assigné
Aucun ECUE assigné
</td>
</tr>
)
@@ -441,7 +441,7 @@ export default function AdminUEs() {
</div>
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
Ajouter un module à cette UE
Ajouter un ECUE à cette UE
</p>
{addError && (
<p class="state-error" style="padding: 0.3rem 0.5rem">
@@ -458,7 +458,7 @@ export default function AdminUEs() {
)}
style="min-width: 12rem"
>
<option value="">Module </option>
<option value="">ECUE </option>
{modules.map((m) => (
<option key={m.id} value={m.id}>
{m.id} {m.nom}
@@ -504,7 +504,7 @@ export default function AdminUEs() {
: (
<div class="panel-box">
<p class="state-empty" style="padding: 2rem 0">
Sélectionnez une UE pour voir ses modules
Sélectionnez une UE pour voir ses ECUEs
</p>
</div>
)}
@@ -33,7 +33,7 @@ export default function EditModule({ moduleId }: Props) {
fetch("/admin/api/users"),
fetch("/students/api/promotions"),
]);
if (!mRes.ok) throw new Error("Module introuvable");
if (!mRes.ok) throw new Error("ECUE introuvable");
const m: Module = await mRes.json();
setMod(m);
setNom(m.nom);
@@ -70,7 +70,7 @@ export default function EditModule({ moduleId }: Props) {
if (!res.ok) throw new Error("Modification échouée");
const updated: Module = await res.json();
setMod(updated);
setSaveMsg("Module enregistré.");
setSaveMsg("ECUE enregistré.");
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
@@ -79,7 +79,7 @@ export default function EditModule({ moduleId }: Props) {
}
async function deleteModule() {
if (!confirm(`Supprimer définitivement le module ${moduleId} ?`)) return;
if (!confirm(`Supprimer définitivement l'ECUE ${moduleId} ?`)) return;
try {
const res = await fetch(
`/admin/api/modules/${encodeURIComponent(moduleId)}`,
@@ -173,7 +173,7 @@ export default function EditModule({ moduleId }: Props) {
class="page-title"
style="border-bottom: none; margin-bottom: 0.5rem"
>
Module -- {mod.id}
ECUE -- {mod.id}
</h2>
<div class="info-bar">
@@ -202,7 +202,7 @@ export default function EditModule({ moduleId }: Props) {
/>
</div>
<div class="form-field">
<label>Nom du module</label>
<label>Nom de l'ECUE</label>
<input
class="form-input"
value={nom}
@@ -224,7 +224,7 @@ export default function EditModule({ moduleId }: Props) {
class="btn btn-danger"
onClick={deleteModule}
>
Supprimer le module
Supprimer l'ECUE
</button>
</div>
</div>
@@ -281,7 +281,7 @@ export default function ImportMaquette() {
globalThis.open("/templates/modele_maquette.xlsx", "_blank");
}
function downloadExport() {
function _downloadExport() {
Promise.all([
fetch("/admin/api/ues").then((r) => r.json()),
fetch("/admin/api/ue-modules").then((r) => r.json()),
@@ -67,7 +67,9 @@ function validatedWeeks(mobs: Mobilite[]): number {
.reduce((sum, m) => sum + m.duree, 0);
}
export default function MobilityOverview() {
export default function MobilityOverview(
{ initialNumEtud }: { initialNumEtud?: number } = {},
) {
const [students, setStudents] = useState<Student[]>([]);
const [promos, setPromos] = useState<Promotion[]>([]);
const [mobilites, setMobilites] = useState<Mobilite[]>([]);
@@ -105,6 +107,12 @@ export default function MobilityOverview() {
setStagesMap(
Object.fromEntries((stData as Stage[]).map((s) => [s.id, s])),
);
if (initialNumEtud) {
const s = (sData as Student[]).find((s) =>
s.numEtud === initialNumEtud
);
if (s) setDetailStudent(s);
}
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
@@ -116,6 +124,18 @@ export default function MobilityOverview() {
load();
}, []);
function openStudent(s: Student) {
setDetailStudent(s);
history.pushState(null, "", `/mobility/overview/${s.numEtud}`);
}
function closeStudent() {
setDetailStudent(null);
setEditingMob(null);
setShowAddForm(false);
history.pushState(null, "", "/mobility/overview");
}
// If in detail view, render that
if (detailStudent) {
return (
@@ -128,11 +148,7 @@ export default function MobilityOverview() {
setEditingMob={setEditingMob}
showAddForm={showAddForm}
setShowAddForm={setShowAddForm}
onBack={() => {
setDetailStudent(null);
setEditingMob(null);
setShowAddForm(false);
}}
onBack={closeStudent}
onReload={load}
/>
);
@@ -207,14 +223,14 @@ export default function MobilityOverview() {
<ListView
students={filtered}
mobsByStudent={mobsByStudent}
onConsult={(s) => setDetailStudent(s)}
onConsult={(s) => openStudent(s)}
/>
)
: (
<KanbanView
students={filtered}
mobsByStudent={mobsByStudent}
onConsult={(s) => setDetailStudent(s)}
onConsult={(s) => openStudent(s)}
/>
)}
</div>
@@ -637,6 +653,9 @@ function DetailView(
numEtud={student.numEtud}
ecoles={ecoles}
paysList={paysList}
availableStages={Object.values(stagesMap)
.filter((s) => s.numEtud === student.numEtud)
.filter((s) => !mobilites.some((m) => m.idStage === s.id))}
onCancel={() => setShowAddForm(false)}
onSave={async () => {
setShowAddForm(false);
@@ -774,10 +793,11 @@ function MobEditForm(
}
function MobAddForm(
{ numEtud, ecoles, paysList, onCancel, onSave }: {
{ numEtud, ecoles, paysList, availableStages, onCancel, onSave }: {
numEtud: number;
ecoles: string[];
paysList: string[];
availableStages: Stage[];
onCancel: () => void;
onSave: () => Promise<void>;
},
@@ -786,8 +806,19 @@ function MobAddForm(
const [ecole, setEcole] = useState("");
const [pays, setPays] = useState("");
const [status, setStatus] = useState("contracts_received");
const [selectedStageId, setSelectedStageId] = useState("");
const [busy, setBusy] = useState(false);
const isStageLinked = selectedStageId !== "";
function onStageChange(value: string) {
setSelectedStageId(value);
if (value) {
const stage = availableStages.find((s) => s.id === Number(value));
if (stage) setDuree(String(stage.duree));
}
}
async function submit() {
setBusy(true);
try {
@@ -797,9 +828,10 @@ function MobAddForm(
body: JSON.stringify({
numEtud,
duree: parseInt(duree),
ecole: ecole || null,
pays: pays || null,
status,
ecole: isStageLinked ? null : (ecole || null),
pays: isStageLinked ? null : (pays || null),
status: isStageLinked ? "validated" : status,
idStage: isStageLinked ? Number(selectedStageId) : null,
}),
});
if (!res.ok) throw new Error("Erreur");
@@ -815,6 +847,24 @@ function MobAddForm(
<div class="edit-section" style={{ marginBottom: "1rem" }}>
<p class="edit-section-title">Nouvelle mobilité</p>
<div class="form-grid">
{availableStages.length > 0 && (
<div class="form-field">
<label>Lier à un stage</label>
<select
class="filter-select"
value={selectedStageId}
onChange={(e) =>
onStageChange((e.target as HTMLSelectElement).value)}
>
<option value=""> Mobilité d'étude —</option>
{availableStages.map((s) => (
<option key={s.id} value={String(s.id)}>
{s.nomEntreprise} ({s.duree} sem.)
</option>
))}
</select>
</div>
)}
<div class="form-field">
<label>Durée (semaines)</label>
<input
@@ -825,6 +875,8 @@ function MobAddForm(
onInput={(e) => setDuree((e.target as HTMLInputElement).value)}
/>
</div>
{!isStageLinked && (
<>
<div class="form-field">
<label>École</label>
<input
@@ -854,14 +906,28 @@ function MobAddForm(
<select
class="filter-select"
value={status}
onChange={(e) => setStatus((e.target as HTMLSelectElement).value)}
onChange={(e) =>
setStatus((e.target as HTMLSelectElement).value)}
>
{STATUS_ORDER.map((s) => (
<option key={s} value={s}>{STATUS_LABELS[s]}</option>
))}
</select>
</div>
</>
)}
</div>
{isStageLinked && (
<p
style={{
fontSize: "0.8rem",
opacity: 0.7,
margin: "0.4rem 0",
}}
>
Mobilité liée à un stage — status automatiquement « Validé »
</p>
)}
<div style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}>
<button
type="button"
@@ -0,0 +1,20 @@
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
import MobilityOverview from "../../(_islands)/MobilityOverview.tsx";
// deno-lint-ignore require-await
async function Overview(
_request: Request,
context: FreshContext<State>,
) {
const numEtud = Number(context.params.numEtud);
return <MobilityOverview initialNumEtud={numEtud} />;
}
export { Overview as Page };
export const config = getPartialsConfig();
export default makePartials(Overview);
@@ -268,7 +268,7 @@ export default function ImportNotes() {
globalThis.open("/templates/modele_notes.xlsx", "_blank");
}
function downloadExport() {
function _downloadExport() {
// Export notes from the API in the same format
Promise.all([
fetch("/students/api/students").then((r) => r.json()),
@@ -17,7 +17,9 @@ type Stage = {
const REQUIRED_WEEKS = 40;
export default function StagesOverview() {
export default function StagesOverview(
{ initialNumEtud }: { initialNumEtud?: number } = {},
) {
const [students, setStudents] = useState<Student[]>([]);
const [promos, setPromos] = useState<Promotion[]>([]);
const [stagesList, setStagesList] = useState<Stage[]>([]);
@@ -48,6 +50,12 @@ export default function StagesOverview() {
setStudents(sData);
setPromos(pData);
setStagesList(stData);
if (initialNumEtud) {
const found = (sData as Student[]).find((s) =>
s.numEtud === initialNumEtud
);
if (found) setDetailStudent(found);
}
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
@@ -59,6 +67,18 @@ export default function StagesOverview() {
load();
}, []);
function openStudent(s: Student) {
setDetailStudent(s);
history.pushState(null, "", `/stages/overview/${s.numEtud}`);
}
function closeStudent() {
setDetailStudent(null);
setEditingStage(null);
setShowAddForm(false);
history.pushState(null, "", "/stages/overview");
}
if (detailStudent) {
return (
<DetailView
@@ -69,11 +89,7 @@ export default function StagesOverview() {
setEditingStage={setEditingStage}
showAddForm={showAddForm}
setShowAddForm={setShowAddForm}
onBack={() => {
setDetailStudent(null);
setEditingStage(null);
setShowAddForm(false);
}}
onBack={closeStudent}
onReload={load}
/>
);
@@ -129,7 +145,7 @@ export default function StagesOverview() {
<ListView
students={filtered}
stagesByStudent={stagesByStudent}
onConsult={(s) => setDetailStudent(s)}
onConsult={(s) => openStudent(s)}
/>
</div>
);
@@ -0,0 +1,20 @@
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
import StagesOverview from "../../(_islands)/StagesOverview.tsx";
// deno-lint-ignore require-await
async function Overview(
_request: Request,
context: FreshContext<State>,
) {
const numEtud = Number(context.params.numEtud);
return <StagesOverview initialNumEtud={numEtud} />;
}
export { Overview as Page };
export const config = getPartialsConfig();
export default makePartials(Overview);
@@ -8,6 +8,8 @@ type Student = {
};
type Promo = { id: string; annee: string };
type Module = { id: string; nom: string };
type Mobilite = { id: number; duree: number; status: string };
type Stage = { id: number; duree: number };
type Props = { numEtud: number };
@@ -25,6 +27,8 @@ export default function EditStudents({ numEtud }: Props) {
const [student, setStudent] = useState<Student | null>(null);
const [promos, setPromos] = useState<Promo[]>([]);
const [_modules, setModules] = useState<Module[]>([]);
const [mobWeeks, setMobWeeks] = useState(0);
const [stageWeeks, setStageWeeks] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [saveMsg, setSaveMsg] = useState<string | null>(null);
@@ -38,10 +42,12 @@ export default function EditStudents({ numEtud }: Props) {
useEffect(() => {
async function load() {
try {
const [sRes, pRes, mRes] = await Promise.all([
const [sRes, pRes, mRes, mobRes, stRes] = await Promise.all([
fetch(`/students/api/students/${numEtud}`),
fetch("/students/api/promotions"),
fetch("/admin/api/modules"),
fetch("/notes/api/modules"),
fetch(`/mobility/api/mobilites?numEtud=${numEtud}`),
fetch(`/stages/api/stages?numEtud=${numEtud}`),
]);
if (!sRes.ok) throw new Error("Élève introuvable");
const s: Student = await sRes.json();
@@ -51,6 +57,19 @@ export default function EditStudents({ numEtud }: Props) {
setIdPromo(s.idPromo);
if (pRes.ok) setPromos(await pRes.json());
if (mRes.ok) setModules(await mRes.json());
if (mobRes.ok) {
const mobs: Mobilite[] = await mobRes.json();
setMobWeeks(
mobs.filter((m) => m.status === "validated").reduce(
(s, m) => s + m.duree,
0,
),
);
}
if (stRes.ok) {
const stages: Stage[] = await stRes.json();
setStageWeeks(stages.reduce((s, st) => s + st.duree, 0));
}
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
@@ -207,30 +226,69 @@ export default function EditStudents({ numEtud }: Props) {
</div>
</div>
{/* Section 2: Spécialisations */}
{/* Section 2: Notes */}
<div class="edit-section">
<p class="edit-section-title">Spécialisations</p>
<p
class="state-empty"
style="padding: 1rem 0; text-align: left"
>
Fonctionnalité non disponible (endpoint non implémenté).
</p>
</div>
{/* Section 3: Notes lecture seule */}
<div class="edit-section">
<p class="edit-section-title">Notes (lecture seule)</p>
<p class="edit-section-title">Notes</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span class="col-dim" style="font-size: 0.82rem">
Voir le récap complet des notes et moyennes de cet étudiant
Récap complet des notes et moyennes
</span>
<a
class="btn btn-secondary"
href={`/notes/recap/${numEtud}`}
f-client-nav={false}
>
Récap notes
Voir les notes
</a>
</div>
</div>
{/* Section 3: Mobilités */}
<div class="edit-section">
<p class="edit-section-title">Mobilités</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
<span
style={{
fontFamily: "monospace",
fontWeight: "var(--font-weight-bold)",
color: mobWeeks >= 12 ? "#22c55e" : "#dc2626",
}}
>
{mobWeeks}/12 semaines validées
</span>
</span>
<a
class="btn btn-secondary"
href={`/mobility/overview/${numEtud}`}
f-client-nav={false}
>
Consulter
</a>
</div>
</div>
{/* Section 4: Stages */}
<div class="edit-section">
<p class="edit-section-title">Stages</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
<span
style={{
fontFamily: "monospace",
fontWeight: "var(--font-weight-bold)",
color: stageWeeks >= 40 ? "#22c55e" : "#dc2626",
}}
>
{stageWeeks}/40 semaines
</span>
</span>
<a
class="btn btn-secondary"
href={`/stages/overview/${numEtud}`}
f-client-nav={false}
>
Consulter
</a>
</div>
</div>
+10 -2
View File
@@ -40,6 +40,12 @@
font-size: 0.8rem;
font-family: inherit;
min-width: 8rem;
box-sizing: border-box;
}
.form-field .filter-select {
width: 100%;
min-width: 0;
}
.filter-input:focus,
@@ -368,7 +374,9 @@
color: light-dark(var(--light-foreground), var(--dark-foreground));
font-size: 0.82rem;
font-family: inherit;
min-width: 12rem;
min-width: 0;
width: 100%;
box-sizing: border-box;
}
.form-input:focus {
@@ -799,7 +807,7 @@
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
gap: 0.75rem 1rem;
margin-bottom: 0.75rem;
}
+8 -8
View File
@@ -1,15 +1,15 @@
(function () {
var t = localStorage.getItem("theme");
const t = localStorage.getItem("theme");
if (t) document.documentElement.style.colorScheme = t;
document.addEventListener("click", function (e) {
var btn = e.target.closest("#theme-toggle");
const btn = e.target.closest("#theme-toggle");
if (!btn) return;
var cs = getComputedStyle(document.documentElement).colorScheme;
var isDark = cs === "dark" ||
const cs = getComputedStyle(document.documentElement).colorScheme;
const isDark = cs === "dark" ||
(!cs || cs === "light dark") &&
matchMedia("(prefers-color-scheme:dark)").matches;
var next = isDark ? "light" : "dark";
const next = isDark ? "light" : "dark";
document.documentElement.style.colorScheme = next;
localStorage.setItem("theme", next);
btn.querySelector("span").textContent = next === "dark"
@@ -18,10 +18,10 @@
});
document.addEventListener("DOMContentLoaded", function () {
var btn = document.getElementById("theme-toggle");
const btn = document.getElementById("theme-toggle");
if (!btn) return;
var cs = getComputedStyle(document.documentElement).colorScheme;
var isDark = cs === "dark" ||
const cs = getComputedStyle(document.documentElement).colorScheme;
const isDark = cs === "dark" ||
(!cs || cs === "light dark") &&
matchMedia("(prefers-color-scheme:dark)").matches;
btn.querySelector("span").textContent = isDark ? "light_mode" : "dark_mode";
+2 -2
View File
@@ -34,7 +34,7 @@ Deno.test({
});
Deno.test({
name: "e2e modules: GET /modules returns empty for non-employee",
name: "e2e modules: GET /modules returns all for non-employee",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
@@ -44,7 +44,7 @@ Deno.test({
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
assertEquals(body.length, 1);
},
sanitizeResources: false,
sanitizeOps: false,