@@ -9,8 +9,10 @@ import { ArrowLeft } from "lucide-react";
99import { toast } from "sonner" ;
1010
1111import type { Id } from "@package/backend/convex/_generated/dataModel" ;
12+ import type { DateRange } from "@package/ui/calendar" ;
1213import { api } from "@package/backend/convex/_generated/api" ;
1314import { Button } from "@package/ui/button" ;
15+ import { Calendar } from "@package/ui/calendar" ;
1416import {
1517 Card ,
1618 CardContent ,
@@ -27,12 +29,6 @@ import { Editor } from "~/components/editor";
2729import { StarterCodeUploader } from "~/components/starter-code-uploader" ;
2830import { 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-
3632function 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 aria-label = "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,66 @@ 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+ export function updateDateRange (
271+ dateRange : DateRange ,
272+ newDateRange ?: DateRange ,
273+ ) : DateRange {
274+ if ( ! newDateRange ) {
275+ return dateRange ;
276+ }
277+
278+ let newFrom = undefined ;
279+ if ( newDateRange . from ) {
280+ newFrom = new Date ( newDateRange . from ) ;
281+ newFrom . setHours (
282+ dateRange . from ?. getHours ( ) ?? 0 ,
283+ dateRange . from ?. getMinutes ( ) ?? 0 ,
284+ ) ;
285+ }
286+
287+ let newTo = undefined ;
288+ if ( newDateRange . to ) {
289+ newTo = new Date ( newDateRange . to ) ;
290+ newTo . setHours (
291+ dateRange . to ?. getHours ( ) ?? 0 ,
292+ dateRange . to ?. getMinutes ( ) ?? 0 ,
293+ 59 ,
294+ 999 ,
295+ ) ;
296+ }
297+
298+ return { from : newFrom , to : newTo } ;
299+ }
300+
301+ function updateDateRangeTime (
302+ dateRange : DateRange | undefined ,
303+ field : "from" | "to" ,
304+ time : string ,
305+ ) {
306+ if ( ! dateRange ) return defaultDateRange ( ) ;
307+ const currentDate = dateRange [ field ] ;
308+ if ( ! currentDate ) return dateRange ;
309+ const [ hours , minutes ] = time . split ( ":" ) . map ( Number ) as [ number , number ] ;
310+ const newDate = new Date ( currentDate ) ;
311+ newDate . setHours (
312+ hours ,
313+ minutes ,
314+ field === "to" ? 59 : 0 ,
315+ field === "to" ? 999 : 0 ,
316+ ) ;
317+ console . log ( newDate ) ;
318+ return { ...dateRange , [ field ] : newDate } ;
319+ }
0 commit comments