|
| 1 | + |
| 2 | +'use client'; |
| 3 | + |
| 4 | +import { useState, useEffect, useActionState, useTransition } from 'react'; |
| 5 | +import { useAuth } from '@/hooks/useAuth'; |
| 6 | +import { useToast } from '@/hooks/use-toast'; |
| 7 | +import { getRemindersAction, createReminderAction, deleteReminderAction } from './actions'; |
| 8 | +import type { Reminder } from '@/types'; |
| 9 | +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; |
| 10 | +import { Button } from '@/components/ui/button'; |
| 11 | +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger, DialogClose } from "@/components/ui/dialog"; |
| 12 | +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; |
| 13 | +import { Input } from '@/components/ui/input'; |
| 14 | +import { Textarea } from '@/components/ui/textarea'; |
| 15 | +import { Skeleton } from '@/components/ui/skeleton'; |
| 16 | +import { PlusCircle, CalendarCheck, Loader2, Trash2, Bell, XCircle } from 'lucide-react'; |
| 17 | +import { zodResolver } from '@hookform/resolvers/zod'; |
| 18 | +import * as z from 'zod'; |
| 19 | +import { useForm } from 'react-hook-form'; |
| 20 | +import { cn } from '@/lib/utils'; |
| 21 | +import { format, formatDistanceToNow, isPast } from 'date-fns'; |
| 22 | +import { Calendar } from '@/components/ui/calendar'; |
| 23 | +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; |
| 24 | + |
| 25 | + |
| 26 | +const reminderFormSchema = z.object({ |
| 27 | + content: z.string().min(3, "Reminder content must be at least 3 characters.").max(500), |
| 28 | + remindAtDate: z.date({ required_error: "A date is required."}), |
| 29 | + remindAtTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format (HH:mm)."), |
| 30 | +}); |
| 31 | + |
| 32 | +type ReminderFormValues = z.infer<typeof reminderFormSchema>; |
| 33 | + |
| 34 | +export default function RemindersPage() { |
| 35 | + const { user } = useAuth(); |
| 36 | + const { toast } = useToast(); |
| 37 | + const [reminders, setReminders] = useState<Reminder[]>([]); |
| 38 | + const [isLoading, setIsLoading] = useState(true); |
| 39 | + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); |
| 40 | + const [reminderToDelete, setReminderToDelete] = useState<Reminder | null>(null); |
| 41 | + const [isDeleting, startDeleteTransition] = useTransition(); |
| 42 | + |
| 43 | + const [createState, createFormAction, isCreating] = useActionState(createReminderAction, { success: false, error: null }); |
| 44 | + const form = useForm<ReminderFormValues>({ resolver: zodResolver(reminderFormSchema) }); |
| 45 | + |
| 46 | + const loadReminders = async () => { |
| 47 | + setIsLoading(true); |
| 48 | + const result = await getRemindersAction(); |
| 49 | + if ('error' in result) { |
| 50 | + toast({ variant: 'destructive', title: 'Error', description: result.error }); |
| 51 | + } else { |
| 52 | + setReminders(result); |
| 53 | + } |
| 54 | + setIsLoading(false); |
| 55 | + }; |
| 56 | + |
| 57 | + useEffect(() => { |
| 58 | + loadReminders(); |
| 59 | + }, []); |
| 60 | + |
| 61 | + useEffect(() => { |
| 62 | + if (createState.success) { |
| 63 | + toast({ title: "Success!", description: "Your reminder has been set." }); |
| 64 | + setIsCreateDialogOpen(false); |
| 65 | + form.reset(); |
| 66 | + loadReminders(); |
| 67 | + } else if (createState.error) { |
| 68 | + toast({ variant: 'destructive', title: 'Error', description: createState.error }); |
| 69 | + } |
| 70 | + }, [createState, form, toast]); |
| 71 | + |
| 72 | + const handleCreateSubmit = (data: ReminderFormValues) => { |
| 73 | + const [hours, minutes] = data.remindAtTime.split(':').map(Number); |
| 74 | + const combinedDate = new Date(data.remindAtDate); |
| 75 | + combinedDate.setHours(hours, minutes, 0, 0); |
| 76 | + |
| 77 | + const formData = new FormData(); |
| 78 | + formData.append('content', data.content); |
| 79 | + formData.append('remindAt', combinedDate.toISOString()); |
| 80 | + |
| 81 | + createFormAction(formData); |
| 82 | + }; |
| 83 | + |
| 84 | + const handleDeleteReminder = (uuid: string) => { |
| 85 | + startDeleteTransition(async () => { |
| 86 | + const result = await deleteReminderAction(uuid); |
| 87 | + if (result.success) { |
| 88 | + toast({ title: "Reminder Deleted", description: "The reminder has been removed." }); |
| 89 | + setReminderToDelete(null); |
| 90 | + loadReminders(); |
| 91 | + } else { |
| 92 | + toast({ variant: 'destructive', title: 'Error', description: result.error }); |
| 93 | + } |
| 94 | + }); |
| 95 | + } |
| 96 | + |
| 97 | + return ( |
| 98 | + <div className="space-y-6"> |
| 99 | + <div className="flex flex-col sm:flex-row justify-between items-center gap-4"> |
| 100 | + <div> |
| 101 | + <h1 className="text-3xl font-headline font-semibold flex items-center gap-2"> |
| 102 | + <CalendarCheck className="text-primary"/>Reminders |
| 103 | + </h1> |
| 104 | + <p className="text-muted-foreground">Manage your personal reminders and notifications.</p> |
| 105 | + </div> |
| 106 | + <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> |
| 107 | + <DialogTrigger asChild> |
| 108 | + <Button><PlusCircle className="mr-2 h-4 w-4"/> New Reminder</Button> |
| 109 | + </DialogTrigger> |
| 110 | + <DialogContent> |
| 111 | + <DialogHeader> |
| 112 | + <DialogTitle>Set a New Reminder</DialogTitle> |
| 113 | + <DialogDescription>Describe what you want to be reminded of and when.</DialogDescription> |
| 114 | + </DialogHeader> |
| 115 | + <Form {...form}> |
| 116 | + <form onSubmit={form.handleSubmit(handleCreateSubmit)} className="space-y-4"> |
| 117 | + <FormField control={form.control} name="content" render={({ field }) => ( |
| 118 | + <FormItem><FormLabel>Remind me to...</FormLabel><FormControl><Textarea {...field} placeholder="e.g., Follow up with the design team about mockups" /></FormControl><FormMessage /></FormItem> |
| 119 | + )}/> |
| 120 | + <div className="grid grid-cols-2 gap-4"> |
| 121 | + <FormField control={form.control} name="remindAtDate" render={({ field }) => ( |
| 122 | + <FormItem className="flex flex-col"><FormLabel>Date</FormLabel> |
| 123 | + <Popover><PopoverTrigger asChild> |
| 124 | + <FormControl> |
| 125 | + <Button variant="outline" className={cn("pl-3 text-left font-normal", !field.value && "text-muted-foreground")}> |
| 126 | + {field.value ? format(field.value, "PPP") : <span>Pick a date</span>} |
| 127 | + </Button> |
| 128 | + </FormControl> |
| 129 | + </PopoverTrigger> |
| 130 | + <PopoverContent className="w-auto p-0" align="start"> |
| 131 | + <Calendar mode="single" selected={field.value} onSelect={field.onChange} disabled={(date) => date < new Date()} initialFocus /> |
| 132 | + </PopoverContent> |
| 133 | + </Popover> |
| 134 | + <FormMessage /> |
| 135 | + </FormItem> |
| 136 | + )}/> |
| 137 | + <FormField control={form.control} name="remindAtTime" render={({ field }) => ( |
| 138 | + <FormItem><FormLabel>Time (HH:mm)</FormLabel><FormControl><Input {...field} type="time" /></FormControl><FormMessage /></FormItem> |
| 139 | + )}/> |
| 140 | + </div> |
| 141 | + <DialogFooter> |
| 142 | + <DialogClose asChild><Button type="button" variant="ghost" disabled={isCreating}>Cancel</Button></DialogClose> |
| 143 | + <Button type="submit" disabled={isCreating}>{isCreating && <Loader2 className="mr-2 h-4 w-4 animate-spin"/>} Set Reminder</Button> |
| 144 | + </DialogFooter> |
| 145 | + </form> |
| 146 | + </Form> |
| 147 | + </DialogContent> |
| 148 | + </Dialog> |
| 149 | + </div> |
| 150 | + |
| 151 | + <Card> |
| 152 | + <CardHeader> |
| 153 | + <CardTitle>Your Upcoming Reminders</CardTitle> |
| 154 | + </CardHeader> |
| 155 | + <CardContent> |
| 156 | + {isLoading ? ( |
| 157 | + <div className="space-y-3"> |
| 158 | + {[...Array(3)].map((_, i) => <Skeleton key={i} className="h-16 w-full"/>)} |
| 159 | + </div> |
| 160 | + ) : reminders.length === 0 ? ( |
| 161 | + <div className="text-center py-12 text-muted-foreground border-2 border-dashed rounded-lg"> |
| 162 | + <Bell className="mx-auto h-12 w-12 mb-4"/> |
| 163 | + <h3 className="text-lg font-medium">No Reminders Set</h3> |
| 164 | + <p>Click "New Reminder" to create one.</p> |
| 165 | + </div> |
| 166 | + ) : ( |
| 167 | + <div className="space-y-3"> |
| 168 | + {reminders.map(r => ( |
| 169 | + <Card key={r.uuid} className={cn("p-3 flex justify-between items-center gap-4", isPast(new Date(r.remindAt)) && "opacity-50 bg-muted/50")}> |
| 170 | + <div className="flex-grow"> |
| 171 | + <p className="font-medium">{r.content}</p> |
| 172 | + <p className={cn("text-sm", isPast(new Date(r.remindAt)) ? "text-muted-foreground" : "text-primary")}> |
| 173 | + {isPast(new Date(r.remindAt)) ? 'Due ' : 'Due in '} |
| 174 | + {formatDistanceToNow(new Date(r.remindAt), { addSuffix: true })} |
| 175 | + <span className="text-muted-foreground text-xs"> ({format(new Date(r.remindAt), 'PPp')})</span> |
| 176 | + </p> |
| 177 | + </div> |
| 178 | + <Dialog> |
| 179 | + <DialogTrigger asChild> |
| 180 | + <Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setReminderToDelete(r)}> |
| 181 | + <Trash2 className="h-4 w-4"/> |
| 182 | + </Button> |
| 183 | + </DialogTrigger> |
| 184 | + {reminderToDelete?.uuid === r.uuid && ( |
| 185 | + <DialogContent> |
| 186 | + <DialogHeader> |
| 187 | + <DialogTitle>Delete Reminder?</DialogTitle> |
| 188 | + <DialogDescription>This action cannot be undone. Are you sure you want to delete this reminder?</DialogDescription> |
| 189 | + </DialogHeader> |
| 190 | + <DialogFooter> |
| 191 | + <DialogClose asChild><Button variant="ghost">Cancel</Button></DialogClose> |
| 192 | + <Button variant="destructive" onClick={() => handleDeleteReminder(r.uuid)} disabled={isDeleting}> |
| 193 | + {isDeleting && <Loader2 className="h-4 w-4 animate-spin mr-2"/>} Delete |
| 194 | + </Button> |
| 195 | + </DialogFooter> |
| 196 | + </DialogContent> |
| 197 | + )} |
| 198 | + </Dialog> |
| 199 | + </Card> |
| 200 | + ))} |
| 201 | + </div> |
| 202 | + )} |
| 203 | + </CardContent> |
| 204 | + </Card> |
| 205 | + </div> |
| 206 | + ); |
| 207 | +} |
0 commit comments