Skip to content

Commit 96164b4

Browse files
committed
Merge branch 'staging'
2 parents b659c16 + 0dbb3d5 commit 96164b4

File tree

10 files changed

+1013
-195
lines changed

10 files changed

+1013
-195
lines changed

src/components/BookmarkView.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Button, Flex, Text, VStack } from "@chakra-ui/react"
2+
3+
const Bookmark: React.FC<{
4+
bookmark: Bookmark
5+
handleBookmarkSelection: (bookmark: Bookmark) => void
6+
color: string
7+
}> = ({ bookmark, handleBookmarkSelection, color }) => {
8+
const nameOnly = bookmark.studentId.split("@")[0]
9+
const testsPassed = bookmark.testsPassed.reduce(
10+
(accumulator, currentVal) => (accumulator += currentVal),
11+
)
12+
return (
13+
<Button
14+
borderRadius={"lg"}
15+
h={10}
16+
width={"full"}
17+
colorScheme={color}
18+
bg={`${color}.500`}
19+
color={"white"}
20+
onClick={() => handleBookmarkSelection(bookmark)}
21+
>
22+
<Flex flex={1}>
23+
<Text flex={1} align={"start"}>
24+
{nameOnly}
25+
</Text>
26+
<Text flex={1} align={"start"}>
27+
Tests Passed: {testsPassed}/{bookmark.testsPassed.length}
28+
</Text>
29+
</Flex>
30+
</Button>
31+
)
32+
}
33+
34+
export const BookmarkView: React.FC<{
35+
bookmarks: Bookmark[] | null
36+
getSubmissionColor: (submissionId: number) => string
37+
handleBookmarkSelection: (bookmark: Bookmark) => void
38+
}> = ({ bookmarks, handleBookmarkSelection, getSubmissionColor }) => {
39+
if (!bookmarks)
40+
return (
41+
<Flex justify={"center"} flex={1}>
42+
<Text> There are no Bookmarks yet.</Text>
43+
</Flex>
44+
)
45+
46+
return (
47+
<VStack width={"full"} align={"start"}>
48+
{bookmarks.map((bookmark, key) => (
49+
<Bookmark
50+
key={key}
51+
bookmark={bookmark}
52+
handleBookmarkSelection={handleBookmarkSelection}
53+
color={getSubmissionColor(bookmark.submissionId)}
54+
/>
55+
))}
56+
</VStack>
57+
)
58+
}

src/components/Carousel.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
flex-shrink: 0;
2525
width: 100%;
2626
height: 100%;
27-
margin-right: 50px;
2827
border-radius: 10px;
2928
background: #ffffff;
3029
display: flex;

src/components/CountdownTimer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,18 @@ export const CountdownTimer: React.FC<{
2525
onTimeIsUp &&
2626
startTime !== null &&
2727
endTime !== null &&
28-
timeLeftInSeconds == 0
28+
timeLeftInSeconds !== null &&
29+
timeLeftInSeconds <= 0
2930
) {
3031
onTimeIsUp()
3132
}
3233
}, [endTime, onTimeIsUp, startTime, timeLeftInSeconds])
3334

34-
const totalTimeInSeconds = ((endTime ?? 0) - (startTime ?? 0)) / 1000
35+
const totalTimeInSeconds = useMemo(() => {
36+
if (startTime == null || endTime == null) return 0
37+
return Math.max(0, (endTime - startTime) / 1000)
38+
}, [startTime, endTime])
39+
3540
const remainingTimeString = formatSeconds(timeLeftInSeconds ?? 0)
3641
const fontSize = size === "large" ? "4xl" : size === "medium" ? "3xl" : "md"
3742
const dynamicColor = useMemo(() => {

src/components/Hooks.tsx

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import axios, { AxiosError } from "axios"
55
import { EventSource } from "extended-eventsource"
66
import { compact, concat, flatten } from "lodash"
77
import { Uri } from "monaco-editor"
8-
import { useEffect, useRef, useState } from "react"
8+
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"
99
import { useParams } from "react-router-dom"
1010
import { 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

3636
export 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+
129150
export 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

235256
export 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 = () => {
374406
export 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+
386431
export 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

Comments
 (0)