@@ -2,21 +2,57 @@ import { useMutation, useQuery } from '@apollo/client'
22import { faKeyboard } from '@fortawesome/free-regular-svg-icons'
33import {
44 faArrowLeft ,
5+ faArrowRight ,
6+ faCheck ,
57 faUserSecret ,
68 faUserTie ,
79} from '@fortawesome/free-solid-svg-icons'
10+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
811import {
912 LoginTemporaryParticipantDocument ,
1013 SelfDocument ,
1114 UserRole ,
1215} from '@klicker-uzh/graphql/dist/ops'
1316import { useLocalStorage } from '@uidotdev/usehooks'
14- import { Button , FormikTextField , Modal , toast } from '@uzh-bf/design-system'
17+ import {
18+ Button ,
19+ Carousel ,
20+ CarouselApi ,
21+ CarouselContent ,
22+ CarouselNext ,
23+ CarouselPrevious ,
24+ FormikTextField ,
25+ Modal ,
26+ toast ,
27+ } from '@uzh-bf/design-system'
1528import { Form , Formik } from 'formik'
1629import { useTranslations } from 'next-intl'
30+ import Image from 'next/image'
1731import { useRouter } from 'next/router'
1832import { useEffect , useState } from 'react'
33+ import { twMerge } from 'tailwind-merge'
1934import * as Yup from 'yup'
35+ import CarouselMonitor from './CarouselMonitor'
36+
37+ const AVAILABLE_AVATARS = [
38+ '' ,
39+ '43de5cc3e88371b82515e365b61ca4f56b3fff76' ,
40+ 'd8bced94c310832dc36fc22ad3a8fd7fbde69010' ,
41+ 'eda64b05916c5d5d219a840befc63e269978a1da' ,
42+ '2d1cfed9ead85a12389badd09e48e0ded97540f9' ,
43+ 'a9178a73231f5583c39316b23579dc8fb2f6ba2e' ,
44+ '5bbb97018ce7d9de56c93feb71c20c4741d80205' ,
45+ '1668749bf2981ef2d432d16aabe01151ddb774a2' ,
46+ 'b877b984af00e8d901f714a470d50ba1282a6e00' ,
47+ '212fa161b03cc873b03eee91fce1e6b123305700' ,
48+ 'ba359939f81458694d3ae1ead5d42a9c52204ba9' ,
49+ 'b8c64871872753fb5e6f61c3e93c89f4aeceb7d7' ,
50+ 'af8392c87388febc5814c15d97630138b419b2ed' ,
51+ 'cabe95f3e740b9503e1bf771972d230600a4528c' ,
52+ 'f60344fc3fef0b4e4e1d7a9b17950b9102a89ecf' ,
53+ '075bcfa2a31a58e38badb2b81381787e2c7386c5' ,
54+ '77749da96b827d1fa055e916d6e7a0d26b42bdff' ,
55+ ]
2056
2157function AccountSelector ( {
2258 quizId,
@@ -29,10 +65,11 @@ function AccountSelector({
2965 const router = useRouter ( )
3066
3167 const [ open , setOpen ] = useState ( false )
32- const [ definePseudonym , setDefinePseudonym ] = useState ( false )
68+ const [ step , setStep ] = useState < 'choice' | 'pseudonym' | 'avatar' > ( 'choice' )
3369 const [ loginState , setLoginState ] = useLocalStorage <
3470 'anonymous' | 'temporary' | 'loggedIn' | undefined
3571 > ( `login-state-${ quizId } ` , undefined )
72+ const [ api , setApi ] = useState < CarouselApi > ( )
3673
3774 const [ loginTemporaryParticipant , { loading : loggingIn } ] = useMutation (
3875 LoginTemporaryParticipantDocument
@@ -77,7 +114,7 @@ function AccountSelector({
77114 open = { open && isGamificationEnabled }
78115 onClose = { ( ) => {
79116 setOpen ( false )
80- setDefinePseudonym ( false )
117+ setStep ( 'choice' )
81118 } }
82119 title = {
83120 < div >
@@ -88,7 +125,7 @@ function AccountSelector({
88125 }
89126 className = { { title : 'mb-4 text-center' , content : 'max-w-[25rem] pb-2' } }
90127 >
91- { ! definePseudonym ? (
128+ { step === 'choice' ? (
92129 < >
93130 < div className = "mb-4 text-sm" >
94131 { t . rich ( 'pwa.liveQuiz.loginSelectionHint' , {
@@ -113,7 +150,7 @@ function AccountSelector({
113150 </ Button >
114151 < Button
115152 className = { { root : 'justify-start' } }
116- onClick = { ( ) => setDefinePseudonym ( true ) }
153+ onClick = { ( ) => setStep ( 'pseudonym' ) }
117154 data = { { cy : 'create-temporary-pseudonym' } }
118155 >
119156 < Button . Icon icon = { faKeyboard } />
@@ -125,7 +162,7 @@ function AccountSelector({
125162 className = { { root : 'justify-start' } }
126163 onClick = { ( ) => {
127164 setLoginState ( 'anonymous' )
128- setDefinePseudonym ( false )
165+ setStep ( 'choice' )
129166 setOpen ( false )
130167 } }
131168 data = { { cy : 'participate-anonymously' } }
@@ -137,99 +174,192 @@ function AccountSelector({
137174 </ Button >
138175 </ div >
139176 </ >
140- ) : (
141- < >
142- < Button
143- basic
144- className = { { root : 'px-2 py-1 text-sm' } }
145- onClick = { ( ) => setDefinePseudonym ( false ) }
146- data = { { cy : 'cancel-define-pseudonym' } }
147- >
148- < Button . Icon icon = { faArrowLeft } />
149- < Button . Label > { t ( 'pwa.liveQuiz.changeLoginMode' ) } </ Button . Label >
150- </ Button >
151- < div className = "mb-2 mt-1 text-sm" >
152- { t . rich ( 'pwa.liveQuiz.pseudonymExplanation' , {
153- b : ( text ) => < b > { text } </ b > ,
154- } ) }
155- </ div >
156- < Formik
157- initialValues = { { pseudonym : '' } }
158- validationSchema = { Yup . object ( {
159- pseudonym : Yup . string ( )
160- . required ( t ( 'pwa.liveQuiz.pseudonymRequired' ) )
161- . min ( 5 , t ( 'pwa.liveQuiz.pseudonymMinLength' , { length : '5' } ) )
162- . max (
163- 15 ,
164- t ( 'pwa.liveQuiz.pseudonymMaxLength' , { length : '15' } )
165- ) ,
166- } ) }
167- onSubmit = { async ( values ) => {
168- try {
169- const { data } = await loginTemporaryParticipant ( {
170- variables : {
171- liveQuizId : quizId ,
177+ ) : null }
178+ < Formik
179+ initialValues = { { pseudonym : '' , avatar : '' } }
180+ validationSchema = { Yup . object ( {
181+ pseudonym : Yup . string ( )
182+ . required ( t ( 'pwa.liveQuiz.pseudonymRequired' ) )
183+ . min ( 5 , t ( 'pwa.liveQuiz.pseudonymMinLength' , { length : '5' } ) )
184+ . max ( 15 , t ( 'pwa.liveQuiz.pseudonymMaxLength' , { length : '15' } ) ) ,
185+ avatar : Yup . string ( ) ,
186+ } ) }
187+ onSubmit = { async ( values ) => {
188+ try {
189+ const { data } = await loginTemporaryParticipant ( {
190+ variables : {
191+ liveQuizId : quizId ,
192+ pseudonym : values . pseudonym ,
193+ avatar : values . avatar !== '' ? values . avatar : undefined ,
194+ } ,
195+ refetchQueries : [ { query : SelfDocument } ] ,
196+ } )
197+
198+ if ( data ?. loginTemporaryParticipant ) {
199+ setLoginState ( 'temporary' )
200+ setOpen ( false )
201+
202+ toast ( {
203+ type : 'success' ,
204+ message : t . rich (
205+ 'pwa.liveQuiz.joinedSuccessfullyWithPseudonym' ,
206+ {
172207 pseudonym : values . pseudonym ,
173- } ,
174- } )
208+ b : ( text ) => < b > { text } </ b > ,
209+ }
210+ ) ,
211+ options : { duration : 5000 } ,
212+ } )
175213
176- if ( data ?. loginTemporaryParticipant ) {
177- setLoginState ( 'temporary' )
178- setOpen ( false )
214+ await refetch ( ) // refetch the self query to update the user data
215+ } else {
216+ toast ( {
217+ type : 'error' ,
218+ message : t ( 'pwa.liveQuiz.pseudonymAlreadyExists' ) ,
219+ options : { duration : 5000 } ,
220+ } )
221+ }
222+ } catch ( error ) {
223+ console . error ( 'Error logging in as temporary participant:' , error )
224+ toast ( {
225+ type : 'error' ,
226+ message : t ( 'pwa.liveQuiz.pseudonymCreationFailed' ) ,
227+ options : { duration : 5000 } ,
228+ } )
229+ }
230+ } }
231+ >
232+ { ( { values, setFieldValue } ) => (
233+ < Form className = "flex flex-col" >
234+ { step === 'pseudonym' ? (
235+ < >
236+ < Button
237+ basic
238+ className = { { root : 'w-max px-2 py-1 text-sm' } }
239+ onClick = { ( ) => setStep ( 'choice' ) }
240+ data = { { cy : 'cancel-define-pseudonym' } }
241+ >
242+ < Button . Icon icon = { faArrowLeft } />
243+ < Button . Label >
244+ { t ( 'pwa.liveQuiz.changeLoginMode' ) }
245+ </ Button . Label >
246+ </ Button >
247+ < div className = "my-1 text-sm" >
248+ { t . rich ( 'pwa.liveQuiz.pseudonymExplanation' , {
249+ b : ( text ) => < b > { text } </ b > ,
250+ } ) }
251+ </ div >
179252
180- toast ( {
181- type : 'success' ,
182- message : t . rich (
183- 'pwa.liveQuiz.joinedSuccessfullyWithPseudonym' ,
184- {
185- pseudonym : values . pseudonym ,
186- b : ( text ) => < b > { text } </ b > ,
187- }
188- ) ,
189- options : { duration : 5000 } ,
190- } )
253+ < FormikTextField
254+ required
255+ name = "pseudonym"
256+ label = { t ( 'shared.generic.pseudonym' ) }
257+ placeholder = "klicker123"
258+ className = { { label : 'text-sm' } }
259+ />
260+ < Button
261+ type = "button"
262+ disabled = {
263+ ! values . pseudonym ||
264+ values . pseudonym . length < 5 ||
265+ values . pseudonym . length > 15
266+ }
267+ onClick = { ( ) => setStep ( 'avatar' ) }
268+ className = { { root : 'mt-2 self-end' } }
269+ >
270+ < Button . Icon icon = { faArrowRight } />
271+ < Button . Label > { t ( 'shared.generic.next' ) } </ Button . Label >
272+ </ Button >
273+ </ >
274+ ) : null }
275+ { step === 'avatar' ? (
276+ < >
277+ < Button
278+ basic
279+ className = { { root : 'w-max px-2 py-1 text-sm' } }
280+ onClick = { ( ) => setStep ( 'pseudonym' ) }
281+ data = { { cy : 'cancel-choose-avatar' } }
282+ >
283+ < Button . Icon icon = { faArrowLeft } />
284+ < Button . Label >
285+ { t ( 'pwa.liveQuiz.pseudonymSelection' ) }
286+ </ Button . Label >
287+ </ Button >
288+ < div className = "my-1 text-sm" >
289+ { t . rich ( 'pwa.liveQuiz.avatarExplanation' , {
290+ b : ( text ) => < b > { text } </ b > ,
291+ } ) }
292+ </ div >
191293
192- await refetch ( ) // refetch the self query to update the user data
193- } else {
194- toast ( {
195- type : 'error' ,
196- message : t ( 'pwa.liveQuiz.pseudonymAlreadyExists' ) ,
197- options : { duration : 5000 } ,
198- } )
199- }
200- } catch ( error ) {
201- console . error (
202- 'Error logging in as temporary participant:' ,
203- error
204- )
205- toast ( {
206- type : 'error' ,
207- message : t ( 'pwa.liveQuiz.pseudonymCreationFailed' ) ,
208- options : { duration : 5000 } ,
209- } )
210- }
211- } }
212- >
213- < Form className = "flex flex-col gap-2" >
214- < FormikTextField
215- required
216- name = "pseudonym"
217- label = { t ( 'shared.generic.pseudonym' ) }
218- placeholder = "klicker123"
219- className = { { label : 'text-sm' } }
220- />
221- < Button
222- primary
223- type = "submit"
224- loading = { loggingIn }
225- className = { { root : 'self-end' } }
226- >
227- { t ( 'shared.generic.submit' ) }
228- </ Button >
229- </ Form >
230- </ Formik >
231- </ >
232- ) }
294+ < CarouselMonitor
295+ avatars = { AVAILABLE_AVATARS }
296+ api = { api }
297+ setFieldValue = { setFieldValue }
298+ />
299+ < Carousel
300+ opts = { { loop : true } }
301+ className = "w-full overflow-visible"
302+ setApi = { setApi }
303+ >
304+ < CarouselContent className = "mt-4 overflow-visible px-10" >
305+ { AVAILABLE_AVATARS . map ( ( avatar , index ) => (
306+ < div
307+ key = { avatar }
308+ className = { twMerge (
309+ 'relative flex justify-center overflow-visible pb-10' ,
310+ index === AVAILABLE_AVATARS . length - 1 && '-mr-10'
311+ ) }
312+ >
313+ < div
314+ className = { twMerge (
315+ 'flex h-40 w-40 cursor-pointer items-center justify-center' ,
316+ avatar === '' && 'p-5 pt-14'
317+ ) }
318+ onClick = { ( ) => setFieldValue ( 'avatar' , avatar ) }
319+ >
320+ < Image
321+ src = {
322+ avatar && avatar !== ''
323+ ? `${ process . env . NEXT_PUBLIC_AVATAR_BASE_PATH } /${ avatar } .svg`
324+ : '/user-solid.svg'
325+ }
326+ alt = "Avatar option"
327+ width = { 140 }
328+ height = { 140 }
329+ className = { twMerge (
330+ 'h-auto w-full rounded-full' ,
331+ avatar ? '' : 'p-2'
332+ ) }
333+ />
334+ </ div >
335+ { values . avatar === avatar && (
336+ < div className = "absolute bottom-0 left-0 right-0 z-10 mx-auto flex w-fit flex-row items-center gap-2.5 rounded-md border-2 border-green-600 bg-white px-2 py-1 text-sm" >
337+ < FontAwesomeIcon
338+ icon = { faCheck }
339+ className = "h-4 w-4 font-bold text-green-600"
340+ />
341+ { t ( 'shared.generic.selected' ) }
342+ </ div >
343+ ) }
344+ </ div >
345+ ) ) }
346+ </ CarouselContent >
347+ < CarouselPrevious type = "button" className = "left-0" />
348+ < CarouselNext type = "button" className = "right-0" />
349+ </ Carousel >
350+ < Button
351+ primary
352+ type = "submit"
353+ loading = { loggingIn }
354+ className = { { root : 'mt-2 self-end' } }
355+ >
356+ { t ( 'shared.generic.submit' ) }
357+ </ Button >
358+ </ >
359+ ) : null }
360+ </ Form >
361+ ) }
362+ </ Formik >
233363 </ Modal >
234364 )
235365}
0 commit comments