Commit 9acf1bb8 by PLN (Algolia)

Finish Clarté persistence + offline, and the couple subsite engine

parent 8aac0fb3
...@@ -161,6 +161,49 @@ const state = { ...@@ -161,6 +161,49 @@ const state = {
}; };
// ============================================ // ============================================
// PERSISTENCE (localStorage shim)
// Survive reloads: answers and current section are saved locally,
// restored before first render. Silent no-op in private mode.
// ============================================
const STORAGE_KEY = 'clarte:v1';
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
responses: state.responses,
currentSection: state.currentSection
}));
} catch (e) { /* storage unavailable (private mode, quota): skip */ }
}
function clearSavedState() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) { /* storage unavailable: nothing to clear */ }
}
// Merge saved answers into state before first render, so question
// rendering (which reads state.responses) shows restored answers.
// Returns the saved section to resume at, or null.
function restoreState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const saved = JSON.parse(raw);
if (saved && typeof saved === 'object') {
if (saved.responses && typeof saved.responses === 'object') {
Object.assign(state.responses, saved.responses);
}
if (sections.includes(saved.currentSection)) {
return saved.currentSection;
}
}
} catch (e) { /* corrupt or unavailable storage: start fresh */ }
return null;
}
// ============================================
// NAVIGATION // NAVIGATION
// ============================================ // ============================================
...@@ -177,6 +220,7 @@ function showSection(sectionId) { ...@@ -177,6 +220,7 @@ function showSection(sectionId) {
state.currentSection = sectionId; state.currentSection = sectionId;
updateProgress(); updateProgress();
window.scrollTo(0, 0); window.scrollTo(0, 0);
saveState();
} }
} }
...@@ -336,6 +380,7 @@ function attachOptionHandlers(container) { ...@@ -336,6 +380,7 @@ function attachOptionHandlers(container) {
container.querySelectorAll('input[type="radio"]').forEach(input => { container.querySelectorAll('input[type="radio"]').forEach(input => {
input.addEventListener('change', (e) => { input.addEventListener('change', (e) => {
state.responses[e.target.name] = parseInt(e.target.value); state.responses[e.target.name] = parseInt(e.target.value);
saveState();
const question = e.target.closest('.question'); const question = e.target.closest('.question');
if (question) question.classList.add('answered'); if (question) question.classList.add('answered');
}); });
...@@ -789,6 +834,9 @@ function validateSection(section) { ...@@ -789,6 +834,9 @@ function validateSection(section) {
// ============================================ // ============================================
function init() { function init() {
// Restore saved answers BEFORE first render so radios render checked
const savedSection = restoreState();
// Render all questionnaires // Render all questionnaires
renderASRS(); renderASRS();
renderDIVA(); renderDIVA();
...@@ -833,6 +881,7 @@ function init() { ...@@ -833,6 +881,7 @@ function init() {
document.getElementById('restart').addEventListener('click', () => { document.getElementById('restart').addEventListener('click', () => {
state.responses = {}; state.responses = {};
state.results = null; state.results = null;
clearSavedState();
renderASRS(); renderASRS();
renderDIVA(); renderDIVA();
renderExecutive(); renderExecutive();
...@@ -841,6 +890,14 @@ function init() { ...@@ -841,6 +890,14 @@ function init() {
// Initialize // Initialize
showSection('home'); showSection('home');
// Resume where the user left off (after showSection('home') above)
if (savedSection && savedSection !== 'home') {
if (savedSection === 'results') {
renderResults(); // recomputes purely from state.responses
}
showSection(savedSection);
}
} }
// Start app when DOM is ready // Start app when DOM is ready
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives."> <meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives.">
<meta name="theme-color" content="#5B8A8A"> <meta name="theme-color" content="#5B8A8A">
<link rel="manifest" href="manifest.webmanifest">
<!-- Open Graph / Social --> <!-- Open Graph / Social -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
...@@ -247,5 +248,10 @@ ...@@ -247,5 +248,10 @@
</footer> </footer>
<script src="app.js"></script> <script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(function () {});
}
</script>
</body> </body>
</html> </html>
{
"name": "Clarté — Auto-Évaluation TDAH",
"short_name": "Clarté TDAH",
"lang": "fr",
"start_url": ".",
"display": "standalone",
"background_color": "#FAF8F5",
"theme_color": "#5B8A8A"
}
/* Cache-first: la page et ses données chargent hors réseau; les réponses
vivent dans localStorage et ne passent jamais par le réseau. */
var CACHE = 'clarte-v1';
var ASSETS = ['./', './index.html', './style.css', './app.js', './jspdf.min.js',
'./manifest.webmanifest',
'./fonts/AtkinsonHyperlegible-Regular.woff2',
'./fonts/AtkinsonHyperlegible-Bold.woff2',
'./fonts/AtkinsonHyperlegible-Italic.woff2'];
self.addEventListener('install', function (e) {
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; })
.map(function (k) { return caches.delete(k); }));
}));
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(caches.match(e.request, { ignoreSearch: true })
.then(function (hit) { return hit || fetch(e.request); }));
});
...@@ -3,12 +3,17 @@ ...@@ -3,12 +3,17 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bientôt — me.nech.pl/couple/</title> <meta name="robots" content="noindex, nofollow">
<style> <title>Clarté — Espace couple</title>
body { font-family: Georgia, serif; max-width: 30rem; margin: 20vh auto; padding: 0 1.5rem; line-height: 1.7; color: #3a3630; background: #faf8f4; } <meta name="description" content="Questionnaires d'auto-évaluation de couple, en privé, sur votre appareil.">
</style> <link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#faf8f4">
<link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<p>Une page arrive ici. Bientôt.</p> <div id="progress" hidden></div>
<main id="app" aria-live="polite"></main>
<script src="data.js"></script>
<script src="app.js"></script>
</body> </body>
</html> </html>
{
"name": "Clarté — Espace couple",
"short_name": "Clarté couple",
"lang": "fr",
"start_url": ".",
"display": "standalone",
"background_color": "#faf8f4",
"theme_color": "#faf8f4"
}
/* Clarté couple — paper-like, low cognitive load */
@font-face { font-family: 'Atkinson'; src: url('fonts/AtkinsonHyperlegible-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Atkinson'; src: url('fonts/AtkinsonHyperlegible-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'Atkinson'; src: url('fonts/AtkinsonHyperlegible-Italic.woff2') format('woff2'); font-style: italic; font-display: swap; }
:root {
--paper: #faf8f4;
--ink: #3a3630;
--ink-faint: #837d76;
--teal: #3e7d78;
--teal-soft: #e3efee;
--line: #e5e0d8;
--measure: 46ch;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; background: var(--paper); color: var(--ink);
font-family: 'Atkinson', Georgia, serif; font-size: 1.05rem; line-height: 1.65;
}
main { max-width: 40rem; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
header.progress {
position: sticky; top: 0; z-index: 2; background: var(--paper);
padding: .6rem 1.25rem; border-bottom: 1px solid var(--line);
font-size: .9rem; color: var(--ink-faint);
max-width: 40rem; margin: 0 auto;
}
header.progress .fill {
height: 3px; background: var(--line); border-radius: 2px; margin-top: .35rem; overflow: hidden;
}
header.progress .fill i { display: block; height: 100%; background: var(--teal); transition: width .25s ease; }
h1 { font-size: 1.5rem; line-height: 1.3; margin: 1.2rem 0 .6rem; }
h2 { font-size: 1.2rem; margin: 1.6rem 0 .4rem; }
p { margin: .6rem 0; max-width: var(--measure); }
.muted { color: var(--ink-faint); font-size: .92rem; }
small.source { display: block; margin-top: .8rem; color: var(--ink-faint); font-size: .8rem; }
button { font: inherit; cursor: pointer; }
.cardlist { list-style: none; padding: 0; margin: 1.2rem 0; }
.cardlist li { margin: 0 0 .7rem; }
.cardbtn {
width: 100%; text-align: left; background: #fff; border: 1px solid var(--line);
border-radius: 10px; padding: .85rem 1rem; display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
transition: border-color .15s ease;
}
.cardbtn:hover { border-color: var(--teal); }
.cardbtn .meta { color: var(--ink-faint); font-size: .85rem; white-space: nowrap; }
.cardbtn .state { color: var(--teal); font-size: .85rem; white-space: nowrap; }
.item-text { font-size: 1.18rem; line-height: 1.55; margin: 1.4rem 0 1.2rem; max-width: var(--measure); }
.scale { display: flex; flex-direction: column; gap: .5rem; margin: 1.2rem 0; max-width: var(--measure); }
.scale button {
text-align: left; padding: .75rem .9rem; border-radius: 9px;
border: 1px solid var(--line); background: #fff; transition: all .12s ease;
}
.scale button:hover { border-color: var(--teal); }
.scale button[aria-pressed="true"] { background: var(--teal-soft); border-color: var(--teal); font-weight: 700; }
.scale button.optout { border-style: dashed; color: var(--ink-faint); font-size: .92rem; padding: .5rem .9rem; }
.who { margin: 1.6rem 0 0; display: flex; align-items: baseline; gap: .45rem; }
.who input {
font: inherit; color: var(--ink); background: transparent;
border: none; border-bottom: 1px solid var(--line);
width: 12ch; padding: .1rem .15rem;
}
.who input:focus { outline: none; border-bottom-color: var(--teal); }
.nav { display: flex; justify-content: space-between; gap: 1rem; margin-top: 1.6rem; }
.nav button, .cta {
border: 1px solid var(--line); background: #fff; border-radius: 9px; padding: .6rem 1.1rem;
}
.cta { background: var(--teal); border-color: var(--teal); color: #fff; font-weight: 700; }
.cta:disabled { opacity: .45; cursor: default; }
.result-band {
background: var(--teal-soft); border-radius: 12px; padding: 1.1rem 1.2rem; margin: 1.2rem 0;
}
.result-band .score { font-size: 2rem; font-weight: 700; color: var(--teal); }
blockquote.orig {
border-left: 3px solid var(--line); margin: 1rem 0 0; padding: .2rem 0 .2rem 1rem;
color: var(--ink-faint); font-size: .92rem; font-style: italic;
}
.recap { width: 100%; border-collapse: collapse; font-size: .92rem; margin-top: 1rem; }
.recap td, .recap th { border-bottom: 1px solid var(--line); padding: .45rem .5rem .45rem 0; text-align: left; vertical-align: top; }
.recap td:last-child { padding-right: 0; }
details { margin-top: 1.4rem; }
summary { cursor: pointer; color: var(--ink-faint); }
footer.pagefoot { margin-top: 3rem; font-size: .85rem; color: var(--ink-faint); }
a { color: var(--teal); }
/* Cache-first: la page et ses données chargent hors réseau; les réponses
vivent dans localStorage et ne passent jamais par le réseau. */
var CACHE = 'couple-v3';
var ASSETS = ['./', './index.html', './style.css', './app.js', './data.js',
'./manifest.webmanifest',
'./fonts/AtkinsonHyperlegible-Regular.woff2',
'./fonts/AtkinsonHyperlegible-Bold.woff2',
'./fonts/AtkinsonHyperlegible-Italic.woff2'];
self.addEventListener('install', function (e) {
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; })
.map(function (k) { return caches.delete(k); }));
}));
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(caches.match(e.request, { ignoreSearch: true })
.then(function (hit) { return hit || fetch(e.request); }));
});
...@@ -161,6 +161,49 @@ const state = { ...@@ -161,6 +161,49 @@ const state = {
}; };
// ============================================ // ============================================
// PERSISTENCE (localStorage shim)
// Survive reloads: answers and current section are saved locally,
// restored before first render. Silent no-op in private mode.
// ============================================
const STORAGE_KEY = 'clarte:v1';
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
responses: state.responses,
currentSection: state.currentSection
}));
} catch (e) { /* storage unavailable (private mode, quota): skip */ }
}
function clearSavedState() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) { /* storage unavailable: nothing to clear */ }
}
// Merge saved answers into state before first render, so question
// rendering (which reads state.responses) shows restored answers.
// Returns the saved section to resume at, or null.
function restoreState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const saved = JSON.parse(raw);
if (saved && typeof saved === 'object') {
if (saved.responses && typeof saved.responses === 'object') {
Object.assign(state.responses, saved.responses);
}
if (sections.includes(saved.currentSection)) {
return saved.currentSection;
}
}
} catch (e) { /* corrupt or unavailable storage: start fresh */ }
return null;
}
// ============================================
// NAVIGATION // NAVIGATION
// ============================================ // ============================================
...@@ -177,6 +220,7 @@ function showSection(sectionId) { ...@@ -177,6 +220,7 @@ function showSection(sectionId) {
state.currentSection = sectionId; state.currentSection = sectionId;
updateProgress(); updateProgress();
window.scrollTo(0, 0); window.scrollTo(0, 0);
saveState();
} }
} }
...@@ -336,6 +380,7 @@ function attachOptionHandlers(container) { ...@@ -336,6 +380,7 @@ function attachOptionHandlers(container) {
container.querySelectorAll('input[type="radio"]').forEach(input => { container.querySelectorAll('input[type="radio"]').forEach(input => {
input.addEventListener('change', (e) => { input.addEventListener('change', (e) => {
state.responses[e.target.name] = parseInt(e.target.value); state.responses[e.target.name] = parseInt(e.target.value);
saveState();
const question = e.target.closest('.question'); const question = e.target.closest('.question');
if (question) question.classList.add('answered'); if (question) question.classList.add('answered');
}); });
...@@ -789,6 +834,9 @@ function validateSection(section) { ...@@ -789,6 +834,9 @@ function validateSection(section) {
// ============================================ // ============================================
function init() { function init() {
// Restore saved answers BEFORE first render so radios render checked
const savedSection = restoreState();
// Render all questionnaires // Render all questionnaires
renderASRS(); renderASRS();
renderDIVA(); renderDIVA();
...@@ -833,6 +881,7 @@ function init() { ...@@ -833,6 +881,7 @@ function init() {
document.getElementById('restart').addEventListener('click', () => { document.getElementById('restart').addEventListener('click', () => {
state.responses = {}; state.responses = {};
state.results = null; state.results = null;
clearSavedState();
renderASRS(); renderASRS();
renderDIVA(); renderDIVA();
renderExecutive(); renderExecutive();
...@@ -841,6 +890,14 @@ function init() { ...@@ -841,6 +890,14 @@ function init() {
// Initialize // Initialize
showSection('home'); showSection('home');
// Resume where the user left off (after showSection('home') above)
if (savedSection && savedSection !== 'home') {
if (savedSection === 'results') {
renderResults(); // recomputes purely from state.responses
}
showSection(savedSection);
}
} }
// Start app when DOM is ready // Start app when DOM is ready
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives."> <meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives.">
<meta name="theme-color" content="#5B8A8A"> <meta name="theme-color" content="#5B8A8A">
<link rel="manifest" href="manifest.webmanifest">
<!-- Open Graph / Social --> <!-- Open Graph / Social -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
...@@ -247,5 +248,10 @@ ...@@ -247,5 +248,10 @@
</footer> </footer>
<script src="app.js"></script> <script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(function () {});
}
</script>
</body> </body>
</html> </html>
{
"name": "Clarté — Auto-Évaluation TDAH",
"short_name": "Clarté TDAH",
"lang": "fr",
"start_url": ".",
"display": "standalone",
"background_color": "#FAF8F5",
"theme_color": "#5B8A8A"
}
/* Cache-first: la page et ses données chargent hors réseau; les réponses
vivent dans localStorage et ne passent jamais par le réseau. */
var CACHE = 'clarte-v1';
var ASSETS = ['./', './index.html', './style.css', './app.js', './jspdf.min.js',
'./manifest.webmanifest',
'./fonts/AtkinsonHyperlegible-Regular.woff2',
'./fonts/AtkinsonHyperlegible-Bold.woff2',
'./fonts/AtkinsonHyperlegible-Italic.woff2'];
self.addEventListener('install', function (e) {
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; })
.map(function (k) { return caches.delete(k); }));
}));
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(caches.match(e.request, { ignoreSearch: true })
.then(function (hit) { return hit || fetch(e.request); }));
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment