@@ -5,7 +5,7 @@ import axios, { AxiosError } from "axios"
55import { EventSource } from "extended-eventsource"
66import { compact , concat , flatten } from "lodash"
77import { Uri } from "monaco-editor"
8- import { useEffect , useRef , useState } from "react"
8+ import { Dispatch , SetStateAction , useEffect , useRef , useState } from "react"
99import { useParams } from "react-router-dom"
1010import { useEventSource } from "../context/EventSourceContext"
1111
@@ -27,17 +27,17 @@ const usePath = (prefix: string): string[] => {
2727 "assignments" ,
2828 assignmentSlug ,
2929 prefix !== "assignments" && [ "tasks" , taskSlug ] ,
30- ]
31- )
32- )
30+ ] ,
31+ ) ,
32+ ) ,
3333 )
3434}
3535
3636export const useCreate = ( slug : string ) => {
3737 const target = slug === "" ? "/create" : "/edit"
3838 const { mutate, isLoading } = useMutation < string , AxiosError , object > (
3939 ( repository ) => axios . post ( target , repository ) ,
40- { onSuccess : ( ) => window . location . reload ( ) }
40+ { onSuccess : ( ) => window . location . reload ( ) } ,
4141 )
4242 return { mutate, isLoading }
4343}
@@ -46,7 +46,7 @@ export const usePull = () => {
4646 const path = usePath ( "" )
4747 const { mutate, isLoading } = useMutation (
4848 ( ) => axios . post ( "/courses" + `/${ path [ 1 ] } /pull` , { } ) ,
49- { onSuccess : ( ) => window . location . reload ( ) }
49+ { onSuccess : ( ) => window . location . reload ( ) } ,
5050 )
5151 return { mutate, isLoading }
5252}
@@ -126,6 +126,27 @@ export const useResetExample = () => {
126126 return { resetExample }
127127}
128128
129+ export const useCategorize = ( ) => {
130+ const { courseSlug, exampleSlug } = useParams ( )
131+
132+ const { mutateAsync : categorize , isLoading } = useMutation <
133+ {
134+ categories : Record < string , number [ ] >
135+ } ,
136+ AxiosError ,
137+ number [ ]
138+ > ( {
139+ mutationFn : ( submissionIds ) => {
140+ const url = `/courses/${ courseSlug } /examples/${ exampleSlug } /categorize`
141+ return axios . post ( url , {
142+ submissionIds,
143+ } )
144+ } ,
145+ } )
146+
147+ return { categorize, isLoading }
148+ }
149+
129150export const useExamples = ( ) => {
130151 const { courseSlug } = useParams ( )
131152
@@ -163,7 +184,7 @@ export const useAssignment = () => {
163184 const { courseSlug, assignmentSlug } = useParams ( )
164185 return useQuery < AssignmentProps > (
165186 [ "courses" , courseSlug , "assignments" , assignmentSlug ] ,
166- { enabled : ! ! assignmentSlug }
187+ { enabled : ! ! assignmentSlug } ,
167188 )
168189}
169190
@@ -172,7 +193,7 @@ export const useExample = (userId: string) => {
172193 const { courseSlug, exampleSlug } = useParams ( )
173194 const query = useQuery < TaskProps > (
174195 [ "courses" , courseSlug , "examples" , exampleSlug , "users" , userId ] ,
175- { enabled : ! timer }
196+ { enabled : ! timer } ,
176197 )
177198 // eslint-disable-next-line
178199 const { mutateAsync } = useMutation < any , AxiosError , any [ ] > (
@@ -181,7 +202,7 @@ export const useExample = (userId: string) => {
181202 onMutate : ( ) => setTimer ( Date . now ( ) + 30000 ) ,
182203 onSettled : ( ) => setTimer ( undefined ) ,
183204 onSuccess : query . refetch ,
184- }
205+ } ,
185206 )
186207 const submit = ( data : NewSubmissionProps ) =>
187208 mutateAsync ( [
@@ -205,7 +226,7 @@ export const useTask = (userId: string) => {
205226 "users" ,
206227 userId ,
207228 ] ,
208- { enabled : ! timer }
229+ { enabled : ! timer } ,
209230 )
210231 // eslint-disable-next-line
211232 const { mutateAsync } = useMutation < any , AxiosError , any [ ] > (
@@ -214,7 +235,7 @@ export const useTask = (userId: string) => {
214235 onMutate : ( ) => setTimer ( Date . now ( ) + 30000 ) ,
215236 onSettled : ( ) => setTimer ( undefined ) ,
216237 onSuccess : query . refetch ,
217- }
238+ } ,
218239 )
219240 const submit = ( data : NewSubmissionProps ) =>
220241 mutateAsync ( [
@@ -234,38 +255,49 @@ export const useTask = (userId: string) => {
234255
235256export const useCountdown = ( start : number | null , end : number | null ) => {
236257 const [ timeLeftInSeconds , setTimeLeftInSeconds ] = useState < number | null > (
237- null
258+ null ,
238259 )
239260 const [ circleValue , setCircleValue ] = useState < number | null > ( null )
240- const requestRef = useRef < number | null > ( null )
261+
262+ const timeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
263+ const cancelledRef = useRef ( false )
241264
242265 useEffect ( ( ) => {
243- if ( start === null || end === null ) {
266+ if ( start === null || end === null || end <= start ) {
267+ setTimeLeftInSeconds ( null )
268+ setCircleValue ( null )
244269 return
245270 }
246271
247- const update = ( ) => {
248- const total = end - start
272+ cancelledRef . current = false
273+ const total = end - start
274+
275+ const tick = ( ) => {
276+ if ( cancelledRef . current ) return
277+
249278 const now = Date . now ( )
250279 const timeLeft = Math . max ( 0 , end - now )
251280
252- setTimeLeftInSeconds ( Math . floor ( timeLeft / 1000 ) )
281+ setTimeLeftInSeconds ( ( prev ) => {
282+ const current = Math . floor ( timeLeft / 1000 )
283+ return prev !== current ? current : prev
284+ } )
253285
254286 const progress = Math . max ( 0 , ( timeLeft / total ) * 100 )
255287 setCircleValue ( progress )
288+
256289 if ( timeLeft > 0 ) {
257- requestRef . current = requestAnimationFrame ( update )
258- } else {
259- requestRef . current = null
290+ timeoutRef . current = setTimeout ( tick , 100 )
260291 }
261292 }
262293
263- update ( ) // only called initially or when startUnix or endUnix changes
294+ tick ( )
264295
265296 return ( ) => {
266- if ( requestRef . current ) {
267- cancelAnimationFrame ( requestRef . current )
268- requestRef . current = null
297+ cancelledRef . current = true
298+ if ( timeoutRef . current !== null ) {
299+ clearTimeout ( timeoutRef . current )
300+ timeoutRef . current = null
269301 }
270302 }
271303 } , [ start , end ] )
@@ -290,7 +322,7 @@ export const useTimeframeFromSSE = () => {
290322 {
291323 headers : { Authorization : `Bearer ${ token } ` } ,
292324 retry : 3000 ,
293- }
325+ } ,
294326 )
295327
296328 const onTimeEvent = ( e : MessageEvent ) => {
@@ -350,7 +382,7 @@ export const useInspect = () => {
350382
351383 if ( ! courseSlug || ! exampleSlug ) {
352384 throw new Error (
353- `Course Slug ${ courseSlug } or example slug ${ exampleSlug } undefined`
385+ `Course Slug ${ courseSlug } or example slug ${ exampleSlug } undefined` ,
354386 )
355387 }
356388
@@ -374,7 +406,7 @@ export const useInspect = () => {
374406export const useStudentSubmissions = ( ) => {
375407 const { courseSlug, exampleSlug } = useParams ( )
376408
377- return useQuery < SubmissionSsePayload [ ] > ( [
409+ return useQuery < ExampleSubmissionsDTO > ( [
378410 "courses" ,
379411 courseSlug ,
380412 "examples" ,
@@ -383,6 +415,19 @@ export const useStudentSubmissions = () => {
383415 ] )
384416}
385417
418+ export const useExamplePointDistribution = (
419+ options : UseQueryOptions < PointDistribution > = { } ,
420+ ) => {
421+ const { courseSlug, exampleSlug } = useParams ( )
422+ return useQuery < PointDistribution > (
423+ [ "courses" , courseSlug , "examples" , exampleSlug , "point-distribution" ] ,
424+ {
425+ enabled : options . enabled ,
426+ ...options ,
427+ } ,
428+ )
429+ }
430+
386431export const useHeartbeat = ( ) => {
387432 const { courseSlug } = useParams ( )
388433
@@ -400,3 +445,24 @@ export const useHeartbeat = () => {
400445
401446 return { sendHeartbeat }
402447}
448+
449+ const getStorageValue = < T , > ( key : string , defaultValue : T ) => {
450+ const saved = localStorage . getItem ( key )
451+ if ( ! saved ) return defaultValue
452+ return JSON . parse ( saved )
453+ }
454+
455+ export const useLocalStorage = < T , > (
456+ key : string ,
457+ defaultValue : T ,
458+ ) : [ T , Dispatch < SetStateAction < T > > ] => {
459+ const [ value , setValue ] = useState < T > ( ( ) =>
460+ getStorageValue ( key , defaultValue ) ,
461+ )
462+
463+ useEffect ( ( ) => {
464+ localStorage . setItem ( key , JSON . stringify ( value ) )
465+ } , [ key , value ] )
466+
467+ return [ value , setValue ]
468+ }
0 commit comments