11'use client' ;
22
3+ import { QRCodeSVG } from 'qrcode.react' ;
34import { useEffect , useState } from 'react' ;
45import type { AccessDetailDTO } from '@/lib/types' ;
56import {
@@ -9,11 +10,53 @@ import {
910 ExternalLinkIcon ,
1011 PlusIcon ,
1112 PowerIcon ,
13+ QrIcon ,
1214 SparkIcon ,
1315 TrashIcon ,
16+ XIcon ,
1417} from './icons' ;
1518import { SupervisionDrawer } from './supervision-drawer' ;
1619
20+ /** Modale plein écran : QR code de l'accès + consigne « Scannez ». */
21+ function QrModal ( { url, code, onClose } : { url : string ; code ?: string ; onClose : ( ) => void } ) {
22+ useEffect ( ( ) => {
23+ const onKey = ( e : KeyboardEvent ) => {
24+ if ( e . key === 'Escape' ) onClose ( ) ;
25+ } ;
26+ document . addEventListener ( 'keydown' , onKey ) ;
27+ return ( ) => document . removeEventListener ( 'keydown' , onKey ) ;
28+ } , [ onClose ] ) ;
29+
30+ return (
31+ < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6" >
32+ < button type = "button" aria-label = "Fermer" onClick = { onClose } className = "absolute inset-0" />
33+ < div className = "relative z-10 w-full max-w-md animate-pop-in rounded-card bg-surface p-7 text-center shadow-pop" >
34+ < button
35+ type = "button"
36+ onClick = { onClose }
37+ aria-label = "Fermer"
38+ className = "absolute right-3 top-3 inline-flex size-8 items-center justify-center rounded-button text-muted-foreground transition-colors hover:bg-surface-muted hover:text-foreground"
39+ >
40+ < XIcon className = "size-5" />
41+ </ button >
42+ < h2 className = "font-display text-2xl font-bold tracking-tight" > Scannez pour rejoindre</ h2 >
43+ < p className = "mt-1 text-sm text-muted-foreground" > Avec l’appareil photo du téléphone.</ p >
44+ { /* Fond blanc requis pour la lisibilité du QR code (contraste). */ }
45+ < div className = "mx-auto mt-5 w-fit rounded-2xl bg-white p-4 shadow-card" >
46+ < QRCodeSVG value = { url } size = { 232 } marginSize = { 1 } className = "h-auto w-[min(60vw,232px)]" />
47+ </ div >
48+ { code ? (
49+ < p className = "mt-5 text-sm text-muted-foreground" >
50+ Puis saisissez le code :{ ' ' }
51+ < span className = "font-mono font-bold tracking-wider text-foreground" > { code } </ span >
52+ </ p >
53+ ) : null }
54+ < code className = "mt-3 block truncate text-xs text-muted-foreground" > { url } </ code >
55+ </ div >
56+ </ div >
57+ ) ;
58+ }
59+
1760function CopyLinkButton ( { url } : { url : string } ) {
1861 const [ copied , setCopied ] = useState ( false ) ;
1962 return (
@@ -55,12 +98,16 @@ export function AccessDetail({
5598 const [ origin , setOrigin ] = useState ( '' ) ;
5699 const [ newName , setNewName ] = useState ( '' ) ;
57100 const [ supervising , setSupervising ] = useState < { id : string ; name : string } | null > ( null ) ;
101+ const [ qrOpen , setQrOpen ] = useState ( false ) ;
102+ const [ hideCode , setHideCode ] = useState ( false ) ;
58103
59104 useEffect ( ( ) => {
60105 setOrigin ( window . location . origin ) ;
61106 } , [ ] ) ;
62107
63- const link = `${ origin } /e/${ access . token } ` ;
108+ // `hideCode` : on partage un lien sans le code (`/e`) ; l'élève saisit le code.
109+ const baseLink = `${ origin } /e` ;
110+ const link = hideCode ? baseLink : `${ origin } /e/${ access . token } ` ;
64111
65112 return (
66113 < div className = "animate-fade-in space-y-5" >
@@ -126,27 +173,66 @@ export function AccessDetail({
126173 </ section >
127174
128175 < section className = "rounded-card border border-border bg-surface p-5 shadow-card" >
129- < h3 className = "text-sm font-bold" > Lien d’accès élève</ h3 >
130- < p className = "mt-0.5 text-xs text-muted-foreground" >
131- Partagez ce lien avec vos élèves. Ils choisissent leur prénom et discutent avec
132- l’assistant < em > sous ce Cadre</ em > . (Données fictives — préfiguration d’un compte élève.)
133- </ p >
134- { access . active ? (
135- < div className = "mt-3 flex flex-wrap items-center gap-2" >
136- < code className = "min-w-0 flex-1 truncate rounded-button border border-border bg-surface-muted px-3 py-2 font-mono text-sm" >
137- { link }
138- </ code >
139- < CopyLinkButton url = { link } />
140- < a
141- href = { link }
142- target = "_blank"
143- rel = "noopener noreferrer"
144- className = "inline-flex items-center gap-1.5 rounded-button border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-surface-muted"
145- >
146- Ouvrir
147- < ExternalLinkIcon className = "size-3.5" />
148- </ a >
176+ < div className = "flex flex-wrap items-start justify-between gap-2" >
177+ < div className = "min-w-0" >
178+ < h3 className = "text-sm font-bold" > Lien d’accès élève</ h3 >
179+ < p className = "mt-0.5 text-xs text-muted-foreground" >
180+ Partagez ce lien avec vos élèves. Ils choisissent leur prénom et discutent avec
181+ l’assistant < em > sous ce Cadre</ em > .
182+ </ p >
149183 </ div >
184+ < button
185+ type = "button"
186+ aria-pressed = { hideCode }
187+ onClick = { ( ) => setHideCode ( ( v ) => ! v ) }
188+ title = "Partager un lien sans le code : l’élève saisit le code lui-même."
189+ className = { `inline-flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
190+ hideCode
191+ ? 'border-accent/40 bg-accent-soft text-accent'
192+ : 'border-border text-muted-foreground hover:bg-surface-muted hover:text-foreground'
193+ } `}
194+ >
195+ { hideCode ? < CheckIcon className = "size-3.5" /> : null }
196+ Masquer le code
197+ </ button >
198+ </ div >
199+ { access . active ? (
200+ < >
201+ < div className = "mt-3 flex flex-wrap items-center gap-2" >
202+ < code className = "min-w-0 flex-1 truncate rounded-button border border-border bg-surface-muted px-3 py-2 font-mono text-sm" >
203+ { link }
204+ </ code >
205+ < CopyLinkButton url = { link } />
206+ < a
207+ href = { link }
208+ target = "_blank"
209+ rel = "noopener noreferrer"
210+ className = "inline-flex items-center gap-1.5 rounded-button border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-surface-muted"
211+ >
212+ Ouvrir
213+ < ExternalLinkIcon className = "size-3.5" />
214+ </ a >
215+ < button
216+ type = "button"
217+ onClick = { ( ) => setQrOpen ( true ) }
218+ aria-label = "Afficher le QR code"
219+ title = "Afficher le QR code"
220+ className = "inline-flex items-center gap-1.5 rounded-button border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-surface-muted"
221+ >
222+ < QrIcon className = "size-4" />
223+ QR code
224+ </ button >
225+ </ div >
226+ { hideCode ? (
227+ < p className = "mt-2 rounded-button bg-surface-muted px-3 py-2 text-xs text-muted-foreground" >
228+ Les élèves vont sur < span className = "font-mono text-foreground" > { baseLink } </ span > { ' ' }
229+ puis saisissent le code :{ ' ' }
230+ < span className = "font-mono font-bold tracking-wider text-foreground" >
231+ { access . token }
232+ </ span >
233+ </ p >
234+ ) : null }
235+ </ >
150236 ) : (
151237 < p className = "mt-3 rounded-button border border-dashed border-border bg-surface-muted px-3 py-2.5 text-sm text-muted-foreground" >
152238 Lien désactivé — réactivez l’espace pour le partager à nouveau.
@@ -237,6 +323,14 @@ export function AccessDetail({
237323 onClose = { ( ) => setSupervising ( null ) }
238324 />
239325 ) : null }
326+
327+ { qrOpen && access . active ? (
328+ < QrModal
329+ url = { link }
330+ code = { hideCode ? access . token : undefined }
331+ onClose = { ( ) => setQrOpen ( false ) }
332+ />
333+ ) : null }
240334 </ div >
241335 ) ;
242336}
0 commit comments