Skip to content

Commit 2bb4ec4

Browse files
committed
refactor(web): extract DatePickerWithRange component from assignment form
1 parent 17e7fec commit 2bb4ec4

File tree

3 files changed

+401
-106
lines changed

3 files changed

+401
-106
lines changed

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

Lines changed: 8 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { toast } from "sonner";
1212
import type { Id } from "@package/backend/convex/_generated/dataModel";
1313
import { api } from "@package/backend/convex/_generated/api";
1414
import { Button } from "@package/ui/button";
15-
import { Calendar } from "@package/ui/calendar";
1615
import {
1716
Card,
1817
CardContent,
@@ -25,6 +24,10 @@ import { Label } from "@package/ui/label";
2524
import { Spinner } from "@package/ui/spinner";
2625

2726
import type { StarterCodeUploaderHandle } from "~/components/starter-code-uploader";
27+
import {
28+
DatePickerWithRange,
29+
defaultDateRange,
30+
} from "~/components/date-picker-with-range";
2831
import { Editor } from "~/components/editor";
2932
import { StarterCodeUploader } from "~/components/starter-code-uploader";
3033
import { Authenticated, AuthLoading, Unauthenticated } from "~/lib/auth";
@@ -127,63 +130,12 @@ function NewAssignmentForm({ classroomId }: { classroomId: Id<"classrooms"> }) {
127130
placeholder="Week 3 - Sorting Algorithms"
128131
/>
129132
</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}
133+
<DatePickerWithRange
134+
className="space-y-4"
135+
date={dateRange}
136+
onDateChange={setDateRange}
141137
required
142138
/>
143-
<div className="grid gap-4 md:grid-cols-2">
144-
<div className="space-y-2">
145-
<Label htmlFor="release-time">
146-
Availability Start Time / Release Time
147-
</Label>
148-
<Input
149-
id="release-time"
150-
type="time"
151-
required
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-
}
165-
/>
166-
</div>
167-
<div className="space-y-2">
168-
<Label htmlFor="due-time">
169-
Availability End Time / Due Time
170-
</Label>
171-
<Input
172-
id="due-time"
173-
type="time"
174-
required
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-
}
184-
/>
185-
</div>
186-
</div>
187139
<div className="space-y-2">
188140
<Label htmlFor="assignment-description">Description</Label>
189141
<Editor
@@ -254,53 +206,3 @@ export default function NewAssignmentPage({
254206
</main>
255207
);
256208
}
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-
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import type { DateRange } from "react-day-picker";
2+
import { describe, expect, it } from "vitest";
3+
4+
import {
5+
defaultDateRange,
6+
formatTime,
7+
updateDateRange,
8+
updateDateRangeTime,
9+
} from "./date-picker-with-range";
10+
11+
describe("DatePickerWithRange", () => {
12+
describe("defaultDateRange", () => {
13+
it("should return a DateRange with from and to dates", () => {
14+
const result = defaultDateRange();
15+
16+
expect(result.from).toBeInstanceOf(Date);
17+
expect(result.to).toBeInstanceOf(Date);
18+
});
19+
20+
it("should set 'to' date to 7 days after 'from' date", () => {
21+
const result = defaultDateRange();
22+
23+
const fromDateOnly = new Date(result.from);
24+
fromDateOnly.setHours(0, 0, 0, 0);
25+
const toDateOnly = new Date(result.to);
26+
toDateOnly.setHours(0, 0, 0, 0);
27+
28+
const fromMs = fromDateOnly.getTime();
29+
const toMs = toDateOnly.getTime();
30+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
31+
32+
expect(toMs - fromMs).toBe(sevenDaysMs);
33+
});
34+
35+
it("should set 'to' time to 23:59:59.999", () => {
36+
const result = defaultDateRange();
37+
38+
expect(result.to.getHours()).toBe(23);
39+
expect(result.to.getMinutes()).toBe(59);
40+
expect(result.to.getSeconds()).toBe(59);
41+
expect(result.to.getMilliseconds()).toBe(999);
42+
});
43+
});
44+
45+
describe("formatTime", () => {
46+
it("should return empty string for undefined", () => {
47+
expect(formatTime(undefined)).toBe("");
48+
});
49+
50+
it("should return formatted time with leading zeros", () => {
51+
const date = new Date();
52+
date.setHours(9, 5, 0, 0);
53+
54+
expect(formatTime(date)).toBe("09:05");
55+
});
56+
57+
it("should handle double-digit hours and minutes", () => {
58+
const date = new Date();
59+
date.setHours(14, 30, 0, 0);
60+
61+
expect(formatTime(date)).toBe("14:30");
62+
});
63+
64+
it("should handle zero values", () => {
65+
const date = new Date();
66+
date.setHours(0, 0, 0, 0);
67+
68+
expect(formatTime(date)).toBe("00:00");
69+
});
70+
});
71+
72+
describe("updateDateRangeTime", () => {
73+
it("should return defaultDateRange when dateRange is undefined", () => {
74+
const result = updateDateRangeTime(undefined, "from", "10:30");
75+
76+
expect(result.from).toBeInstanceOf(Date);
77+
expect(result.to).toBeInstanceOf(Date);
78+
});
79+
80+
it("should update the 'from' time", () => {
81+
const dateRange: DateRange = {
82+
from: new Date(2024, 0, 1, 8, 0),
83+
to: new Date(2024, 0, 8, 23, 59),
84+
};
85+
86+
const result = updateDateRangeTime(dateRange, "from", "14:30");
87+
88+
expect(result.from?.getHours()).toBe(14);
89+
expect(result.from?.getMinutes()).toBe(30);
90+
});
91+
92+
it("should update the 'to' time and set seconds/milliseconds for end of day", () => {
93+
const dateRange: DateRange = {
94+
from: new Date(2024, 0, 1, 8, 0),
95+
to: new Date(2024, 0, 8, 23, 59),
96+
};
97+
98+
const result = updateDateRangeTime(dateRange, "to", "23:45");
99+
100+
expect(result.to?.getHours()).toBe(23);
101+
expect(result.to?.getMinutes()).toBe(45);
102+
expect(result.to?.getSeconds()).toBe(59);
103+
expect(result.to?.getMilliseconds()).toBe(999);
104+
});
105+
106+
it("should preserve the date portion", () => {
107+
const dateRange: DateRange = {
108+
from: new Date(2024, 5, 15, 8, 0),
109+
to: new Date(2024, 5, 22, 23, 59),
110+
};
111+
112+
const result = updateDateRangeTime(dateRange, "from", "12:00");
113+
114+
expect(result.from?.getFullYear()).toBe(2024);
115+
expect(result.from?.getMonth()).toBe(5);
116+
expect(result.from?.getDate()).toBe(15);
117+
});
118+
119+
it("should return original dateRange when field date is undefined", () => {
120+
const dateRange: DateRange = {
121+
from: undefined,
122+
to: new Date(2024, 0, 8, 23, 59),
123+
};
124+
125+
const result = updateDateRangeTime(dateRange, "from", "10:00");
126+
127+
expect(result.from).toBeUndefined();
128+
expect(result.to).toBe(dateRange.to);
129+
});
130+
});
131+
132+
describe("updateDateRange", () => {
133+
it("should return empty DateRange when newDateRange is undefined", () => {
134+
const dateRange: DateRange = {
135+
from: new Date(2024, 0, 1, 8, 0),
136+
to: new Date(2024, 0, 8, 23, 59),
137+
};
138+
139+
const result = updateDateRange(dateRange, undefined);
140+
141+
expect(result.from).toBe(dateRange.from);
142+
expect(result.to).toBe(dateRange.to);
143+
});
144+
145+
it("should preserve time from original dateRange 'from'", () => {
146+
const dateRange: DateRange = {
147+
from: new Date(2024, 0, 1, 10, 30),
148+
to: new Date(2024, 0, 8, 23, 59),
149+
};
150+
151+
const newDateRange: DateRange = {
152+
from: new Date(2024, 1, 15),
153+
to: new Date(2024, 1, 20),
154+
};
155+
156+
const result = updateDateRange(dateRange, newDateRange);
157+
158+
expect(result.from?.getHours()).toBe(10);
159+
expect(result.from?.getMinutes()).toBe(30);
160+
expect(result.from?.getDate()).toBe(15);
161+
expect(result.from?.getMonth()).toBe(1);
162+
});
163+
164+
it("should preserve time from original dateRange 'to'", () => {
165+
const dateRange: DateRange = {
166+
from: new Date(2024, 0, 1, 8, 0),
167+
to: new Date(2024, 0, 8, 14, 45),
168+
};
169+
170+
const newDateRange: DateRange = {
171+
from: new Date(2024, 1, 15),
172+
to: new Date(2024, 1, 20),
173+
};
174+
175+
const result = updateDateRange(dateRange, newDateRange);
176+
177+
expect(result.to?.getHours()).toBe(14);
178+
expect(result.to?.getMinutes()).toBe(45);
179+
expect(result.to?.getSeconds()).toBe(59);
180+
expect(result.to?.getMilliseconds()).toBe(999);
181+
expect(result.to?.getDate()).toBe(20);
182+
expect(result.to?.getMonth()).toBe(1);
183+
});
184+
185+
it("should use default hours when original dateRange has no time", () => {
186+
const dateRange: DateRange = {
187+
from: new Date(2024, 0, 1),
188+
to: new Date(2024, 0, 8),
189+
};
190+
191+
const newDateRange: DateRange = {
192+
from: new Date(2024, 1, 15),
193+
to: new Date(2024, 1, 20),
194+
};
195+
196+
const result = updateDateRange(dateRange, newDateRange);
197+
198+
expect(result.from?.getHours()).toBe(0);
199+
expect(result.from?.getMinutes()).toBe(0);
200+
});
201+
202+
it("should preserve year from new dateRange", () => {
203+
const dateRange: DateRange = {
204+
from: new Date(2024, 0, 1, 8, 0),
205+
to: new Date(2024, 0, 8, 23, 59),
206+
};
207+
208+
const newDateRange: DateRange = {
209+
from: new Date(2025, 5, 15),
210+
to: new Date(2025, 5, 20),
211+
};
212+
213+
const result = updateDateRange(dateRange, newDateRange);
214+
215+
expect(result.from?.getFullYear()).toBe(2025);
216+
expect(result.to?.getFullYear()).toBe(2025);
217+
});
218+
219+
it("should handle newDateRange with only 'from' set", () => {
220+
const dateRange: DateRange = {
221+
from: new Date(2024, 0, 1, 8, 0),
222+
to: new Date(2024, 0, 8, 23, 59),
223+
};
224+
225+
const newDateRange: DateRange = {
226+
from: new Date(2024, 1, 15),
227+
to: undefined,
228+
};
229+
230+
const result = updateDateRange(dateRange, newDateRange);
231+
232+
expect(result.from?.getDate()).toBe(15);
233+
expect(result.from?.getMonth()).toBe(1);
234+
expect(result.to).toBeUndefined();
235+
});
236+
237+
it("should return original dateRange when both have same dates but preserve time", () => {
238+
const dateRange: DateRange = {
239+
from: new Date(2024, 0, 1, 10, 30),
240+
to: new Date(2024, 0, 8, 14, 45),
241+
};
242+
243+
const newDateRange: DateRange = {
244+
from: new Date(2024, 0, 1),
245+
to: new Date(2024, 0, 8),
246+
};
247+
248+
const result = updateDateRange(dateRange, newDateRange);
249+
250+
expect(result.from?.getHours()).toBe(10);
251+
expect(result.from?.getMinutes()).toBe(30);
252+
expect(result.to?.getHours()).toBe(14);
253+
expect(result.to?.getMinutes()).toBe(45);
254+
expect(result.to?.getSeconds()).toBe(59);
255+
expect(result.to?.getMilliseconds()).toBe(999);
256+
});
257+
});
258+
});

0 commit comments

Comments
 (0)