Skip to content

Commit f9cfe46

Browse files
authored
Add cooldown timer (#140)
1 parent 6879236 commit f9cfe46

File tree

6 files changed

+150
-6
lines changed

6 files changed

+150
-6
lines changed

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,5 @@ enum SiteSettingsValues {
220220

221221
enum VariableSiteSettings {
222222
EMAIL_DOMAIN
223+
COOLDOWN_TIME // In minutes
223224
}

src/components/admin/AdminView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
99
import useSiteSettings from "../../utils/hooks/useSiteSettings";
1010
import { trpc } from "../../utils/trpc";
1111
import AdminList from "./AdminList";
12+
import CoolDownTimer from "./CooldownTimer";
1213
import ImportUsersMethod from "./ImportUsersMethod";
1314

1415
/**
@@ -107,7 +108,9 @@ const AdminView = () => {
107108
</Flex>
108109
</Flex>
109110

110-
<Divider />
111+
<CoolDownTimer />
112+
113+
<Divider mt={2} />
111114

112115
<ImportUsersMethod />
113116
</Flex>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
Button,
3+
Flex,
4+
NumberDecrementStepper,
5+
NumberIncrementStepper,
6+
NumberInput,
7+
NumberInputField,
8+
NumberInputStepper,
9+
Text,
10+
useToast,
11+
} from "@chakra-ui/react";
12+
import { useState } from "react";
13+
import { trpc } from "../../utils/trpc";
14+
15+
/** Handles how long a student should wait before they can make another ticket */
16+
const CoolDownTimer = () => {
17+
const [cooldownTime, setCooldownTime] = useState<number>(0);
18+
const setCooldownTimeMutation = trpc.admin.setCooldownTime.useMutation();
19+
const toast = useToast();
20+
21+
trpc.admin.getCoolDownTime.useQuery(undefined, {
22+
refetchOnWindowFocus: false,
23+
onSuccess: (time) => {
24+
setCooldownTime(time);
25+
},
26+
});
27+
28+
const handleSetCooldownTime = async () => {
29+
console.log(cooldownTime);
30+
await setCooldownTimeMutation.mutateAsync({
31+
cooldownTime,
32+
});
33+
34+
toast({
35+
title: "Success",
36+
description: "Cooldown time updated",
37+
status: "success",
38+
duration: 5000,
39+
position: "top-right",
40+
isClosable: true,
41+
});
42+
};
43+
44+
return (
45+
<Flex direction="column">
46+
<Text fontSize="xl">Cooldown</Text>
47+
<Text fontSize="md">
48+
Students must wait this many minutes since their last ticket was
49+
resolved to make another ticket
50+
</Text>
51+
<Flex flexDir="row">
52+
<NumberInput
53+
value={cooldownTime}
54+
min={0}
55+
max={180}
56+
width="100%"
57+
onChange={(val) => setCooldownTime(parseInt(val))}
58+
>
59+
<NumberInputField />
60+
<NumberInputStepper>
61+
<NumberIncrementStepper />
62+
<NumberDecrementStepper />
63+
</NumberInputStepper>
64+
</NumberInput>
65+
<Button colorScheme="telegram" onClick={handleSetCooldownTime}>
66+
Confirm
67+
</Button>
68+
</Flex>
69+
</Flex>
70+
);
71+
};
72+
73+
export default CoolDownTimer;

src/components/queue/CreateTicketForm.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ const CreateTicketForm = (props: CreateTicketFormProps) => {
151151
},
152152
});
153153

154+
const cooldownPeriod = trpc.admin.getCoolDownTime.useQuery(undefined, {
155+
refetchOnWindowFocus: false,
156+
}).data;
157+
154158
const handleTicketTypeChange = (newVal: TicketType) => {
155159
setTicketType(newVal);
156160
if (newVal === TicketType.DEBUGGING) {
@@ -223,13 +227,15 @@ const CreateTicketForm = (props: CreateTicketFormProps) => {
223227
})
224228
.then((ticket) => {
225229
if (!ticket) {
230+
const coolDownText = cooldownPeriod
231+
? `You must wait ${cooldownPeriod} minutes since your last ticket was resolved.`
232+
: "";
226233
toast({
227234
title: "Error",
228-
description:
229-
"Could not create ticket. You may already have a ticket open. If not, refresh and try again.",
235+
description: `Could not create ticket. You may already have a ticket open. If not, refresh and try again. ${coolDownText}`,
230236
status: "error",
231237
position: "top-right",
232-
duration: 5000,
238+
duration: 10000,
233239
isClosable: true,
234240
});
235241
return;
@@ -328,8 +334,8 @@ const CreateTicketForm = (props: CreateTicketFormProps) => {
328334
Public
329335
<Tooltip
330336
hasArrow
331-
label="Public tickets can be joined by other students. This is great for group work
332-
or conceptual questions! If your ticket is public, we are more likely to
337+
label="Public tickets can be joined by other students. This is great for group work
338+
or conceptual questions! If your ticket is public, we are more likely to
333339
help you for a longer time."
334340
bg="gray.300"
335341
color="black"

src/server/trpc/router/admin.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,45 @@ export const adminRouter = router({
246246
});
247247
}),
248248

249+
setCooldownTime: protectedStaffProcedure
250+
.input(
251+
z.object({
252+
cooldownTime: z.number(),
253+
}),
254+
)
255+
.mutation(async ({ input, ctx }) => {
256+
if (input.cooldownTime < 0) {
257+
throw new TRPCClientError("Cooldown time cannot be negative");
258+
}
259+
260+
await ctx.prisma.variableSettings.upsert({
261+
where: {
262+
setting: VariableSiteSettings.COOLDOWN_TIME,
263+
},
264+
update: {
265+
value: input.cooldownTime.toString(),
266+
},
267+
create: {
268+
setting: VariableSiteSettings.COOLDOWN_TIME,
269+
value: input.cooldownTime.toString(),
270+
},
271+
});
272+
}),
273+
274+
getCoolDownTime: protectedProcedure.query(async ({ ctx }) => {
275+
const setting = await ctx.prisma.variableSettings.upsert({
276+
where: {
277+
setting: VariableSiteSettings.COOLDOWN_TIME,
278+
},
279+
update: {},
280+
create: {
281+
setting: VariableSiteSettings.COOLDOWN_TIME,
282+
value: "0",
283+
},
284+
});
285+
return parseInt(setting.value);
286+
}),
287+
249288
getEmailDomain: protectedStaffProcedure.query(async ({ ctx }) => {
250289
const setting = await ctx.prisma.variableSettings.upsert({
251290
where: {

src/server/trpc/router/ticket.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
TicketType,
1010
User,
1111
UserRole,
12+
VariableSiteSettings,
1213
} from "@prisma/client";
1314
import { TRPCClientError } from "@trpc/client";
1415
import Ably from "ably/promises";
@@ -120,6 +121,27 @@ export const ticketRouter = router({
120121
// If a ticket is made with a priority assigment, it's a priority ticket
121122
const isPriority = assignment?.isPriority;
122123

124+
// Check if the user has made a ticket within the cooldown period
125+
const cooldownTimeResult = await ctx.prisma.variableSettings.findUnique({
126+
where: { setting: VariableSiteSettings.COOLDOWN_TIME },
127+
});
128+
129+
// in minutes
130+
const cooldownTime = parseInt(cooldownTimeResult?.value ?? "0");
131+
132+
const lastTicket = await ctx.prisma.ticket.findFirst({
133+
where: {
134+
createdByUserId: ctx.session.user.id,
135+
resolvedAt: {
136+
gte: new Date(Date.now() - cooldownTime * 60 * 1000),
137+
},
138+
},
139+
});
140+
141+
if (lastTicket && ctx.session.user.role !== UserRole.STAFF) {
142+
return;
143+
}
144+
123145
const ticket = await ctx.prisma.ticket.create({
124146
data: {
125147
description: input.description,

0 commit comments

Comments
 (0)