events form

This commit is contained in:
gom-by
2025-02-09 14:28:29 +01:00
parent 535b32317e
commit bc7cb196c7
5 changed files with 368 additions and 298 deletions

View File

@@ -1,114 +0,0 @@
"use client"
import * as React from "react";
import { format } from "date-fns";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
interface DateTimePickerProps {
onDateSelectChange: (selectedDate: Date | undefined) => void;
}
export function DateTimePicker({
onDateSelectChange
}: DateTimePickerProps) {
const [date, setDate] = React.useState<Date>();
const [isOpen, setIsOpen] = React.useState(false);
// TODO : this is buggy as hell
const hours = Array.from({ length: 24 }, (_, i) => i);
const handleDateSelect = (selectedDate: Date | undefined) => {
if (selectedDate) {
setDate(selectedDate);
onDateSelectChange(selectedDate)
}
};
const handleTimeChange = (
type: "hour" | "minute",
value: string
) => {
if (date) {
const newDate = new Date(date);
if (type === "hour") {
newDate.setHours(parseInt(value));
} else if (type === "minute") {
newDate.setMinutes(parseInt(value));
}
setDate(newDate);
}
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-fit justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
{/*<CalendarIcon className="mr-2 h-4 w-4" />*/}
<p className="w-full"> {date ? (
format(date, "MM/dd/yyyy hh:mm")
) : (
<span>MM/DD/YYYY hh:mm</span>
)} </p>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<div className="sm:flex">
<Calendar
mode="single"
selected={date}
onSelect={handleDateSelect}
initialFocus
/>
<div className="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea className="w-64 sm:w-auto">
<div className="flex sm:flex-col p-2">
{hours.reverse().map((hour) => (
<Button
key={hour}
size="icon"
variant={date && date.getHours() === hour ? "default" : "ghost"}
className="sm:w-full shrink-0 aspect-square"
onClick={() => handleTimeChange("hour", hour.toString())}
>
{hour}
</Button>
))}
</div>
<ScrollBar orientation="horizontal" className="sm:hidden" />
</ScrollArea>
<ScrollArea className="w-64 sm:w-auto">
<div className="flex sm:flex-col p-2">
{Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
<Button
key={minute}
size="icon"
variant={date && date.getMinutes() === minute ? "default" : "ghost"}
className="sm:w-full shrink-0 aspect-square"
onClick={() => handleTimeChange("minute", minute.toString())}
>
{minute.toString().padStart(2, '0')}
</Button>
))}
</div>
<ScrollBar orientation="horizontal" className="sm:hidden" />
</ScrollArea>
</div>
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,286 @@
"use client"
import * as React from "react"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Checkbox } from "@/components/ui/checkbox"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useForm } from "react-hook-form"
import {
CalendarEventExternal,
} from "@schedule-x/calendar";
import ICalendarEvent from "@/interfaces/ICalendarEvent"
export const eventFormSchema = z.object({
title: z.string().min(1, "Titre requis"),
startDate: z.date({
required_error: "Date de début requise",
}),
startTime: z.string(),
endDate: z.date({
required_error: "Date finale requise",
}),
endTime: z.string(),
fullDay: z.boolean().default(false),
frequency: z.enum(["unique", "quotidien", "hebdomadaire", "mensuel"]),
frequencyEndDate: z.date().optional(),
isVisible: z.boolean().default(true),
})
export type EventFormValues = z.infer<typeof eventFormSchema>
const frequencies = [
{ label: "Unique", value: "unique" },
{ label: "Quotidien", value: "quotidien" },
{ label: "Hebdomadaire", value: "hebdomadaire" },
{ label: "Mensuel", value: "mensuel" },
]
const isCalendarEventExternal = (event: CalendarEventExternal | Omit<CalendarEventExternal, "id">): event is CalendarEventExternal => {
return (event as CalendarEventExternal).id !== undefined;
};
export const EventForm: React.FC<
{
event: ICalendarEvent | Omit<ICalendarEvent, "id">;
onSubmitEvent: (eventFormValues: EventFormValues) => void;
}
> = ({
event,
onSubmitEvent,
}) => {
const form = useForm<EventFormValues>({
resolver: zodResolver(eventFormSchema),
defaultValues: {
title: isCalendarEventExternal(event) ? event.title : "",
startDate: isCalendarEventExternal(event) ? new Date(event.start) : new Date(),
startTime: isCalendarEventExternal(event) ? `${new Date(event.start).getHours()}:${new Date(event.start).getMinutes()}` : "10:22",
endDate: isCalendarEventExternal(event) ? new Date(event.end) : new Date(),
endTime: isCalendarEventExternal(event) ? `${new Date(event.end).getHours()}:${new Date(event.end).getMinutes()}` : "11:22",
fullDay: isCalendarEventExternal(event) ? event.fullday : false,
frequency: isCalendarEventExternal(event) ? event.rrule : "unique",
isVisible: isCalendarEventExternal(event) ? event.visibility : true,
},
})
const frequency = form.watch("frequency")
async function onSubmit(data: EventFormValues) {
try {
const validatedData = eventFormSchema.parse(data)
onSubmitEvent(validatedData)
} catch (error) {
console.error("On submit error : ", error)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full max-w-md space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Titre</FormLabel>
<FormControl>
<Input placeholder="Ajouter un titre" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-[1fr,auto,1fr] items-end gap-2">
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Début</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{field.value ? format(field.value, "dd/MM/yyyy") : <span>Choisis une date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={field.value} onSelect={field.onChange} initialFocus />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="time" {...field} className="w-[120px]" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<span className="invisible">Until</span>
<FormField
control={form.control}
name="endDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Fin</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{field.value ? format(field.value, "MM/dd/yyyy") : <span>Choisis une date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={field.value} onSelect={field.onChange} initialFocus />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endTime"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="time" {...field} className="w-[120px]" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="fullDay"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel>Journée complète</FormLabel>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-4 items-end">
<FormField
control={form.control}
name="frequency"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Fréquence</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Selectionner Fréquence" />
</SelectTrigger>
</FormControl>
<SelectContent>
{frequencies.map((frequency) => (
<SelectItem key={frequency.value} value={frequency.value}>
{frequency.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{frequency !== "unique" && (
<FormField
control={form.control}
name="frequencyEndDate"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Jusqu'au</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{field.value ? format(field.value, "MM/dd/yyyy") : <span>Choisis une date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={field.value} onSelect={field.onChange} initialFocus />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<FormField
control={form.control}
name="isVisible"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="align-sub">Evènement visible ?</FormLabel>
<FormControl>
<Checkbox className="m-3 align-top justify-center"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2">
<Button variant="outline" type="button">
Abandonner
</Button>
<Button type="submit" className="bg-[#6B4EFF] hover:bg-[#5B3FEF]">
Sauvegarder
</Button>
</div>
</form>
</Form>
)
}

View File

@@ -8,7 +8,6 @@ import { createDragAndDropPlugin } from "@schedule-x/drag-and-drop";
import { createResizePlugin } from "@schedule-x/resize";
import { createEventRecurrencePlugin } from "@schedule-x/event-recurrence";
import {
CalendarEventExternal,
createViewDay,
createViewWeek,
} from "@schedule-x/calendar";
@@ -22,25 +21,43 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { KeyedMutator } from "swr";
import { getCookie } from "cookies-next";
import { useTheme } from "next-themes";
import { Checkbox } from "@/components/ui/checkbox";
import { eventNames } from "process";
import { useSearchParams } from "next/navigation";
import { CheckedState } from "@radix-ui/react-checkbox";
import { DateTimePicker } from "./date-time-picker";
import { EventForm, EventFormValues } from "./event-dialog";
import ICalendarEvent from "@/interfaces/ICalendarEvent";
interface CalendarEventExternalDB extends CalendarEventExternal {
status: "Active" | "Inactive"
}
const mapFrequencyToRrule = (frequency: "unique" | "quotidien" | "hebdomadaire" | "mensuel", frequencyEndDate?: Date): string => {
let rrule = "";
switch (frequency) {
case "quotidien":
rrule = "FREQ=DAILY";
break;
case "hebdomadaire":
rrule = "FREQ=WEEKLY";
break;
case "mensuel":
rrule = "FREQ=MONTHLY";
break;
default:
return "";
}
if (frequencyEndDate) {
const until = frequencyEndDate.getTime();
const untilDate = new Date(until);
const epochDateString = untilDate.toISOString().replace(/[-:]/g, "").split(".")[0]; // Format as YYYYMMDDTHHmmss
rrule += `;UNTIL=${epochDateString}`;
}
return rrule;
};
const Planning: React.FC<{
events: CalendarEventExternal[];
mutate?: KeyedMutator<ApiResponse<CalendarEventExternal[]>>;
events: ICalendarEvent[];
mutate?: KeyedMutator<ApiResponse<ICalendarEvent[]>>;
}> = ({ events, mutate }) => {
const { resolvedTheme } = useTheme();
console.log(resolvedTheme);
@@ -54,15 +71,14 @@ const Planning: React.FC<{
]
: [];
const [eventSelected, setEventSelected] =
useState<CalendarEventExternal | null>(null);
const [eventStatus, setEventStatus] = useState<"Active" | "Inactive">("Active")
useState<ICalendarEvent | null>(null);
const [newEvent, setNewEvent] = useState<Omit<
CalendarEventExternal,
ICalendarEvent,
"id"
> | null>(null);
const handleEventUpdate = async (eventSelected: CalendarEventExternal) => {
const event: CalendarEventExternal = {
const handleEventUpdate = async (eventSelected: ICalendarEvent) => {
const event: ICalendarEvent = {
...eventSelected,
start: `${new Date(eventSelected.start).toISOString()}`,
end: `${new Date(eventSelected.end).toISOString()}`,
@@ -101,14 +117,24 @@ const Planning: React.FC<{
})),
callbacks: {
onEventClick(event, e) {
setEventSelected(event);
},
// async onBeforeEventUpdate(oldEvent, newEvent) {
// await request("/api/me/has-permissions")
// },
console.log("event from schedule-x: " + event)
const eSelected: ICalendarEvent = {
...event,
rrule: "",
isVisible: true,
fullday: false
}
setEventSelected(eSelected);
},
async onEventUpdate(newEvent) {
// console.log(event);
await handleEventUpdate(newEvent);
console.log("event from schedule-x: " + newEvent)
const eSelected: ICalendarEvent = {
...newEvent,
rrule: "",
isVisible: true,
fullday: false
}
await handleEventUpdate(eSelected);
},
},
},
@@ -142,69 +168,20 @@ const Planning: React.FC<{
setNewEvent((e) => (open ? e : null));
}}
event={newEvent}
//onStartChange={(e) => {
// const val = e.currentTarget.value;
// setNewEvent((ev) => {
// if (ev)
// return {
// ...ev,
// start: val,
// };
// return ev;
// });
//}}
onStartDateChange={date => {
setNewEvent((ev) => {
if (ev) return {
...ev,
start: date,
};
return ev
})
}}
onEndDateChange={date => {
setNewEvent((ev) => {
if (ev) return {
...ev,
end: date,
};
return ev
})
}}
onTitleChange={(e) => {
const val = e.currentTarget.value;
setNewEvent((ev) => {
if (ev)
return {
...ev,
title: val,
};
return ev;
});
}}
onActiveStateChange={(e) => {
e ? setEventStatus("Active")
: setEventStatus("Inactive")
}}
//onEndChange={(e) => {
// const val = e.currentTarget.value;
// setNewEvent((ev) => {
// if (ev)
// return {
// ...ev,
// end: val,
// };
// return ev;
// });
//}}
onAdd={async () => {
onSubmitEvent={async (eventFormValues) => {
const rrule = mapFrequencyToRrule(
eventFormValues.frequency,
eventFormValues.frequencyEndDate
)
try {
const event: Omit<CalendarEventExternal, "id"> = {
const event: Omit<ICalendarEvent, "id"> = {
...newEvent,
start: `${new Date(newEvent.start).toISOString()}`,
end: `${new Date(newEvent.end).toISOString()}`,
title: newEvent.title,
status: eventStatus
start: `${eventFormValues.startDate} ${eventFormValues.startTime}`,
end: `${eventFormValues.endDate} ${eventFormValues.endTime}`,
title: `${eventFormValues.title}`,
fullDay: eventFormValues.fullDay,
rrule: rrule,
isVisible: eventFormValues.isVisible
}
const res = await request<undefined>(
`/events/new`,
@@ -235,25 +212,8 @@ const Planning: React.FC<{
setEventSelected((e) => (open ? e : null));
}}
event={eventSelected}
onStartDateChange={(date) => {
setEventSelected((ev) => {
if (ev)
return {
...ev,
start: date,
};
return ev;
});
}}
onEndDateChange={(date) => {
setEventSelected((ev) => {
if (ev)
return {
...ev,
end: date,
};
return ev;
});
onSubmitEvent={ (eventForm) => {
console.log("Event form: " + eventForm)
}}
onDelete={async () => {
calendar?.events?.remove(eventSelected.id);
@@ -290,33 +250,21 @@ const Planning: React.FC<{
const EventDialog: React.FC<
{
// onEndChange: React.ChangeEventHandler<HTMLInputElement>;
// onStartChange: React.ChangeEventHandler<HTMLInputElement>;
onStartDateChange: (selectedDate: Date | undefined) => void;
onEndDateChange: (selectedDate: Date | undefined) => void;
onSubmitEvent: (eventFormValues: EventFormValues) => void;
onDelete?: () => void;
onUpdate?: () => void;
onAdd?: () => void;
onTitleChange?: React.ChangeEventHandler<HTMLInputElement>;
onActiveStateChange?: (status: boolean) => void;
event: CalendarEventExternal | Omit<CalendarEventExternal, "id">;
event: ICalendarEvent | Omit<ICalendarEvent, "id">;
} & DialogProps
> = ({
open,
onOpenChange,
// onEndChange,
// onStartChange,
onStartDateChange,
onEndDateChange,
onSubmitEvent,
onDelete,
onUpdate,
onAdd,
onTitleChange,
onActiveStateChange,
event,
}) => {
const [checked, setChecked] = useState<CheckedState>(event.status === "Active")
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
@@ -324,62 +272,9 @@ const EventDialog: React.FC<
<DialogTitle>{event.title}</DialogTitle>
<DialogDescription>{event.description}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
Titre
</Label>
<Input
id="title"
value={event.title || ""}
onChange={onTitleChange}
className="col-span-3"
type="text"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="start" className="text-right">
Début
</Label>
<DateTimePicker
onDateSelectChange={date => onStartDateChange(date)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="end" className="text-right">
Fin
</Label>
{/*<Input
id="end"
value={event.end || ""}
onChange={onEndChange}
className="col-span-3"
/> */}
<DateTimePicker
onDateSelectChange={date => onEndDateChange(date)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="status" className="col-span-3 text-right">
Rendre cette évènement actif ?
</Label>
<Checkbox
id="status"
checked={
checked
}
onCheckedChange={(e) => {
const booleanCheck = !!e
setChecked(prev => { return !prev })
if (onActiveStateChange) {
onActiveStateChange(booleanCheck)
}
}}
/>
</div>
</div>
<EventForm
event={event}
onSubmitEvent={onSubmitEvent}/>
<DialogFooter className="flex flex-row justify-end">
{onUpdate && (
<Button

View File

@@ -0,0 +1,8 @@
import {
CalendarEventExternal,
} from "@schedule-x/calendar";
export default interface ICalendarEvent extends CalendarEventExternal {
isVisible: boolean,
fullday: boolean,
rrule: string
}