11"use client" ;
22
33import type { FormEvent } from "react" ;
4+ import type { DateRange } from "react-day-picker" ;
45import { use , useRef , useState } from "react" ;
56import Link from "next/link" ;
67import { useRouter } from "next/navigation" ;
@@ -11,6 +12,7 @@ import { toast } from "sonner";
1112import type { Id } from "@package/backend/convex/_generated/dataModel" ;
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 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+ }
0 commit comments