Skip to content

Commit 1832c5d

Browse files
Faut faire un onglet comme les notif mais genre reminde
1 parent 0bb1444 commit 1832c5d

File tree

4 files changed

+302
-2
lines changed

4 files changed

+302
-2
lines changed

src/app/(app)/reminders/actions.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
'use server';
3+
4+
import { z } from 'zod';
5+
import { createReminder, getRemindersForUser, deleteReminder } from '@/lib/db';
6+
import { auth } from '@/lib/authEdge';
7+
import { revalidatePath } from 'next/cache';
8+
9+
export async function getRemindersAction() {
10+
const session = await auth();
11+
if (!session?.user?.uuid) {
12+
return { error: 'Authentication required.' };
13+
}
14+
return getRemindersForUser(session.user.uuid);
15+
}
16+
17+
const CreateReminderSchema = z.object({
18+
content: z.string().min(3, "Reminder content must be at least 3 characters.").max(500),
19+
remindAt: z.string().refine((val) => !isNaN(Date.parse(val)), {
20+
message: "Invalid date format.",
21+
}),
22+
});
23+
24+
export async function createReminderAction(
25+
prevState: { success: boolean; error: string | null; fieldErrors?: any },
26+
formData: FormData
27+
) {
28+
const session = await auth();
29+
if (!session?.user?.uuid) {
30+
return { success: false, error: 'You must be logged in to create a reminder.' };
31+
}
32+
33+
const validatedFields = CreateReminderSchema.safeParse({
34+
content: formData.get('content'),
35+
remindAt: formData.get('remindAt'),
36+
});
37+
38+
if (!validatedFields.success) {
39+
return {
40+
success: false,
41+
error: 'Invalid input.',
42+
fieldErrors: validatedFields.error.flatten().fieldErrors,
43+
};
44+
}
45+
46+
try {
47+
await createReminder({
48+
userUuid: session.user.uuid,
49+
content: validatedFields.data.content,
50+
remindAt: new Date(validatedFields.data.remindAt).toISOString(),
51+
});
52+
revalidatePath('/reminders');
53+
return { success: true, error: null };
54+
} catch (error: any) {
55+
return { success: false, error: error.message || 'Failed to create reminder.' };
56+
}
57+
}
58+
59+
export async function deleteReminderAction(uuid: string) {
60+
const session = await auth();
61+
if (!session?.user?.uuid) {
62+
return { success: false, error: 'Permission denied.' };
63+
}
64+
try {
65+
const success = await deleteReminder(uuid, session.user.uuid);
66+
if (success) {
67+
revalidatePath('/reminders');
68+
return { success: true };
69+
}
70+
return { success: false, error: 'Reminder not found or you do not have permission to delete it.' };
71+
} catch (error: any) {
72+
return { success: false, error: error.message || 'Failed to delete reminder.' };
73+
}
74+
}

src/app/(app)/reminders/page.tsx

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
}

src/components/layout/AppSidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
SidebarMenuSkeleton,
1515
} from '@/components/ui/sidebar';
1616
import { Logo } from '@/components/Logo';
17-
import { LayoutDashboard, FolderKanban, Megaphone, Settings, Users, ShieldCheck, BookText, Compass, Lightbulb, MessageSquare, Sparkles } from 'lucide-react';
17+
import { LayoutDashboard, FolderKanban, Megaphone, Settings, Users, ShieldCheck, BookText, Compass, Lightbulb, MessageSquare, Sparkles, CalendarCheck } from 'lucide-react';
1818
import { useAuth } from '@/hooks/useAuth';
1919
import { Button } from '../ui/button';
2020

@@ -26,6 +26,7 @@ const navItems = [
2626
{ href: '/team', label: 'Team', icon: Users, adminOnly: false },
2727
{ href: '/chat', label: 'Chat', icon: MessageSquare },
2828
{ href: '/announcements', label: 'Announcements', icon: Megaphone },
29+
{ href: '/reminders', label: 'Reminders', icon: CalendarCheck },
2930
{ href: '/documentation', label: 'Docs', icon: BookText, adminOnly: false },
3031
{ href: '/suggestions', label: 'Suggestions', icon: Lightbulb, adminOnly: false },
3132
{ href: '/secure-vault', label: 'Secure Vault', icon: ShieldCheck, adminOnly: false },

src/lib/db.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,23 @@ export async function createReminder(data: {
834834
return { ...data, uuid, isTriggered: false, createdAt: now };
835835
}
836836

837+
export async function getRemindersForUser(userUuid: string): Promise<Reminder[]> {
838+
const connection = await getDbConnection();
839+
return connection.all<Reminder[]>(
840+
'SELECT * FROM reminders WHERE userUuid = ? ORDER BY remindAt ASC',
841+
userUuid
842+
);
843+
}
844+
845+
export async function deleteReminder(uuid: string, userUuid: string): Promise<boolean> {
846+
const connection = await getDbConnection();
847+
const result = await connection.run(
848+
'DELETE FROM reminders WHERE uuid = ? AND userUuid = ?',
849+
uuid, userUuid
850+
);
851+
return result.changes ? result.changes > 0 : false;
852+
}
853+
837854
export async function getDueReminders(): Promise<Reminder[]> {
838855
const connection = await getDbConnection();
839856
const now = new Date().toISOString();
@@ -2418,8 +2435,8 @@ export async function getFlowAppConsentsForUser(userUuid: string): Promise<FlowA
24182435
ufac.flowAppUuid,
24192436
fa.name as flowAppName,
24202437
u.name as flowAppOwnerName,
2421-
fa.scopes,
24222438
ufac.status,
2439+
fa.scopes,
24232440
ufac.createdAt,
24242441
ufac.updatedAt
24252442
FROM user_flow_app_consents ufac
@@ -2939,3 +2956,4 @@ export async function getAlbumsForDocument(documentUuid: string): Promise<Pick<D
29392956

29402957

29412958

2959+

0 commit comments

Comments
 (0)