Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ let scoreDidnt = 0;
let searchTerm = '';
let failedCards = new Set(); // keys of cards marked "didn't know"
let reviewFailedOnly = false; // when true, only show failed cards
const POSITION_KEY = 'admingo-position'; // last viewed card index (resume on reload)
const konamiSequence = [
'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight',
Expand Down Expand Up @@ -81,9 +82,14 @@ async function loadCSV() {
const cards = [];

parsed.data.forEach((row) => {
const examen = (row['Examen'] || '').trim();
// Missing/blank "Examen" falls back to 'all' so a mislabeled row is
// never silently dropped: such cards stay visible under every exam filter.
const examen = (row['Examen'] || '').trim() || 'all';
const domaine = (row['Domaine'] || '').trim();
const sujet = (row['Sujet'] || '').trim();
// Optional "Difficulté" column (facile / moyen / difficile). Absent in
// the current CSV — left empty, no badge is shown.
const difficulte = (row['Difficulté'] || '').trim().toLowerCase();

// Generate up to 10 cards per row
for (let i = 1; i <= 10; i++) {
Expand All @@ -93,7 +99,7 @@ async function loadCSV() {

// Skip empty blocks
if (question && reponse) {
cards.push({ examen, domaine, sujet, question, reponse, explication });
cards.push({ examen, domaine, sujet, question, reponse, explication, difficulte });
}
}
});
Expand All @@ -108,6 +114,12 @@ async function loadCSV() {
// ============================================
// FISHER-YATES SHUFFLE
// ============================================
/**
* Return a new array with the elements of `array` randomly shuffled.
* Uses the Fisher-Yates algorithm; the input array is not mutated.
* @param {Array} array - source array
* @returns {Array} a shuffled copy
*/
function shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
Expand All @@ -120,6 +132,12 @@ function shuffleArray(array) {
// ============================================
// TEXT NORMALIZATION (accent-insensitive search)
// ============================================
/**
* Lower-case `str` and strip diacritics (é → e) so searches are
* accent-insensitive.
* @param {string} str
* @returns {string} normalized string
*/
function normalizeText(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
Expand Down Expand Up @@ -206,7 +224,7 @@ const EXAM_STORAGE_KEY = 'admingo-exam';
function getExamCards() {
return activeExam === 'all'
? allCards
: allCards.filter((c) => c.examen === activeExam);
: allCards.filter((c) => c.examen === activeExam || c.examen === 'all');
}

/**
Expand Down Expand Up @@ -252,7 +270,7 @@ function initExamFilter() {
*/
function applyFilters() {
let cards = allCards.filter(
(c) => (activeExam === 'all' || c.examen === activeExam) && activeDomains.has(c.domaine)
(c) => (activeExam === 'all' || c.examen === activeExam || c.examen === 'all') && activeDomains.has(c.domaine)
);

if (searchTerm) {
Expand All @@ -277,6 +295,12 @@ function applyFilters() {
// ============================================
// CARD RENDERING
// ============================================
/**
* Render the card at `currentIndex` into the DOM: front (domain, subject,
* question), back (answer, explanation), progress bar and counter. Persists
* the current position and fires confetti at 100%. Shows an empty state when
* no card matches the active filters.
*/
function renderCard() {
if (filteredCards.length === 0) {
domainBadge.textContent = '—';
Expand Down Expand Up @@ -315,6 +339,20 @@ function renderCard() {
}
}

// Optional difficulty badge (shown only when the CSV provides the column)
const diffBadge = document.getElementById('difficulty-badge');
if (diffBadge) {
const labels = { facile: 'Facile', moyen: 'Moyen', difficile: 'Difficile' };
const label = labels[card.difficulte];
if (label) {
diffBadge.textContent = label;
diffBadge.setAttribute('data-level', card.difficulte);
diffBadge.style.display = 'inline-block';
} else {
diffBadge.style.display = 'none';
}
}

// Back
highlightInto(reponseText, card.reponse, searchTerm);
explicationText.textContent = card.explication;
Expand All @@ -337,6 +375,9 @@ function renderCard() {
cardCounter.textContent = counterText;
}

// Remember the current position so a page reload resumes the same spot
localStorage.setItem(POSITION_KEY, currentIndex);

// Progress
const pct = Math.round(((currentIndex + 1) / filteredCards.length) * 100);
progressBar.style.width = `${pct}%`;
Expand All @@ -355,6 +396,10 @@ function renderCard() {
// ============================================
// NAVIGATION
// ============================================
/**
* Toggle the card between its question (front) and answer (back) faces,
* showing the scoring panel on the back and syncing ARIA visibility.
*/
function flipCard() {
if (filteredCards.length === 0) return;
isFlipped = !isFlipped;
Expand All @@ -374,6 +419,7 @@ function flipCard() {
if (back) back.setAttribute('aria-hidden', isFlipped ? 'false' : 'true');
}

/** Advance to the next card, wrapping around to the first. */
function nextCard() {
if (filteredCards.length === 0) return;
currentIndex = (currentIndex + 1) % filteredCards.length;
Expand All @@ -382,6 +428,7 @@ function nextCard() {
slideCard('next');
}

/** Go back to the previous card, wrapping around to the last. */
function prevCard() {
if (filteredCards.length === 0) return;
currentIndex = (currentIndex - 1 + filteredCards.length) % filteredCards.length;
Expand Down Expand Up @@ -424,6 +471,8 @@ function saveScore() {
localStorage.setItem('admingo-score-didnt', scoreDidnt);
}

/** Mark the current card as known: bump the score, drop it from the
* review-failed set, then auto-advance. */
function markKnew() {
scoreKnew++;
saveScore();
Expand All @@ -438,6 +487,8 @@ function markKnew() {
nextCard();
}

/** Mark the current card as not known: bump the score, add it to the
* review-failed set, then auto-advance. */
function markDidnt() {
scoreDidnt++;
saveScore();
Expand Down Expand Up @@ -465,6 +516,13 @@ function updateScoreDisplay() {
if (headerEl && (scoreKnew > 0 || scoreDidnt > 0)) {
headerEl.style.display = 'flex';
}

// Session stats panel (reuses the persisted scores, read-only)
const total = scoreKnew + scoreDidnt;
const seenEl = document.getElementById('stats-seen');
const rateEl = document.getElementById('stats-rate');
if (seenEl) seenEl.textContent = total;
if (rateEl) rateEl.textContent = total > 0 ? Math.round((scoreKnew / total) * 100) + ' %' : '—';
}

function showScorePanel() {
Expand Down Expand Up @@ -742,6 +800,13 @@ async function init() {

// Initial shuffle & render (respect saved exam)
filteredCards = shuffleArray(getExamCards());

// Resume at the last viewed position (clamped to the current deck size)
const savedPos = parseInt(localStorage.getItem(POSITION_KEY) || '0', 10);
if (!isNaN(savedPos) && savedPos > 0 && savedPos < filteredCards.length) {
currentIndex = savedPos;
}

renderCard();

// Bind events
Expand Down
36 changes: 33 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,33 @@
<title>AdminGo — Révisions examen AIS</title>
<meta name="description"
content="AdminGo : application gratuite de révision par flashcards pour l'examen AIS - Administrateur d'Infrastructures Sécurisées. 500+ questions couvrant tous les domaines de la formation AIS." />
<link rel="canonical" href="https://admingo.tutotech.org/" />

<!-- Open Graph / Social sharing -->
<meta property="og:title" content="AdminGo — Révisions examen AIS" />
<meta property="og:description"
content="500+ flashcards interactives pour réviser l'examen AIS. Proposé gratuitement par l'association TutoTech." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://admingo.tutotech.org/" />
<meta property="og:image" content="https://admingo.tutotech.org/logo.png" />
<meta property="og:site_name" content="AdminGo" />
<meta property="og:locale" content="fr_FR" />

<!-- Twitter Cards -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="AdminGo — Révisions examen AIS" />
<meta name="twitter:description"
content="500+ flashcards interactives pour réviser l'examen AIS. Proposé gratuitement par l'association TutoTech." />
<meta name="twitter:image" content="https://admingo.tutotech.org/logo.png" />

<meta name="theme-color" content="#58CC02" />

<!-- Accélère la résolution/connexion des CDN au premier chargement -->
<link rel="preconnect" href="https://cdn.tailwindcss.com" />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.tailwindcss.com" />
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com" />

<!-- PWA -->
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="logo.png" />
Expand Down Expand Up @@ -65,7 +84,7 @@ <h1>🎓 AIS Flashcards</h1>
aria-label="Chargement en cours" style="background: rgb(var(--color-bg));">
<div class="text-center">
<div class="mb-6 animate-bounce" aria-hidden="true">
<img src="logo.png" alt="" class="w-16 h-16 mx-auto"
<img src="logo.png" alt="" width="64" height="64" decoding="async" class="w-16 h-16 mx-auto"
onerror="this.style.display='none';this.nextElementSibling.style.display='block';" />
<span class="text-6xl" style="display:none;">🎓</span>
</div>
Expand All @@ -89,7 +108,7 @@ <h1>🎓 AIS Flashcards</h1>
<!-- Logo -->
<div class="flex items-center gap-2 shrink-0">
<div class="w-10 h-10 flex items-center justify-center shrink-0">
<img src="logo.png" alt="AdminGo" class="w-10 h-10 rounded-lg object-contain"
<img src="logo.png" alt="AdminGo" width="40" height="40" decoding="async" class="w-10 h-10 rounded-lg object-contain"
onerror="this.style.display='none';this.nextElementSibling.style.display='block';" />
<span class="text-3xl" style="display:none;">🎓</span>
</div>
Expand Down Expand Up @@ -227,6 +246,16 @@ <h2 class="font-black text-sm uppercase tracking-wider" style="color: rgb(var(--
</button>
</div>

<!-- Session stats -->
<div class="mt-4 p-4 rounded-2xl text-xs"
style="background: rgb(var(--color-surface)); color: rgb(var(--color-text-secondary)); border: 1px solid rgb(var(--color-border));">
<p class="font-bold mb-2" style="color: rgb(var(--color-text));">📊 Statistiques</p>
<p class="flex justify-between"><span>Cartes évaluées</span> <strong id="stats-seen"
style="color: rgb(var(--color-text));">0</strong></p>
<p class="flex justify-between mt-1"><span>Taux de réussite</span> <strong id="stats-rate"
style="color: rgb(var(--color-primary));">—</strong></p>
</div>

<!-- Backup -->
<div class="mt-4 p-4 rounded-2xl text-xs"
style="background: rgb(var(--color-surface)); color: rgb(var(--color-text-secondary)); border: 1px solid rgb(var(--color-border));">
Expand Down Expand Up @@ -254,6 +283,7 @@ <h2 class="font-black text-sm uppercase tracking-wider" style="color: rgb(var(--
<!-- FRONT -->
<div class="card-face card-front" aria-hidden="false" id="card-front-face">
<span id="domain-badge" class="badge">Domaine</span>
<span id="difficulty-badge" class="difficulty-badge" style="display:none;"></span>
<p id="sujet-label" class="sujet-label">Sujet</p>
<p id="question-text" class="question-text" aria-live="polite">Question</p>
<p class="flip-hint" aria-hidden="true">👆 Clique pour retourner la carte</p>
Expand Down Expand Up @@ -394,7 +424,7 @@ <h2 id="reset-title" class="modal-title">🗑️ Réinitialiser ?</h2>
<div id="pwa-banner" class="pwa-banner" role="alert" style="display:none;">
<div class="pwa-banner-content">
<div class="pwa-banner-icon">
<img src="logo.png" alt="" class="w-10 h-10 rounded-lg"
<img src="logo.png" alt="" width="40" height="40" decoding="async" loading="lazy" class="w-10 h-10 rounded-lg"
onerror="this.style.display='none';this.nextElementSibling.style.display='block';" />
<span class="text-3xl" style="display:none;">🎓</span>
</div>
Expand Down
12 changes: 10 additions & 2 deletions particles.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,20 @@
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = motionQuery.matches;

// Motion is reduced when the OS asks for it OR when the user enabled the
// in-app "Réduire les animations" setting (html.reduce-motion).
/**
* Whether motion should be reduced: true when the OS requests it
* (prefers-reduced-motion) OR the in-app "Réduire les animations" setting
* is on (html.reduce-motion).
* @returns {boolean}
*/
function isMotionReduced() {
return prefersReducedMotion || document.documentElement.classList.contains('reduce-motion');
}

/**
* Start or stop the animation loop based on the current motion preference,
* clearing the canvas when motion is disabled.
*/
function applyMotionPreference() {
if (isMotionReduced()) {
cancelAnimationFrame(animationId);
Expand Down
4 changes: 4 additions & 0 deletions robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://admingo.tutotech.org/sitemap.xml
13 changes: 13 additions & 0 deletions sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://admingo.tutotech.org/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://admingo.tutotech.org/mentions-legales.html</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>
Loading
Loading