Commit a7ae5dd7 by PLN (Algolia)

tsa: Clarté TSA app — quiz flow, results, /tsa + /asd

Same grammar as Clarté TDAH (clarte kit: ProgressHeader, QuestionCard,
OptionPills, useDigitKeys), linear 25-item flow: consent-gated home with
optional gender, intro, auto-advancing questions, recap with jump,
results with total band + percentiles vs autistic and non-autistic
norms, five interpretation caveats, copyable consult sentence, jspdf
report. FR at /tsa, EN at /asd, mirroring /tdah + /adhd.
parent c8548e01
import ClarteTsa from '@/components/tsa/ClarteTsa';
export const metadata = {
title: 'Clarté TSA: Autistic Camouflaging Self-Assessment (CAT-Q)',
description: 'A structured self-assessment of autistic camouflaging in adults using the CAT-Q (Hull et al., 2019): 25 items, compensation / masking / assimilation subscales, percentiles against autistic and non-autistic norms. Free, private, 100% local.',
};
export default function Page() {
return <ClarteTsa lang="en" />;
}
import ClarteTsa from '@/components/tsa/ClarteTsa';
export const metadata = {
title: 'Clarté TSA : Auto-évaluation du camouflage autistique (CAT-Q)',
description: 'Auto-évaluation du camouflage autistique chez l’adulte avec le CAT-Q (Hull et al., 2019) : 25 items, sous-échelles compensation / masquage / assimilation, percentiles vs normes autistes et non-autistes. Gratuit, confidentiel, 100 % local.',
};
export default function Page() {
return <ClarteTsa lang="fr" />;
}
'use client';
/* Clarté TSA — résultats : rappel non-diagnostique, score total + bande
qualitative + percentiles appariés au genre (Hull 2020 via Φ normale),
trois sous-échelles, cinq points d'interprétation prudents (fiche
NovoPsych), phrase copiable, PDF. */
import { useMemo, useState } from 'react';
import { calculateResults } from './engine';
import { SUBSCALES } from './data';
import { buildReport } from './pdf';
export default function ResultsView({ t, styles, responses, gender, lang, onHome, onReview, onRestart }) {
const r = useMemo(() => calculateResults(responses, gender), [responses, gender]);
const [copied, setCopied] = useState(false);
const fill = (s) => (s || '')
.replace('{score}', String(r.total.score))
.replace('{band}', t['results.band.' + r.total.band])
.replace('{pNA}', String(r.total.pNonAutistic))
.replace('{pA}', String(r.total.pAutistic))
.replace('{genre}', t['results.gender.' + (gender || 'unspecified')]);
const summaryText = fill(t['results.summary.text']);
const consultSentence = t['results.consult.sentence']
.replace('{score}', String(r.total.score))
.replace('{band}', t['results.band.' + r.total.band])
.replace('{comp}', String(r.subscales.compensation.score)).replace('{compMax}', String(r.subscales.compensation.max))
.replace('{mask}', String(r.subscales.masking.score)).replace('{maskMax}', String(r.subscales.masking.max))
.replace('{assim}', String(r.subscales.assimilation.score)).replace('{assimMax}', String(r.subscales.assimilation.max));
const copySentence = () => {
const done = () => { setCopied(true); setTimeout(() => setCopied(false), 2000); };
try {
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(consultSentence).then(done, () => {}); return; }
} catch (e) {}
try {
const inp = document.createElement('textarea');
inp.value = consultSentence; document.body.appendChild(inp); inp.select();
if (document.execCommand('copy')) done();
document.body.removeChild(inp);
} catch (e) {}
};
return (
<div className={styles.root}>
<p className={styles.warningInline}><strong>{t['results.warning.p']}</strong></p>
<h2>{t['results.summary.h2']}</h2>
<p className={styles.scoreBig}>
{r.total.score}<small> / 175 · {t['results.band.' + r.total.band]}</small>
</p>
<p className={styles.summaryText}>{summaryText}</p>
<h2>{t['results.sub.h2']}</h2>
{SUBSCALES.map((sub) => {
const s = r.subscales[sub.key];
const line = t['results.sub.line']
.replace('{name}', t[sub.nameKey])
.replace('{score}', String(s.score)).replace('{max}', String(s.max))
.replace('{pNA}', String(s.pNonAutistic)).replace('{pA}', String(s.pAutistic))
.replace('{band}', t['results.band.' + descriptorKey(s)]);
return (
<div key={sub.key} className={styles.card}>
<h4>{t[sub.nameKey]}</h4>
<p>{t[sub.descKey]}</p>
<p className={styles.subLine}>{line}</p>
</div>
);
})}
<h2>{t['results.percentile.h2']}</h2>
<p>{t['results.percentile.p']}</p>
<h2>{t['results.caveats.h2']}</h2>
<ul>
<li>{t['results.caveat.1']}</li>
<li>{t['results.caveat.2']}</li>
<li>{t['results.caveat.3']}</li>
<li>{t['results.caveat.4']}</li>
<li>{t['results.caveat.5']}</li>
</ul>
<h2>{t['results.consult.title']}</h2>
<p><strong>{t['results.consult.sentence.label']}</strong></p>
<div className={styles.consultRow}>
<p>{consultSentence}</p>
<button className={styles.btn} onClick={copySentence}>{copied ? '✓' : t['recap.share.copy']}</button>
</div>
<div className={styles.nav}>
<button className={styles.btn} onClick={onHome}>{t['recap.backHome']}</button>
<button className={styles.btn} onClick={onReview}>{t['recap.review.fromresults']}</button>
<button className={styles.btn} onClick={() => buildReport(t, responses, { ...r, gender }, lang, summaryText)}>{t['results.download.btn']}</button>
<button className={styles.btn} onClick={onRestart}>{t['results.restart.btn']}</button>
</div>
<h2>{t['results.source.h2']}</h2>
<p className={styles.muted}>{t['results.source.p']}</p>
</div>
);
}
/* Bande qualitative d'une subscale : mêmes tranches de percentiles que le
total, appliquées au percentile non-autiste (la référence du descripteur). */
function descriptorKey(s) {
const p = s.pNonAutistic;
if (p <= 5) return 'extremely_low';
if (p <= 24) return 'low';
if (p <= 75) return 'moderate';
if (p <= 94) return 'high';
return 'extremely_high';
}
/* Clarté TSA — compte rendu PDF : même structure que le rapport TDAH
(titre + avertissement, synthèse, scores, annexe des 25 réponses,
références, pied de page non-diagnostique). */
import { CATQ_ITEMS } from './data';
export async function buildReport(t, responses, r, lang, summaryText) {
let JsPDF;
try { ({ jsPDF: JsPDF } = await import('jspdf')); }
catch (e) { console.error('jspdf unavailable', e); return; }
const doc = new JsPDF({ unit: 'pt', format: 'a4' });
const W = doc.internal.pageSize.getWidth();
const H = doc.internal.pageSize.getHeight();
let y = 50;
function ensure(h) {
if (y + h > H - 50) { footer(); doc.addPage(); y = 50; }
}
function footer() {
const i = doc.internal.getNumberOfPages();
doc.setFontSize(8); doc.setTextColor(130);
doc.text(t['pdf.pagefooter'].replace('{i}', String(i)), 50, H - 30);
}
function text(s, size = 10, bold = false, x = 50) {
doc.setFont('helvetica', bold ? 'bold' : 'normal');
doc.setFontSize(size);
const lines = doc.splitTextToSize(s, W - x - 50);
lines.forEach((line) => { ensure(14); doc.text(line, x, y); y += 14; });
}
text(t['pdf.title'], 16, true); y += 6;
text(t['pdf.warning.p'], 9); y += 10;
text(t['pdf.summary.h'], 13, true);
text(summaryText || '', 10); y += 10;
text(t['pdf.section1.h'], 12, true);
text(t['results.summary.text']
.replace('{score}', String(r.total.score))
.replace('{band}', t['results.band.' + r.total.band])
.replace('{pNA}', String(r.total.pNonAutistic))
.replace('{pA}', String(r.total.pAutistic))
.replace('{genre}', t['results.gender.' + (r.gender || 'unspecified')]), 10);
y += 8;
text(t['pdf.annexe.h'], 12, true);
CATQ_ITEMS.forEach((q) => {
const v = responses[q.id];
const answer = v === undefined ? t['pdf.unanswered'] : t['catq.option.' + v] || String(v);
text('• ' + t[q.t] + ' — ' + answer, 9);
});
text(t['pdf.refs.h'], 12, true);
text(t['pdf.refs.items'], 8);
footer();
doc.save(lang === 'en' ? 'catq-self-assessment-report.pdf' : 'compte-rendu-catq.pdf');
}
/* Clarté TSA — app shell. Même famille visuelle que TDAH (tdah.module.css) :
les tokens vivent globalement (styles/clarte-tokens.css). */
.root { max-width: 40rem; margin: 0 auto; padding: var(--sp-4) var(--sp-3) var(--sp-6); font-family: var(--font-clarte); background: var(--paper); color: var(--ink); line-height: 1.65; }
.root h1 { font-size: var(--fs-h1); line-height: 1.3; }
.root h1 .tagline { display: block; font-size: var(--fs-small); font-weight: 400; color: var(--ink-faint); }
.root h2 { font-size: 1.25rem; margin: var(--sp-4) 0 var(--sp-2); }
.root p { max-width: var(--measure-prose); }
.muted { color: var(--ink-faint); font-size: var(--fs-small); }
.cards { display: grid; gap: var(--sp-2); margin: var(--sp-3) 0; }
.card { background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius); padding: var(--sp-3); }
.card h4 { margin: 0 0 var(--sp-1); font-size: var(--fs-body); }
.card p { margin: 0; font-size: var(--fs-small); color: var(--ink); }
.warning { background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius); padding: var(--sp-3); margin: var(--sp-3) 0; }
.warning h4 { margin: 0 0 var(--sp-2); font-size: var(--fs-body); }
.warning ul { margin: 0; padding-left: 1.1rem; }
.warning li { margin: var(--sp-1) 0; font-size: var(--fs-small); }
.btn { border: 1px solid var(--line-strong); background: #fff; border-radius: var(--radius); padding: .6rem 1.1rem; font: inherit; color: var(--ink); cursor: pointer; }
.btn:hover { border-color: var(--teal); }
.btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.btn:disabled { background: var(--paper); color: var(--ink-faint); border-color: var(--line); cursor: default; }
.primaryBtn { background: var(--teal); border-color: var(--teal); color: #fff; font-weight: 700; }
.primaryBtn:hover { background: var(--teal-hover); }
.primaryBtn:disabled { background: var(--teal); color: #fff; opacity: 1; border-color: var(--line-strong); }
.linkbtn { background: none; border: none; color: var(--teal-text); text-decoration: underline; cursor: pointer; font: inherit; padding: 0; }
.linkbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.nav { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); margin-top: var(--sp-4); flex-wrap: wrap; }
.consent { margin: var(--sp-4) 0; }
.consent label { display: flex; gap: .6rem; align-items: baseline; font-size: var(--fs-small); }
.consent button { margin-top: var(--sp-3); }
.resume { margin: var(--sp-4) 0; padding: var(--sp-3); background: var(--teal-soft); border-radius: var(--radius); }
.quizCard h2:focus { outline: 2px solid var(--teal); outline-offset: 4px; border-radius: 4px; }
.note { margin-top: var(--sp-3); }
.note summary { color: var(--ink-faint); font-size: var(--fs-small); cursor: pointer; }
.note p { font-size: var(--fs-small); color: var(--ink-faint); }
.genderRow { margin: var(--sp-3) 0; }
.genderRow fieldset { border: none; margin: 0; padding: 0; }
.genderRow legend { font-size: var(--fs-small); margin-bottom: var(--sp-1); }
.genderRow label { display: flex; gap: .5rem; align-items: baseline; font-size: var(--fs-small); margin: var(--sp-1) 0; cursor: pointer; }
.interstitial { max-width: 34rem; margin: var(--sp-6) auto; }
.recapGroup h3 { font-size: var(--fs-body); margin: var(--sp-3) 0 var(--sp-1); color: var(--teal-text); }
.recapGroup ul { list-style: none; padding: 0; margin: 0; }
.recapRow { width: 100%; display: flex; justify-content: space-between; gap: var(--sp-2); background: none; border: none; border-bottom: 1px solid var(--line); padding: var(--sp-1) 0; font: inherit; text-align: left; cursor: pointer; }
.recapRow:hover { border-bottom-color: var(--teal); }
.recapText { font-size: var(--fs-small); }
.recapChip { font-size: var(--fs-caption); color: var(--teal-text); white-space: nowrap; }
.recapChipEmpty { font-size: var(--fs-caption); color: var(--ink-faint); white-space: nowrap; }
.summaryText { font-size: var(--fs-question); }
.warningInline strong { color: var(--danger); }
.scoreBig { font-size: 2.4rem; font-weight: 700; line-height: 1.2; margin: var(--sp-2) 0; }
.scoreBig small { font-size: var(--fs-body); font-weight: 400; color: var(--ink-faint); }
.subLine { border-bottom: 1px solid var(--line); padding: var(--sp-1) 0; font-size: var(--fs-small); }
.consultRow { display: flex; gap: var(--sp-2); align-items: baseline; flex-wrap: wrap; }
@media (max-width: 480px) {
.root { padding: var(--sp-3) var(--sp-2) var(--sp-5); }
}
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