From 8e0430289e08ec5219a9dc49d614c382127c5a48 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Wed, 8 Oct 2025 21:15:54 +0200 Subject: [PATCH 1/8] Safe area top fix - Fixed the 50px top safe area --- src/components/ui/sonner.tsx | 117 +++++++++++++++++++---------------- src/styles/globals.css | 2 +- 2 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 5f2c25d..0c27d79 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,67 +1,74 @@ -"use client" +"use client"; -import { useTheme } from "@/components/theme-provider" -import { Toaster as Sonner } from "sonner" +import { useTheme } from "@/components/theme-provider"; +import { Toaster as Sonner } from "sonner"; -type ToasterProps = React.ComponentProps +type ToasterProps = React.ComponentProps; -type OffsetObject = Exclude +type OffsetObject = Exclude; const SAFE_AREA_OFFSET: OffsetObject = { - top: "calc(var(--safe-area-top, 50px) + 1rem)", - right: "calc(var(--safe-area-right, 0px) + 1rem)", - bottom: "calc(var(--safe-area-bottom, 30px) + 1rem)", - left: "calc(var(--safe-area-left, 0px) + 1rem)", -} + top: "calc(var(--safe-area-top, 50px) + 1rem)", + right: "calc(var(--safe-area-right, 0px) + 1rem)", + bottom: "calc(var(--safe-area-bottom, 30px) + 1rem)", + left: "calc(var(--safe-area-left, 0px) + 1rem)", +}; const mergeClassName = (base: string, extra?: string) => - [base, extra].filter(Boolean).join(" ") + [base, extra].filter(Boolean).join(" "); -const resolveOffset = (value?: ToasterProps["offset"]): ToasterProps["offset"] => { - if (!value) return { ...SAFE_AREA_OFFSET } - if (typeof value === "number" || typeof value === "string") return value - return { - ...SAFE_AREA_OFFSET, - ...value, - } -} +const resolveOffset = ( + value?: ToasterProps["offset"] +): ToasterProps["offset"] => { + if (!value) return { ...SAFE_AREA_OFFSET }; + if (typeof value === "number" || typeof value === "string") return value; + return { + ...SAFE_AREA_OFFSET, + ...value, + }; +}; -const Toaster = ({ toastOptions, offset, mobileOffset, ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() +const Toaster = ({ + toastOptions, + offset, + mobileOffset, + ...props +}: ToasterProps) => { + const { theme = "system" } = useTheme(); - const mergedToastOptions: ToasterProps["toastOptions"] = { - ...toastOptions, - classNames: { - ...toastOptions?.classNames, - toast: mergeClassName( - "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", - toastOptions?.classNames?.toast - ), - description: mergeClassName( - "group-[.toast]:text-muted-foreground", - toastOptions?.classNames?.description - ), - actionButton: mergeClassName( - "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", - toastOptions?.classNames?.actionButton - ), - cancelButton: mergeClassName( - "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", - toastOptions?.classNames?.cancelButton - ), - }, - } + const mergedToastOptions: ToasterProps["toastOptions"] = { + ...toastOptions, + classNames: { + ...toastOptions?.classNames, + toast: mergeClassName( + "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", + toastOptions?.classNames?.toast + ), + description: mergeClassName( + "group-[.toast]:text-muted-foreground", + toastOptions?.classNames?.description + ), + actionButton: mergeClassName( + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", + toastOptions?.classNames?.actionButton + ), + cancelButton: mergeClassName( + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + toastOptions?.classNames?.cancelButton + ), + }, + }; - return ( - - ) -} + return ( + + ); +}; -export { Toaster } +export { Toaster }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 7338ecb..3815e56 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -22,7 +22,7 @@ overscroll-behavior: none; } :root { - --safe-area-top: 40px; + --safe-area-top: 50px; --safe-area-bottom: 30px; --safe-area-left: env(safe-area-inset-left, 0px); --safe-area-right: env(safe-area-inset-right, 0px); From cef679f288fd1227a0b550d2d848992bd795d271 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Wed, 8 Oct 2025 21:48:15 +0200 Subject: [PATCH 2/8] Exit cross fix - Fixed the sidebar exit cross position --- src/components/sidebar.tsx | 11 +++++++++-- src/components/ui/sheet.tsx | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 1ed8043..9d67acf 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, type CSSProperties } from "react"; import { Moon, Sun, MoonStar, Menu, HeartHandshake, BadgeX, ThumbsDown, Book, Printer, MailQuestionMark, ImageUpscale, ArrowDownRightFromSquare, Languages, @@ -74,7 +74,14 @@ export default function Sidebar() { {t("sidebar.title")} diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 52128ab..9f45f03 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -80,8 +80,13 @@ const SheetContent = React.forwardRef< {...props} > {children} - - + + From e603e9f3ecc21f699e5d1868fbf443a0153e1778 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Wed, 8 Oct 2025 22:14:15 +0200 Subject: [PATCH 3/8] Hotfix --- src/components/sidebar.tsx | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index d1c18bb..842fbcb 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -187,19 +187,6 @@ export default function Sidebar() { const navigate = useNavigate(); - - - {t("sidebar.title")} const handleNavigate = (path: string) => { setOpen(false); navigate(path); @@ -232,7 +219,14 @@ export default function Sidebar() { {t("sidebar.title")} From 8d52f0ab0cd4366a9d2fa2c319be4c0a3e4567ce Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Tue, 14 Oct 2025 09:07:10 +0200 Subject: [PATCH 4/8] Agenda and planning creation fix - Fixed the task and event confirm button not working properly --- src/components/drawer-event-task.tsx | 163 +++++++++++++++++++++------ 1 file changed, 127 insertions(+), 36 deletions(-) diff --git a/src/components/drawer-event-task.tsx b/src/components/drawer-event-task.tsx index c5200c4..06da045 100644 --- a/src/components/drawer-event-task.tsx +++ b/src/components/drawer-event-task.tsx @@ -11,7 +11,7 @@ import { import { Label } from "./ui/label"; import { Input } from "./ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { TimePicker } from "./ui/time-picker/time-picker"; import { saveTaskToLocalStorage } from "@/lib/utils/agenda"; import { Lesson } from "@/types/aurion"; @@ -20,6 +20,43 @@ import { toast } from "sonner"; import { format } from "date-fns"; import { useTranslation } from 'react-i18next'; +const combineDateAndTime = ( + targetDate: Date | undefined, + time: Date | undefined +): Date | undefined => { + if (!targetDate || !time) return undefined; + + return new Date( + targetDate.getFullYear(), + targetDate.getMonth(), + targetDate.getDate(), + time.getHours(), + time.getMinutes(), + time.getSeconds(), + time.getMilliseconds() + ); +}; + +const createDefaultDateTimes = () => { + const start = new Date(); + start.setHours(start.getHours() + 1); + + const date = new Date( + start.getFullYear(), + start.getMonth(), + start.getDate() + ); + + const end = new Date(start); + end.setHours(end.getHours() + 1); + + return { + date, + startTime: start, + endTime: end, + }; +}; + export function DrawerEventTask({ type, onClose, @@ -30,35 +67,59 @@ export function DrawerEventTask({ const [open, setOpen] = useState(false); const { t } = useTranslation(); + const defaultDateTimesRef = useRef(createDefaultDateTimes()); + const [title, setTitle] = useState(""); - const [date, setDate] = useState(new Date()); - const [startTime, setStartTime] = useState(new Date()); - const [endTime, setEndTime] = useState(new Date()); + const [date, setDate] = useState( + defaultDateTimesRef.current.date + ); + const [startTime, setStartTime] = useState( + defaultDateTimesRef.current.startTime + ); + const [endTime, setEndTime] = useState( + defaultDateTimesRef.current.endTime + ); + + const resetForm = useCallback(() => { + const defaults = createDefaultDateTimes(); + defaultDateTimesRef.current = defaults; + setTitle(""); + setDate(defaults.date); + setStartTime(defaults.startTime); + setEndTime(defaults.endTime); + }, []); + + useEffect(() => { + if (open) { + resetForm(); + } + }, [open, resetForm]); const handleValidate = () => { - if (!title || !date || !startTime || (type === "event" && !endTime)) + const now = new Date(); + const startDateTime = combineDateAndTime(date, startTime); + const endDateTime = combineDateAndTime(date, endTime); + + if (!title || !startDateTime || (type === "event" && !endDateTime)) + return; + + if (startDateTime.getTime() < now.getTime()) return; + if ( + type === "event" && + endDateTime && + endDateTime.getTime() <= startDateTime.getTime() + ) return; + switch (type) { case "event": { - if (!endTime) return; + if (!endDateTime) return; const newUserEvent: Lesson = { id: crypto.randomUUID(), title: title, - start: new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - startTime.getHours(), - startTime.getMinutes() - ).toISOString(), - end: new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - endTime.getHours(), - endTime.getMinutes() - ).toISOString(), + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), allDay: false, editable: true, className: "est-perso", @@ -76,7 +137,7 @@ export function DrawerEventTask({ task: { id: crypto.randomUUID(), task: title, - date: new Date(startTime), + date: startDateTime, }, }); toast.success("Tâche ajoutée", { @@ -94,12 +155,24 @@ export function DrawerEventTask({ const handleClose = () => { onClose(); setOpen(false); - setTitle(""); - setDate(new Date()); - setStartTime(new Date()); - setEndTime(new Date()); + resetForm(); }; + const now = new Date(); + const startDateTimeForValidation = combineDateAndTime(date, startTime); + const endDateTimeForValidation = combineDateAndTime(date, endTime); + const isEventDisabled = + type === "event" && + (!startDateTimeForValidation || + !endDateTimeForValidation || + endDateTimeForValidation.getTime() <= + startDateTimeForValidation.getTime() || + startDateTimeForValidation.getTime() < now.getTime()); + const isTaskDisabled = + type === "task" && + (!startDateTimeForValidation || + startDateTimeForValidation.getTime() < now.getTime()); + return ( @@ -134,24 +207,28 @@ export function DrawerEventTask({
setDate(date)} + value={date} + onChange={setDate} />
{type === "task" ? ( setStartTime(time)} + value={startTime} + onChange={setStartTime} /> ) : ( <> setStartTime(time)} + value={startTime} + onChange={setStartTime} /> setEndTime(time)} + value={endTime} + onChange={setEndTime} /> )} @@ -165,10 +242,8 @@ export function DrawerEventTask({ !title || !date || !startTime || - (endTime && - (startTime.getTime() === endTime.getTime() || - startTime.getTime() > endTime.getTime())) || - (type === "event" && !endTime) + isEventDisabled || + isTaskDisabled } > {type === "event" @@ -182,14 +257,22 @@ export function DrawerEventTask({ } const DatePickerComponent = ({ + value, onChange, }: { + value: Date | undefined; onChange: (date: Date | undefined) => void; }) => { - const [date, setDate] = useState(new Date()); + const [date, setDate] = useState(() => + value ? new Date(value) : undefined + ); const [open, setOpen] = useState(false); const { t } = useTranslation(); + useEffect(() => { + setDate(value ? new Date(value) : undefined); + }, [value]); + const handleDateChange = (selectedDate: Date | undefined) => { setDate(selectedDate); onChange(selectedDate); @@ -233,12 +316,20 @@ const DatePickerComponent = ({ const TimePickerComponent = ({ label, + value, onChange, }: { label: string; - onChange: (time: Date) => void; + value: Date | undefined; + onChange: (time: Date | undefined) => void; }) => { - const [time, setTime] = useState(new Date()); + const [time, setTime] = useState(() => + value ? new Date(value) : new Date() + ); + + useEffect(() => { + setTime(value ? new Date(value) : new Date()); + }, [value]); const handleTimeChange = (newTime: Date | undefined) => { if (!newTime) return; From 78eba078960addff6a06f66b0df2b224214e4072 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Tue, 14 Oct 2025 09:26:28 +0200 Subject: [PATCH 5/8] Undo agenda - Added an undo when checking a task in the agenda --- src/locales/en-US.json | 4 +- src/locales/es-ES.json | 4 +- src/locales/fr-FR.json | 4 +- src/pages/secondary/agenda.tsx | 190 ++++++++++++++++++++++++--------- src/styles/globals.css | 26 +++++ 5 files changed, 177 insertions(+), 51 deletions(-) diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 594edab..6d08b3d 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -135,7 +135,9 @@ "taskDescription": "Enter the title", "date": "Date", "hour": "Hour", - "createTaskBtn": "Create task" + "createTaskBtn": "Create task", + "markTaskDone": "Mark as done", + "undoTaskCompletion": "Undo completion" }, "associationsPage": { "search": "Find an association", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index fb034af..3f8f714 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -135,7 +135,9 @@ "taskDescription": "Introduce el título", "date": "Fecha", "hour": "Hora", - "createTaskBtn": "Crear tarea" + "createTaskBtn": "Crear tarea", + "markTaskDone": "Marcar como completada", + "undoTaskCompletion": "Deshacer la finalización" }, "associationsPage": { "search": "Buscar una asociación", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index ddf7a38..86f1d6d 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -135,7 +135,9 @@ "taskDescription": "Entrez le titre", "date": "Date", "hour": "Heure", - "createTaskBtn": "Créer la tâche" + "createTaskBtn": "Créer la tâche", + "markTaskDone": "Marquer comme effectuée", + "undoTaskCompletion": "Annuler la complétion" }, "associationsPage": { "search": "Rechercher une association", diff --git a/src/pages/secondary/agenda.tsx b/src/pages/secondary/agenda.tsx index bb9d5db..f341afc 100644 --- a/src/pages/secondary/agenda.tsx +++ b/src/pages/secondary/agenda.tsx @@ -7,25 +7,81 @@ import { import { TaskData } from "@/types/data"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; -import { Calendar, Check, ClipboardListIcon } from "lucide-react"; -import { useState } from "react"; -import { useTranslation } from 'react-i18next'; +import { Calendar, Check, ClipboardListIcon, Undo2 } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils/cn"; export function AgendaPage() { - const [tasks, setTasks] = useState(getTasksFromLocalStorage()); + const [tasks, setTasks] = useState(() => + getTasksFromLocalStorage() + ); + const [pendingTaskIds, setPendingTaskIds] = useState< + Record + >({}); + const pendingTimeoutsRef = useRef< + Map> + >(new Map()); const { t } = useTranslation(); - const handleTaskComplete = (taskId: string) => { - removeTaskFromLocalStorage({ taskId }); + const refreshTasks = useCallback(() => { setTasks(getTasksFromLocalStorage()); + }, [setTasks]); + + const finalizeTaskCompletion = useCallback( + (taskId: string) => { + removeTaskFromLocalStorage({ taskId }); + refreshTasks(); + pendingTimeoutsRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + }, + [refreshTasks] + ); + + const handleTaskCompletionToggle = (taskId: string) => { + const isPending = Boolean(pendingTaskIds[taskId]); + + if (isPending) { + const existingTimeout = pendingTimeoutsRef.current.get(taskId); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + pendingTimeoutsRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + return; + } + + setPendingTaskIds((prev) => ({ ...prev, [taskId]: true })); + const timeoutId = setTimeout( + () => finalizeTaskCompletion(taskId), + 5000 + ); + pendingTimeoutsRef.current.set(taskId, timeoutId); }; + useEffect(() => { + return () => { + pendingTimeoutsRef.current.forEach((timeoutId) => + clearTimeout(timeoutId) + ); + pendingTimeoutsRef.current.clear(); + }; + }, []); + return (
{/* Header Section */}
-

{t("agendaPage.title")}

+

+ {t("agendaPage.title")} +

@@ -39,46 +95,89 @@ export function AgendaPage() { {/* Tasks List */} {tasks.length > 0 ? (
- {tasks.map((task: TaskData, index: number) => ( -
-
-
-
-

- {task.task} -

-
-
- - - {format( - task.date, - "EEEE d MMM p", - { locale: fr } - )} - + {tasks.map((task: TaskData) => { + const isPending = Boolean(pendingTaskIds[task.id]); + return ( +
+
+
+
+

+ {task.task} +

+
+
+ + + {format( + task.date, + "EEEE d MMM p", + { locale: fr } + )} + +
-
-
- +
+ +
-
- ))} + ); + })}
) : (
@@ -96,12 +195,7 @@ export function AgendaPage() {
)}
- { - setTasks(getTasksFromLocalStorage()); - }} - /> +
); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 7338ecb..53f4b65 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -463,6 +463,32 @@ } } +@keyframes task-complete-fade { + 0% { + opacity: 1; + transform: translateY(0); + } + 70% { + opacity: 0.9; + transform: translateY(-2px); + } + 90% { + opacity: 0.4; + transform: translateY(-8px); + } + 100% { + opacity: 0; + transform: translateY(-12px); + } +} + +@layer utilities { + .animate-task-complete { + animation: task-complete-fade 5s ease-in-out forwards; + will-change: opacity, transform; + } +} + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still From 60e343b5f3c430ce581db05a5ecb8b7f062cf212 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Tue, 14 Oct 2025 09:37:45 +0200 Subject: [PATCH 6/8] Hotfix - Fixed the deletion of the task if the user leaves the app while the task is in the undo interval --- src/pages/secondary/agenda.tsx | 49 ++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/pages/secondary/agenda.tsx b/src/pages/secondary/agenda.tsx index f341afc..241ddee 100644 --- a/src/pages/secondary/agenda.tsx +++ b/src/pages/secondary/agenda.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { getTasksFromLocalStorage, removeTaskFromLocalStorage, + saveTaskToLocalStorage, } from "@/lib/utils/agenda"; import { TaskData } from "@/types/data"; import { format } from "date-fns"; @@ -22,24 +23,34 @@ export function AgendaPage() { const pendingTimeoutsRef = useRef< Map> >(new Map()); + const pendingTasksRef = useRef>(new Map()); const { t } = useTranslation(); const refreshTasks = useCallback(() => { - setTasks(getTasksFromLocalStorage()); - }, [setTasks]); + const storedTasks = getTasksFromLocalStorage(); + const pendingTasks = Array.from(pendingTasksRef.current.values()); + const combined = [ + ...storedTasks, + ...pendingTasks.filter( + (pendingTask) => + !storedTasks.some((task) => task.id === pendingTask.id) + ), + ]; + combined.sort( + (a, b) => a.date.getTime() - b.date.getTime() + ); + setTasks(combined); + }, []); - const finalizeTaskCompletion = useCallback( - (taskId: string) => { - removeTaskFromLocalStorage({ taskId }); - refreshTasks(); - pendingTimeoutsRef.current.delete(taskId); - setPendingTaskIds((prev) => { - const { [taskId]: _removed, ...rest } = prev; - return rest; - }); - }, - [refreshTasks] - ); + const finalizeTaskCompletion = useCallback((taskId: string) => { + pendingTimeoutsRef.current.delete(taskId); + pendingTasksRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + setTasks((prev) => prev.filter((task) => task.id !== taskId)); + }, []); const handleTaskCompletionToggle = (taskId: string) => { const isPending = Boolean(pendingTaskIds[taskId]); @@ -50,13 +61,23 @@ export function AgendaPage() { clearTimeout(existingTimeout); } pendingTimeoutsRef.current.delete(taskId); + const pendingTask = pendingTasksRef.current.get(taskId); + if (pendingTask) { + saveTaskToLocalStorage({ task: pendingTask }); + } + pendingTasksRef.current.delete(taskId); setPendingTaskIds((prev) => { const { [taskId]: _removed, ...rest } = prev; return rest; }); + refreshTasks(); return; } + const taskToComplete = tasks.find((task) => task.id === taskId); + if (!taskToComplete) return; + pendingTasksRef.current.set(taskId, taskToComplete); + removeTaskFromLocalStorage({ taskId }); setPendingTaskIds((prev) => ({ ...prev, [taskId]: true })); const timeoutId = setTimeout( () => finalizeTaskCompletion(taskId), From d5bda69e6becd19812729d2b433635c70664597d Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Tue, 14 Oct 2025 09:50:49 +0200 Subject: [PATCH 7/8] Task editing - Added the ability to edit a task created in the agenda --- src/components/drawer-event-task.tsx | 217 ++++++++++++++++++--------- src/lib/utils/agenda.ts | 31 +++- src/locales/en-US.json | 6 +- src/locales/es-ES.json | 6 +- src/locales/fr-FR.json | 6 +- src/pages/secondary/agenda.tsx | 67 +++++++-- 6 files changed, 242 insertions(+), 91 deletions(-) diff --git a/src/components/drawer-event-task.tsx b/src/components/drawer-event-task.tsx index 06da045..7ed576f 100644 --- a/src/components/drawer-event-task.tsx +++ b/src/components/drawer-event-task.tsx @@ -11,14 +11,18 @@ import { import { Label } from "./ui/label"; import { Input } from "./ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { TimePicker } from "./ui/time-picker/time-picker"; -import { saveTaskToLocalStorage } from "@/lib/utils/agenda"; +import { + saveTaskToLocalStorage, + updateTaskInLocalStorage, +} from "@/lib/utils/agenda"; import { Lesson } from "@/types/aurion"; import { saveUserEventToLocalStorage } from "@/lib/utils/planning"; import { toast } from "sonner"; import { format } from "date-fns"; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; +import { TaskData } from "@/types/data"; const combineDateAndTime = ( targetDate: Date | undefined, @@ -57,53 +61,113 @@ const createDefaultDateTimes = () => { }; }; +interface DrawerEventTaskProps { + type: "event" | "task"; + onClose: () => void; + mode?: "create" | "edit"; + initialTask?: TaskData; + onSave?: () => void; + open?: boolean; + onOpenChange?: (open: boolean) => void; + hideTrigger?: boolean; +} + export function DrawerEventTask({ type, onClose, -}: { - type: "event" | "task"; - onClose: () => void; -}) { - const [open, setOpen] = useState(false); + mode = "create", + initialTask, + onSave, + open: controlledOpen, + onOpenChange, + hideTrigger = false, +}: DrawerEventTaskProps) { const { t } = useTranslation(); - - const defaultDateTimesRef = useRef(createDefaultDateTimes()); + const isEditMode = mode === "edit"; + const isTaskDrawer = type === "task"; const [title, setTitle] = useState(""); - const [date, setDate] = useState( - defaultDateTimesRef.current.date - ); - const [startTime, setStartTime] = useState( - defaultDateTimesRef.current.startTime - ); - const [endTime, setEndTime] = useState( - defaultDateTimesRef.current.endTime + const [date, setDate] = useState(); + const [startTime, setStartTime] = useState(); + const [endTime, setEndTime] = useState(); + + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = typeof controlledOpen === "boolean"; + const open = isControlled ? Boolean(controlledOpen) : internalOpen; + + const setDrawerOpen = useCallback( + (next: boolean) => { + if (!isControlled) { + setInternalOpen(next); + } + onOpenChange?.(next); + }, + [isControlled, onOpenChange] ); - const resetForm = useCallback(() => { + const initializeForm = useCallback(() => { + if (isEditMode && isTaskDrawer && initialTask) { + const taskDate = new Date(initialTask.date); + setTitle(initialTask.task); + setDate(new Date(taskDate)); + setStartTime(new Date(taskDate)); + setEndTime(new Date(taskDate)); + return; + } + const defaults = createDefaultDateTimes(); - defaultDateTimesRef.current = defaults; setTitle(""); setDate(defaults.date); setStartTime(defaults.startTime); setEndTime(defaults.endTime); - }, []); + }, [initialTask, isEditMode, isTaskDrawer]); useEffect(() => { if (open) { - resetForm(); + initializeForm(); } - }, [open, resetForm]); + }, [open, initializeForm]); + + const closeDrawer = useCallback(() => { + setDrawerOpen(false); + onClose(); + }, [onClose, setDrawerOpen]); + + const handleDrawerOpenChange = useCallback( + (next: boolean) => { + if (next) { + setDrawerOpen(true); + } else { + closeDrawer(); + } + }, + [closeDrawer, setDrawerOpen] + ); + + const now = new Date(); + const startDateTimeForValidation = combineDateAndTime(date, startTime); + const endDateTimeForValidation = combineDateAndTime(date, endTime); + const isEventDisabled = + type === "event" && + (!startDateTimeForValidation || + !endDateTimeForValidation || + endDateTimeForValidation.getTime() <= + startDateTimeForValidation.getTime() || + startDateTimeForValidation.getTime() < now.getTime()); + const isTaskDisabled = + type === "task" && + (!startDateTimeForValidation || + startDateTimeForValidation.getTime() < now.getTime()); const handleValidate = () => { - const now = new Date(); + const validationNow = new Date(); const startDateTime = combineDateAndTime(date, startTime); const endDateTime = combineDateAndTime(date, endTime); if (!title || !startDateTime || (type === "event" && !endDateTime)) return; - if (startDateTime.getTime() < now.getTime()) return; + if (startDateTime.getTime() < validationNow.getTime()) return; if ( type === "event" && endDateTime && @@ -111,7 +175,6 @@ export function DrawerEventTask({ ) return; - switch (type) { case "event": { if (!endDateTime) return; @@ -133,72 +196,84 @@ export function DrawerEventTask({ } case "task": { - saveTaskToLocalStorage({ - task: { - id: crypto.randomUUID(), + if (isEditMode && initialTask) { + const updatedTask: TaskData = { + ...initialTask, task: title, date: startDateTime, - }, - }); - toast.success("Tâche ajoutée", { - description: "La tâche a été ajoutée à l’agenda.", - duration: 3000, - }); + }; + updateTaskInLocalStorage({ task: updatedTask }); + toast.success(t("agendaPage.taskUpdated"), { + description: t( + "agendaPage.taskUpdatedDescription" + ), + duration: 3000, + }); + } else { + saveTaskToLocalStorage({ + task: { + id: crypto.randomUUID(), + task: title, + date: startDateTime, + }, + }); + toast.success("Tâche ajoutée", { + description: "La tâche a été ajoutée à l’agenda.", + duration: 3000, + }); + } break; } default: break; } - handleClose(); - }; - const handleClose = () => { - onClose(); - setOpen(false); - resetForm(); + onSave?.(); + closeDrawer(); }; - const now = new Date(); - const startDateTimeForValidation = combineDateAndTime(date, startTime); - const endDateTimeForValidation = combineDateAndTime(date, endTime); - const isEventDisabled = - type === "event" && - (!startDateTimeForValidation || - !endDateTimeForValidation || - endDateTimeForValidation.getTime() <= - startDateTimeForValidation.getTime() || - startDateTimeForValidation.getTime() < now.getTime()); - const isTaskDisabled = - type === "task" && - (!startDateTimeForValidation || - startDateTimeForValidation.getTime() < now.getTime()); + const drawerTitle = + type === "event" + ? isEditMode + ? "Modifier l'événement" + : "Ajouter un événement" + : isEditMode + ? t("agendaPage.editTaskTitle") + : t("agendaPage.addTask"); + + const submitLabel = + type === "event" + ? isEditMode + ? "Mettre à jour l'événement" + : "Créer l'événement" + : isEditMode + ? t("agendaPage.updateTaskBtn") + : t("agendaPage.createTaskBtn"); return ( - - - - + + {!hideTrigger && ( + + + + )} - - {type === "event" - ? "Ajouter un événement" - : "Ajouter une tâche"} - + {drawerTitle}
setTitle(e.target.value)} /> @@ -214,7 +289,7 @@ export function DrawerEventTask({
{type === "task" ? ( @@ -246,9 +321,7 @@ export function DrawerEventTask({ isTaskDisabled } > - {type === "event" - ? "Créer l'événement" - : "Créer la tâche"} + {submitLabel}
diff --git a/src/lib/utils/agenda.ts b/src/lib/utils/agenda.ts index fdbb635..08435c4 100644 --- a/src/lib/utils/agenda.ts +++ b/src/lib/utils/agenda.ts @@ -1,16 +1,21 @@ import { TaskData } from "@/types/data"; import { getFromStorage, saveToStorage } from "./storage"; +type StoredTask = Omit & { date: string }; + export function saveTaskToLocalStorage({ task }: { task: TaskData }) { const existingTasks = JSON.parse( getFromStorage("tasks") ?? "[]" - ) as TaskData[]; - existingTasks.push(task); + ) as StoredTask[]; + existingTasks.push({ + ...task, + date: new Date(task.date).toISOString(), + }); saveToStorage("tasks", JSON.stringify(existingTasks)); } export function getTasksFromLocalStorage(): TaskData[] { - const tasks = JSON.parse(getFromStorage("tasks") ?? "[]") as TaskData[]; + const tasks = JSON.parse(getFromStorage("tasks") ?? "[]") as StoredTask[]; return tasks.map((task) => ({ ...task, date: new Date(task.date), @@ -20,7 +25,25 @@ export function getTasksFromLocalStorage(): TaskData[] { export function removeTaskFromLocalStorage({ taskId }: { taskId: string }) { const existingTasks = JSON.parse( getFromStorage("tasks") ?? "[]" - ) as TaskData[]; + ) as StoredTask[]; const updatedTasks = existingTasks.filter((task) => task.id !== taskId); saveToStorage("tasks", JSON.stringify(updatedTasks)); } + +export function updateTaskInLocalStorage({ task }: { task: TaskData }) { + const existingTasks = JSON.parse( + getFromStorage("tasks") ?? "[]" + ) as StoredTask[]; + + const updatedTasks = existingTasks.map((storedTask) => + storedTask.id === task.id + ? { + ...storedTask, + task: task.task, + date: new Date(task.date).toISOString(), + } + : storedTask + ); + + saveToStorage("tasks", JSON.stringify(updatedTasks)); +} diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 6d08b3d..1f729a6 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -137,7 +137,11 @@ "hour": "Hour", "createTaskBtn": "Create task", "markTaskDone": "Mark as done", - "undoTaskCompletion": "Undo completion" + "undoTaskCompletion": "Undo completion", + "editTaskTitle": "Edit task", + "updateTaskBtn": "Update task", + "taskUpdated": "Task updated", + "taskUpdatedDescription": "Your changes have been saved." }, "associationsPage": { "search": "Find an association", diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 3f8f714..1488c91 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -137,7 +137,11 @@ "hour": "Hora", "createTaskBtn": "Crear tarea", "markTaskDone": "Marcar como completada", - "undoTaskCompletion": "Deshacer la finalización" + "undoTaskCompletion": "Deshacer la finalización", + "editTaskTitle": "Editar tarea", + "updateTaskBtn": "Actualizar tarea", + "taskUpdated": "Tarea actualizada", + "taskUpdatedDescription": "Los cambios se han guardado." }, "associationsPage": { "search": "Buscar una asociación", diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 86f1d6d..71d9b82 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -137,7 +137,11 @@ "hour": "Heure", "createTaskBtn": "Créer la tâche", "markTaskDone": "Marquer comme effectuée", - "undoTaskCompletion": "Annuler la complétion" + "undoTaskCompletion": "Annuler la complétion", + "editTaskTitle": "Modifier la tâche", + "updateTaskBtn": "Mettre à jour la tâche", + "taskUpdated": "Tâche mise à jour", + "taskUpdatedDescription": "Les modifications ont été enregistrées." }, "associationsPage": { "search": "Rechercher une association", diff --git a/src/pages/secondary/agenda.tsx b/src/pages/secondary/agenda.tsx index 241ddee..542eb9d 100644 --- a/src/pages/secondary/agenda.tsx +++ b/src/pages/secondary/agenda.tsx @@ -24,6 +24,8 @@ export function AgendaPage() { Map> >(new Map()); const pendingTasksRef = useRef>(new Map()); + const [editingTask, setEditingTask] = useState(null); + const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false); const { t } = useTranslation(); const refreshTasks = useCallback(() => { @@ -42,15 +44,22 @@ export function AgendaPage() { setTasks(combined); }, []); - const finalizeTaskCompletion = useCallback((taskId: string) => { - pendingTimeoutsRef.current.delete(taskId); - pendingTasksRef.current.delete(taskId); - setPendingTaskIds((prev) => { - const { [taskId]: _removed, ...rest } = prev; - return rest; - }); - setTasks((prev) => prev.filter((task) => task.id !== taskId)); - }, []); + const finalizeTaskCompletion = useCallback( + (taskId: string) => { + pendingTimeoutsRef.current.delete(taskId); + pendingTasksRef.current.delete(taskId); + setPendingTaskIds((prev) => { + const { [taskId]: _removed, ...rest } = prev; + return rest; + }); + setTasks((prev) => prev.filter((task) => task.id !== taskId)); + if (editingTask?.id === taskId) { + setIsEditDrawerOpen(false); + setEditingTask(null); + } + }, + [editingTask] + ); const handleTaskCompletionToggle = (taskId: string) => { const isPending = Boolean(pendingTaskIds[taskId]); @@ -78,6 +87,10 @@ export function AgendaPage() { if (!taskToComplete) return; pendingTasksRef.current.set(taskId, taskToComplete); removeTaskFromLocalStorage({ taskId }); + if (editingTask?.id === taskId) { + setIsEditDrawerOpen(false); + setEditingTask(null); + } setPendingTaskIds((prev) => ({ ...prev, [taskId]: true })); const timeoutId = setTimeout( () => finalizeTaskCompletion(taskId), @@ -86,6 +99,15 @@ export function AgendaPage() { pendingTimeoutsRef.current.set(taskId, timeoutId); }; + const handleTaskCardClick = useCallback( + (task: TaskData) => { + if (pendingTaskIds[task.id]) return; + setEditingTask(task); + setIsEditDrawerOpen(true); + }, + [pendingTaskIds] + ); + useEffect(() => { return () => { pendingTimeoutsRef.current.forEach((timeoutId) => @@ -95,6 +117,10 @@ export function AgendaPage() { }; }, []); + useEffect(() => { + refreshTasks(); + }, [refreshTasks]); + return (
@@ -122,6 +148,7 @@ export function AgendaPage() {
handleTaskCardClick(task)} >
+ onClick={(event) => { + event.stopPropagation(); handleTaskCompletionToggle( task.id - ) - } + ); + }} title={ isPending ? t( @@ -217,6 +245,21 @@ export function AgendaPage() { )}
+ { + setIsEditDrawerOpen(open); + if (!open) { + setEditingTask(null); + } + }} + onClose={refreshTasks} + onSave={refreshTasks} + />
); } From e12a43603702c5050a56901116fd0ede870348d8 Mon Sep 17 00:00:00 2001 From: Dark-Louis Date: Wed, 15 Oct 2025 22:41:48 +0200 Subject: [PATCH 8/8] Hotfix - Removed useless useEffects --- src/components/drawer-event-task.tsx | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/components/drawer-event-task.tsx b/src/components/drawer-event-task.tsx index 7ed576f..7b55f86 100644 --- a/src/components/drawer-event-task.tsx +++ b/src/components/drawer-event-task.tsx @@ -43,7 +43,7 @@ const combineDateAndTime = ( const createDefaultDateTimes = () => { const start = new Date(); - start.setHours(start.getHours() + 1); + start.setHours(start.getHours() + 1); // Add an hour to the default time const date = new Date( start.getFullYear(), @@ -52,7 +52,7 @@ const createDefaultDateTimes = () => { ); const end = new Date(start); - end.setHours(end.getHours() + 1); + end.setHours(end.getHours() + 1); // Add an hour to the default time return { date, @@ -122,11 +122,10 @@ export function DrawerEventTask({ setEndTime(defaults.endTime); }, [initialTask, isEditMode, isTaskDrawer]); - useEffect(() => { - if (open) { - initializeForm(); - } - }, [open, initializeForm]); + const handleOpen = useCallback(() => { + initializeForm(); + setDrawerOpen(true); + }, [initializeForm, setDrawerOpen]); const closeDrawer = useCallback(() => { setDrawerOpen(false); @@ -136,12 +135,12 @@ export function DrawerEventTask({ const handleDrawerOpenChange = useCallback( (next: boolean) => { if (next) { - setDrawerOpen(true); + handleOpen(); } else { closeDrawer(); } }, - [closeDrawer, setDrawerOpen] + [closeDrawer, handleOpen] ); const now = new Date(); @@ -396,17 +395,10 @@ const TimePickerComponent = ({ value: Date | undefined; onChange: (time: Date | undefined) => void; }) => { - const [time, setTime] = useState(() => - value ? new Date(value) : new Date() - ); - - useEffect(() => { - setTime(value ? new Date(value) : new Date()); - }, [value]); + const time = value ? new Date(value) : new Date(); const handleTimeChange = (newTime: Date | undefined) => { if (!newTime) return; - setTime(newTime); onChange(newTime); };