Skip to content

Commit 17e7fec

Browse files
committed
feat(web): replace datetime inputs with calendar component for assignment dates
1 parent 6d9c068 commit 17e7fec

File tree

6 files changed

+372
-37
lines changed

6 files changed

+372
-37
lines changed

apps/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"lucide-react": "^0.552.0",
4040
"next": "16.0.10",
4141
"react": "catalog:react19",
42+
"react-day-picker": "^9.14.0",
4243
"react-markdown": "^10.1.0",
4344
"react-scrubber": "^2.1.0",
4445
"react-shiki": "^0.9.0",

apps/nextjs/src/app/teacher/classroom/[id]/assignment/new/page.tsx

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import type { FormEvent } from "react";
4+
import type { DateRange } from "react-day-picker";
45
import { use, useRef, useState } from "react";
56
import Link from "next/link";
67
import { useRouter } from "next/navigation";
@@ -11,6 +12,7 @@ import { toast } from "sonner";
1112
import type { Id } from "@package/backend/convex/_generated/dataModel";
1213
import { api } from "@package/backend/convex/_generated/api";
1314
import { Button } from "@package/ui/button";
15+
import { Calendar } from "@package/ui/calendar";
1416
import {
1517
Card,
1618
CardContent,
@@ -27,12 +29,6 @@ import { Editor } from "~/components/editor";
2729
import { StarterCodeUploader } from "~/components/starter-code-uploader";
2830
import { Authenticated, AuthLoading, Unauthenticated } from "~/lib/auth";
2931

30-
function formatDateForInput(timestamp: number) {
31-
const date = new Date(timestamp);
32-
const offset = date.getTimezoneOffset() * 60_000;
33-
return new Date(date.getTime() - offset).toISOString().slice(0, 16);
34-
}
35-
3632
function NewAssignmentForm({ classroomId }: { classroomId: Id<"classrooms"> }) {
3733
const router = useRouter();
3834
const createAssignment = useMutation(
@@ -48,30 +44,27 @@ function NewAssignmentForm({ classroomId }: { classroomId: Id<"classrooms"> }) {
4844
const [isSubmitting, setIsSubmitting] = useState(false);
4945
const [name, setName] = useState("");
5046
const [description, setDescription] = useState("");
51-
const [releaseDate, setReleaseDate] = useState(
52-
formatDateForInput(Date.now()),
53-
);
54-
const [dueDate, setDueDate] = useState(
55-
formatDateForInput(Date.now() + 24 * 60 * 60 * 1000),
56-
);
47+
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange());
48+
5749
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
5850
event.preventDefault();
5951
setIsSubmitting(true);
6052

6153
try {
62-
const parsedReleaseDate = Date.parse(releaseDate);
63-
const parsedDueDate = Date.parse(dueDate);
64-
65-
if (Number.isNaN(parsedReleaseDate) || Number.isNaN(parsedDueDate)) {
66-
throw new Error("Invalid date values");
67-
}
68-
if (parsedDueDate <= parsedReleaseDate) {
69-
throw new Error("Due date must be after release date");
54+
if (!dateRange.from || !dateRange.to) {
55+
throw new Error("Please select a date range");
7056
}
7157
if (!name.trim()) {
7258
throw new Error("Assignment name is required");
7359
}
7460

61+
const releaseDateVal = new Date(dateRange.from);
62+
const dueDateVal = new Date(dateRange.to);
63+
64+
if (dueDateVal.getTime() <= releaseDateVal.getTime()) {
65+
throw new Error("Due date must be after release date");
66+
}
67+
7568
// Upload starter code first (if any), before creating the assignment
7669
if (uploaderRef.current?.hasFiles()) {
7770
storageKeyRef.current = null;
@@ -82,8 +75,8 @@ function NewAssignmentForm({ classroomId }: { classroomId: Id<"classrooms"> }) {
8275
classroomId,
8376
name: name.trim(),
8477
description: description.trim() || undefined,
85-
releaseDate: parsedReleaseDate,
86-
dueDate: parsedDueDate,
78+
releaseDate: releaseDateVal.getTime(),
79+
dueDate: dueDateVal.getTime(),
8780
starterCodeStorageKey: storageKeyRef.current ?? undefined,
8881
});
8982

@@ -134,25 +127,60 @@ function NewAssignmentForm({ classroomId }: { classroomId: Id<"classrooms"> }) {
134127
placeholder="Week 3 - Sorting Algorithms"
135128
/>
136129
</div>
130+
<Label htmlFor="availability-period">Availability Period</Label>
131+
<Calendar
132+
className="w-full"
133+
mode="range"
134+
defaultMonth={dateRange.from}
135+
selected={dateRange}
136+
onSelect={(newDateRange) =>
137+
setDateRange(updateDateRange(dateRange, newDateRange))
138+
}
139+
numberOfMonths={2}
140+
showOutsideDays={false}
141+
required
142+
/>
137143
<div className="grid gap-4 md:grid-cols-2">
138144
<div className="space-y-2">
139-
<Label htmlFor="release-date">Release Date</Label>
145+
<Label htmlFor="release-time">
146+
Availability Start Time / Release Time
147+
</Label>
140148
<Input
141-
id="release-date"
142-
type="datetime-local"
149+
id="release-time"
150+
type="time"
143151
required
144-
value={releaseDate}
145-
onChange={(event) => setReleaseDate(event.target.value)}
152+
// can't figure out a proper way to colour this. Colouring the text
153+
// and background don't work, so we'll settle with inverting the colour
154+
className="[&::-webkit-calendar-picker-indicator]:invert"
155+
value={formatTime(dateRange.from)}
156+
onChange={(event) =>
157+
setDateRange(
158+
updateDateRangeTime(
159+
dateRange,
160+
"from",
161+
event.target.value,
162+
),
163+
)
164+
}
146165
/>
147166
</div>
148167
<div className="space-y-2">
149-
<Label htmlFor="due-date">Due Date</Label>
168+
<Label htmlFor="due-time">
169+
Availability End Time / Due Time
170+
</Label>
150171
<Input
151-
id="due-date"
152-
type="datetime-local"
172+
id="due-time"
173+
type="time"
153174
required
154-
value={dueDate}
155-
onChange={(event) => setDueDate(event.target.value)}
175+
// can't figure out a proper way to colour this. Colouring the text
176+
// and background don't work, so we'll settle with inverting the colour
177+
className="[&::-webkit-calendar-picker-indicator]:invert"
178+
value={formatTime(dateRange.to)}
179+
onChange={(event) =>
180+
setDateRange(
181+
updateDateRangeTime(dateRange, "to", event.target.value),
182+
)
183+
}
156184
/>
157185
</div>
158186
</div>
@@ -226,3 +254,53 @@ export default function NewAssignmentPage({
226254
</main>
227255
);
228256
}
257+
258+
function formatTime(date: Date | undefined) {
259+
if (!date) return "";
260+
return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
261+
}
262+
263+
function defaultDateRange() {
264+
const from = new Date();
265+
const to = new Date(from);
266+
to.setHours(23, 59, 59, 999);
267+
return { from, to };
268+
}
269+
270+
function updateDateRange(
271+
dateRange: DateRange,
272+
newDateRange?: DateRange,
273+
): DateRange {
274+
const newFrom = newDateRange?.from;
275+
newFrom?.setHours(
276+
dateRange.from?.getHours() ?? 0,
277+
dateRange.from?.getMinutes(),
278+
);
279+
const newTo = newDateRange?.to;
280+
if (dateRange.to) {
281+
const hours = dateRange.to.getHours();
282+
const minutes = dateRange.to.getMinutes();
283+
newTo?.setHours(hours, minutes, 59, 999);
284+
}
285+
return { from: newFrom, to: newTo };
286+
}
287+
288+
function updateDateRangeTime(
289+
dateRange: DateRange | undefined,
290+
field: "from" | "to",
291+
time: string,
292+
) {
293+
if (!dateRange) return defaultDateRange();
294+
const currentDate = dateRange[field];
295+
if (!currentDate) return dateRange;
296+
const [hours, minutes] = time.split(":").map(Number) as [number, number];
297+
const newDate = new Date(currentDate);
298+
newDate.setHours(
299+
hours,
300+
minutes,
301+
field === "to" ? 59 : 0,
302+
field === "to" ? 999 : 0,
303+
);
304+
console.log(newDate);
305+
return { ...dateRange, [field]: newDate };
306+
}

packages/backend/convex/_generated/api.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import type * as api_coder from "../api/coder.js";
1212
import type * as api_extension from "../api/extension.js";
1313
import type * as auth from "../auth.js";
14-
import type * as helpers_coder from "../helpers/coder.js";
1514
import type * as helpers_minio from "../helpers/minio.js";
1615
import type * as helpers_roles from "../helpers/roles.js";
1716
import type * as helpers_storageKeys from "../helpers/storageKeys.js";
@@ -37,7 +36,6 @@ declare const fullApi: ApiFromModules<{
3736
"api/coder": typeof api_coder;
3837
"api/extension": typeof api_extension;
3938
auth: typeof auth;
40-
"helpers/coder": typeof helpers_coder;
4139
"helpers/minio": typeof helpers_minio;
4240
"helpers/roles": typeof helpers_roles;
4341
"helpers/storageKeys": typeof helpers_storageKeys;

packages/ui/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"exports": {
66
".": "./src/index.ts",
77
"./button": "./src/button.tsx",
8+
"./calendar": "./src/calendar.tsx",
89
"./card": "./src/card.tsx",
910
"./dialog": "./src/dialog.tsx",
1011
"./dropdown-menu": "./src/dropdown-menu.tsx",
@@ -16,8 +17,7 @@
1617
"./spinner": "./src/spinner.tsx",
1718
"./theme": "./src/theme.tsx",
1819
"./toast": "./src/toast.tsx",
19-
"./input-group": "./src/input-group.tsx",
20-
"./dialog": "./src/dialog.tsx"
20+
"./input-group": "./src/input-group.tsx"
2121
},
2222
"license": "MIT",
2323
"scripts": {
@@ -35,8 +35,10 @@
3535
"@radix-ui/react-label": "^2.1.8",
3636
"@radix-ui/react-slot": "^1.2.4",
3737
"class-variance-authority": "^0.7.1",
38+
"date-fns": "^4.1.0",
3839
"lucide-react": "^0.552.0",
3940
"radix-ui": "^1.4.3",
41+
"react-day-picker": "^9.14.0",
4042
"react-hook-form": "^7.66.0",
4143
"sonner": "^2.0.7",
4244
"tailwind-merge": "^3.3.1"

0 commit comments

Comments
 (0)