@@ -4,19 +4,23 @@ import useValidateUrlToken from "@/app/hooks/security/useValidateUrlToken";
4
4
import useAuth from "@/app/hooks/context_imports/useAuth" ;
5
5
6
6
import { SurvivalQuizInfo , TimeAttackQuizInfo , Message , Participant , ResponseStatus ,
7
- PostTimeAttackEntry , PostSurvivalEntry } from "@/app/interfaces" ;
7
+ PostTimeAttackEntry , PostSurvivalEntry , TimeAttackEntry , SurvivalEntry } from "@/app/interfaces" ;
8
8
import AnimatedScoreCounter from "@/app/components/AnimatedScoreCounter" ;
9
9
import Modal from "@/app/components/modals/Modal" ;
10
10
import { toggleModal , isModalOpen , applyTextMarkup , renderModalResponseAlert ,
11
- playSoundEffect , executeEventSequence } from "@/app/utilities/miscFunctions" ;
11
+ playSoundEffect , executeEventSequence , isTimeAttackEntry } from "@/app/utilities/miscFunctions" ;
12
12
13
13
import useGetQuizInfo from "@/app/hooks/api_access/quizzes/useGetQuizInfo" ;
14
14
import useGetRandomQuizMessage from "@/app/hooks/api_access/messages/useGetRandomQuizMessage" ;
15
15
import usePostLeaderboardEntry from "@/app/hooks/api_access/leaderboards/usePostLeaderboardEntry" ;
16
16
import useAdjustContentHeight from "@/app/hooks/useAdjustContentHeight" ;
17
+ import useGetPastPlayerLeaderboardEntry from "@/app/hooks/api_access/leaderboards/useGetPastPlayerLeaderboardEntry" ;
18
+ import usePatchLeaderboardEntry from "@/app/hooks/api_access/leaderboards/usePatchLeaderboardEntry" ;
17
19
18
20
import AnimateHeight from "react-animate-height" ;
19
21
import { Height } from "react-animate-height" ;
22
+ import { v4 as uuidv4 } from "uuid" ;
23
+
20
24
import Link from "next/link" ;
21
25
import { useRouter } from "next/navigation" ;
22
26
import { useEffect , useState , useReducer , useRef } from "react" ;
@@ -56,6 +60,8 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
56
60
const getQuizInfo = useGetQuizInfo ( ) ;
57
61
const getRandomQuizMessage = useGetRandomQuizMessage ( ) ;
58
62
const postLeaderboardEntry = usePostLeaderboardEntry ( ) ;
63
+ const getPastPlayerLeaderboardEntry = useGetPastPlayerLeaderboardEntry ( ) ;
64
+ const patchLeaderboardEntry = usePatchLeaderboardEntry ( ) ;
59
65
60
66
// Security
61
67
const { auth } = useAuth ( ) ;
@@ -69,6 +75,8 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
69
75
const [ nextMessage , setNextMessage ] = useState < Message | null > ( null ) ;
70
76
const [ seenMessageIds , setSeenMessageIds ] = useState < Array < number > > ( [ ] ) ; // Prevent repeats
71
77
const [ playerName , setPlayerName ] = useState < string > ( "" ) ; // The name of the player (for submitting to the leaderboard)
78
+ const [ playerUUID , setPlayerUUID ] = useState < string | null > ( null ) ; // The UUID of the player (for updating previous leaderboard submissions)
79
+ const [ previousLeaderboardEntry , setPreviousLeaderboardEntry ] = useState < TimeAttackEntry | SurvivalEntry | null > ( null ) ; // The player's previous leaderboard entry [if any]
72
80
const [ scoreSubmitted , setScoreSubmitted ] = useState < boolean > ( false ) ; // Whether the score has been submitted to the leaderboard
73
81
74
82
// ----------- State (Game) -----------
@@ -82,6 +90,7 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
82
90
// ----------- State (UI) -------------
83
91
const [ staticDataLoading , setStaticDataLoading ] = useState < boolean > ( true ) ;
84
92
const [ submitting , setSubmitting ] = useState < boolean > ( false ) ; // Whether the score is being submitted to the leaderboard
93
+ const [ noResubmit , setNoResubmit ] = useState < boolean > ( false ) ; // Whether the user has decided not to update their previous leaderboard entry
85
94
// The response status of the leaderboard submission
86
95
const [ responseStatus , setResponseStatus ] = useState < ResponseStatus > ( { message : "" , success : false , doAnimate : false } ) ;
87
96
@@ -139,6 +148,19 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
139
148
console . error ( "Error retrieving data, redirecting to root" ) ;
140
149
router . push ( "/" ) ;
141
150
}
151
+
152
+ // See if the player has a UUID in local storage, if not, create one and store it
153
+ let uuid : string | null = localStorage . getItem ( "quizPlayerUUID" ) ;
154
+ if ( uuid ) {
155
+ // If there is a UUID, retrieve the player's previous leaderboard entry (if any)
156
+ const entry : TimeAttackEntry | SurvivalEntry | null = await getPastPlayerLeaderboardEntry ( quizId , uuid , shareableToken || undefined ) ;
157
+ if ( entry ) setPreviousLeaderboardEntry ( entry ) ;
158
+ } else {
159
+ uuid = uuidv4 ( ) ; // Generate a new one
160
+ localStorage . setItem ( "quizPlayerUUID" , uuid ) ;
161
+ }
162
+ setPlayerUUID ( uuid ) ;
163
+
142
164
setStaticDataLoading ( false ) ;
143
165
144
166
// Begin the intro splash timing sequence
@@ -172,24 +194,43 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
172
194
}
173
195
}
174
196
175
- const submitLeaderboardEntry = async ( ) => {
176
- if ( ! quizInfo || ! totalTimeTaken || playerName === "" ) return ;
197
+ const submitLeaderboardEntry = async ( isUpdate : boolean = false ) => {
198
+ if ( ! quizInfo || ! totalTimeTaken || ( ! isUpdate && playerName === "" ) || ! playerUUID ) {
199
+ console . error ( "Error submitting leaderboard entry: Missing required data" ) ;
200
+ return ;
201
+ }
202
+ if ( isUpdate && ! previousLeaderboardEntry ) {
203
+ console . error ( "Error submitting leaderboard entry: No previous entry found" ) ;
204
+ return ;
205
+ }
177
206
setSubmitting ( true ) ;
207
+
208
+ // Build the request object
178
209
let postRequest : PostTimeAttackEntry | PostSurvivalEntry ;
179
210
if ( isTimeAttack ( quizInfo ) ) {
180
211
postRequest = {
181
- playerName : playerName ,
212
+ playerName : ( isUpdate && previousLeaderboardEntry ) ? previousLeaderboardEntry . playerName : playerName ,
182
213
score : score ,
183
214
timeTaken : totalTimeTaken ,
215
+ playerUUID : playerUUID
184
216
} ;
185
217
} else {
186
218
postRequest = {
187
- playerName : playerName ,
219
+ playerName : ( isUpdate && previousLeaderboardEntry ) ? previousLeaderboardEntry . playerName : playerName ,
188
220
streak : score ,
189
221
skipsUsed : skipsUsed ,
222
+ playerUUID : playerUUID
190
223
} ;
191
224
}
192
- const error : string | null = await postLeaderboardEntry ( quizId , postRequest , shareableToken || undefined ) ;
225
+
226
+ // Make the API request
227
+ let error : string | null ;
228
+ if ( isUpdate && previousLeaderboardEntry ) {
229
+ error = await patchLeaderboardEntry ( quizId , previousLeaderboardEntry . id , postRequest , shareableToken || undefined ) ;
230
+ } else {
231
+ error = await postLeaderboardEntry ( quizId , postRequest , shareableToken || undefined ) ;
232
+ }
233
+
193
234
if ( ! error ) {
194
235
setResponseStatus ( { message : "Entry submitted!" , success : true , doAnimate : true } ) ;
195
236
setScoreSubmitted ( true ) ;
@@ -198,14 +239,14 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
198
239
setResponseStatus ( { message : error , success : false , doAnimate : true } ) ;
199
240
}
200
241
201
- // Display the response message for 3 seconds, then close the modal
242
+ // Display the response message for 1.5 seconds, then close the modal
202
243
setTimeout ( ( ) => {
203
244
if ( isModalOpen ( "leaderboard-submit-modal" ) ) {
204
245
toggleModal ( "leaderboard-submit-modal" ) ;
205
246
}
206
247
setSubmitting ( false ) ;
207
248
setResponseStatus ( { message : "" , success : false , doAnimate : false } ) ;
208
- } , 3000 ) ;
249
+ } , 1500 ) ;
209
250
}
210
251
211
252
// Gets a random selection of 4 or 3 (depending on type) participants to choose from, including the correct participant.
@@ -639,16 +680,42 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
639
680
{ renderModalResponseAlert ( responseStatus , true ) }
640
681
</ > ) ;
641
682
} else if ( submitting ) {
642
- modalContent = (
643
- < div className = "mb-8 mt-1 sm:my-12" >
644
- < div className = "mx-auto mb-3 text-2xl text-center" >
683
+ modalContent = ( < >
684
+ < div className = "mt-[-12px]" />
685
+ < div className = "mb-6 mt-4 sm:mb-12 sm:mt-10" >
686
+ < div className = "mx-auto mb-2 text-xl text-center" >
645
687
Submitting...
646
688
</ div >
647
689
< div className = "flex justify-center" >
648
690
< div className = "spinner-circle w-12 h-12 sm:w-14 sm:h-14" />
649
691
</ div >
650
692
</ div >
651
- ) ;
693
+ </ > ) ;
694
+ } else if ( previousLeaderboardEntry && ! noResubmit ) {
695
+ modalContent = ( < >
696
+ < div className = "w-full mt-3 text-center text-2xl font-semibold" >
697
+ Update previous entry?
698
+ </ div >
699
+ < div className = "w-full text-center text-lg mt-3 text-zinc-300 font-light" >
700
+ You had a { isTimeAttack ( quizInfo ) ? "score" : "streak" } of:
701
+ < span className = "ml-2 text-white font-medium" >
702
+ { isTimeAttackEntry ( previousLeaderboardEntry ) ? previousLeaderboardEntry . score : previousLeaderboardEntry . streak }
703
+ </ span >
704
+ </ div >
705
+ < div className = "grid grid-cols-2 gap-2 mt-6 mb-5 mx-7" >
706
+ < button className = "grow btn btn-lg bg-black border border-zinc-900 text-zinc-400 font-light"
707
+ onClick = { ( ) => setNoResubmit ( true ) } >
708
+ No Thanks
709
+ </ button >
710
+ < button className = { `grow btn btn-lg bg-gradient-to-r font-semibold
711
+ ${ isTimeAttack ( quizInfo )
712
+ ? " from-blue-500 from-0% via-blue-400 to-blue-500 to-100% text-indigo-100"
713
+ : " from-purple-500 from-0% via-pink-500 to-purple-500 to-100% text-purple-100" } `}
714
+ onClick = { ( ) => submitLeaderboardEntry ( true ) } >
715
+ Update
716
+ </ button >
717
+ </ div >
718
+ </ > ) ;
652
719
} else {
653
720
modalContent = ( < >
654
721
< div className = { `w-full mt-2 text-center text-3xl font-semibold text-transparent bg-clip-text bg-gradient-to-r
@@ -730,7 +797,10 @@ export default function Quiz({ params }: { params: { query: string[] } }) {
730
797
${ isTimeAttack ( quizInfo )
731
798
? " from-blue-500 from-0% via-blue-400 to-blue-500 to-100% text-indigo-100"
732
799
: " from-purple-500 from-0% via-pink-500 to-purple-500 to-100% text-purple-100" } `}
733
- onClick = { ( ) => { toggleModal ( "leaderboard-submit-modal" ) } } >
800
+ onClick = { ( ) => {
801
+ setNoResubmit ( false ) ;
802
+ toggleModal ( "leaderboard-submit-modal" ) ;
803
+ } } >
734
804
Submit
735
805
</ button >
736
806
}
0 commit comments