CRUD Events and React interaction

This commit is contained in:
cdricms
2025-01-27 17:58:47 +01:00
parent 9843158803
commit ac7e97527f
17 changed files with 887 additions and 349 deletions

View File

@@ -97,11 +97,11 @@ func Verify(ctx context.Context, email, password string) (*User, error) {
type Event struct {
bun.BaseModel `bun:"table:events"`
EventID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"eventID"`
EventID uuid.UUID `bun:"event_id,type:uuid,pk,default:gen_random_uuid()" json:"id"`
CreationDate time.Time `bun:"creation_date,notnull,default:current_timestamp" json:"creationDate"`
ScheduleStart time.Time `bun:"schedule_start,notnull" json:"scheduleStart"`
ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"scheduleEnd"`
Status Status `bun:"status,notnull,default:Inactive" json:"status"`
ScheduleStart time.Time `bun:"schedule_start,notnull" json:"start"`
ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"end"`
Status Status `bun:"status,notnull,default:'Inactive'" json:"status"`
}
type EventToUser struct {

View File

@@ -13,7 +13,7 @@ func HandleDeleteEvent(w http.ResponseWriter, r *http.Request) {
var event core.Event
res, err := core.DB.NewDelete().
Model(&event).
Where("id = ?", uuid).
Where("event_id = ?", uuid).
Returning("*").
Exec(context.Background())
if err != nil {
@@ -24,10 +24,9 @@ func HandleDeleteEvent(w http.ResponseWriter, r *http.Request) {
return
}
log.Println(res)
core.JSONSuccess{
Status: core.Success,
Message: "Event deleted.",
}.Respond(w, http.StatusOK)
}

View File

@@ -10,14 +10,6 @@ import (
)
func HandleCreateBlog(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
core.JSONError{
Status: core.Error,
Message: "Method is not allowed",
}.Respond(w, http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
core.JSONError{

View File

@@ -18,18 +18,19 @@ func HandleCreateEvent(w http.ResponseWriter, r *http.Request) {
}.Respond(w, http.StatusBadRequest)
return
}
_, err = core.DB.NewInsert().Model(&event).Exec(context.Background())
_, err = core.DB.NewInsert().Model(&event).Exec(context.Background())
if err != nil {
core.JSONError{
Status: core.Error,
Message: err.Error(),
}.Respond(w, http.StatusNotAcceptable)
}
return
}
core.JSONSuccess{
Status: core.Success,
Message: "Event created",
Data: event,
Data: event,
}.Respond(w, http.StatusCreated)
}

View File

@@ -22,7 +22,7 @@ func HandleUpdateEvent(w http.ResponseWriter, r *http.Request) {
}
event_uuid := r.PathValue("event_uuid")
event.EventID, err = uuid.FromBytes([]byte(event_uuid))
event.EventID, err = uuid.Parse(event_uuid)
if err != nil {
core.JSONError{
Status: core.Error,
@@ -44,7 +44,7 @@ func HandleUpdateEvent(w http.ResponseWriter, r *http.Request) {
// }
_, err = core.DB.NewUpdate().
Model(event).
Model(&event).
OmitZero().
WherePK().
Exec(context.Background())
@@ -62,4 +62,3 @@ func HandleUpdateEvent(w http.ResponseWriter, r *http.Request) {
Data: event,
}.Respond(w, http.StatusOK)
}

View File

@@ -13,10 +13,11 @@ import (
)
type UpdateShortcodeArgs struct {
Code *string `json:"code"` // The shortcode value
Type *core.ShortcodeType `json:"type"`
Value *string `json:"value"`
MediaID *uuid.UUID `json:"media_id"` // Nullable reference to another table's ID
ID *int64 `json:"id,omitempty"`
Code *string `json:"code,omitempty"` // The shortcode value
Type *core.ShortcodeType `bun:"shortcode_type" json:"type,omitempty"`
Value *string `json:"value,omitempty"`
MediaID *uuid.UUID `json:"media_id,omitempty"` // Nullable reference to another table's ID
}
func HandleUpdateShortcode(w http.ResponseWriter, r *http.Request) {
@@ -51,7 +52,7 @@ func HandleUpdateShortcode(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("shortcode")
_, err = updateQuery.
Where("code = ?", code).
Where("id = ? OR code = ?", updateArgs.ID, code).
Returning("*").
Exec(context.Background())

View File

@@ -48,7 +48,7 @@ export default function ShortcodesPage() {
};
return (
<div className="container mx-auto py-10">
<div className="container mx-auto px-4 py-10">
<h1 className="text-2xl font-bold mb-5">Shortcodes</h1>
{isLoading && (
<Loader2 className="flex w-full min-w-0 flex-col gap-1 justify-center animate-spin" />

View File

@@ -1,276 +1,20 @@
"use client";
import Planning from "@/components/planning.tsx";
import { useApi } from "@/hooks/use-api";
import { type CalendarEventExternal } from "@schedule-x/calendar";
import { Loader2 } from "lucide-react";
import { request, useApi } from "@/hooks/use-api";
import "@schedule-x/theme-shadcn/dist/index.css";
import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { createEventsServicePlugin } from "@schedule-x/events-service";
import {
CalendarEventExternal,
createViewDay,
createViewWeek,
} from "@schedule-x/calendar";
import { useEffect, useState } from "react";
import { format } from "date-fns";
import { Dialog } from "@radix-ui/react-dialog";
import {
DialogContent,
DialogDescription,
DialogFooter,
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 {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { requestFormReset } from "react-dom";
const Page = () => {
const {
data: requestedEvents,
isLoading,
success,
mutate,
} = useApi<CalendarEventExternal[]>("/events", undefined, false, false);
const Planning = () => {
const plugins = [createEventsServicePlugin()];
const [eventSelected, setEventSelected] =
useState<CalendarEventExternal | null>(null);
const [events, setEvents] = useState<CalendarEventExternal[]>([
{
id: "1", // TODO put an uuid there
title: "Event 1",
start: format(new Date(Date.now()), "yyyy-MM-dd HH:mm"),
end: format(
new Date(Date.now() + 1 * 3600 * 1000),
"yyyy-MM-dd HH:mm",
),
},
]);
const [requestCreateEvent, setRequestCreateEvent] = useState(false)
const calendar = useNextCalendarApp(
{
theme: "shadcn",
views: [createViewDay(), createViewWeek()],
defaultView: "week",
isDark: true,
isResponsive: true,
locale: "fr-FR",
dayBoundaries: {
start: "06:00",
end: "00:00",
},
events,
callbacks: {
onEventClick(event, e) {
setEventSelected(event);
},
async onClickDateTime(dateTime) {
setRequestCreateEvent(true)
const newEvent: CalendarEventExternal = {
id: "5",
title: "Event 1",
start: dateTime,
end: format(
new Date(dateTime).getTime() + (1000 * 60 * 60),
"yyyy-MM-dd HH:mm",
),
}
try {
const res = await request<undefined>(
`/events/new`,
{
method: "POST",
body: JSON.stringify(newEvent),
requiresAuth: true,
csrfToken: false
},)
if (res.status === "Error") {
console.log("Error")
}
if (res.status === "Success") {
console.log("Success")
}
} catch (e) {
console.log(e)
}
},
},
},
plugins,
);
const {data: requestedEvents, isLoading, success} = useApi("/events", {
onSuccess: (data) => {
calendar?.events.set(data)
}
}, false, false)
useEffect(() => {
// get all events
calendar?.events.getAll();
}, []);
return (
<div>
<div className="m-8">
<ScheduleXCalendar calendarApp={calendar} />
</div>
<Dialog
open={eventSelected !== null || false}
onOpenChange={(open) => {
setEventSelected((e) => (open ? e : null));
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{eventSelected?.title}</DialogTitle>
<DialogDescription>
{eventSelected?.description}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="start" className="text-right">
Début
</Label>
{/*<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[240px] pl-3 text-left font-normal",
!eventSelected?.start &&
"text-muted-foreground",
)}
>
{eventSelected?.start ? (
format(eventSelected?.start, "PPP")
) : (
<span>Choisissez une date.</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
>
<Calendar
mode="single"
selected={
new Date(
eventSelected?.start ??
Date.now(),
)
}
// onSelect={field.onChange}
disabled={(date) =>
date > new Date() ||
date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover> */}
<Input
id="start"
value={eventSelected?.start || ""}
onChange={(e) => {
const val = e.currentTarget.value;
setEventSelected((ev) => {
if (ev)
return {
...ev,
start: val,
};
return ev;
});
}}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="end" className="text-right">
Fin
</Label>
<Input
id="end"
value={eventSelected?.end || ""}
onChange={(e) => {
const val = e.currentTarget.value
setEventSelected((ev) => {
if (ev)
return {
...ev,
end: val,
};
return ev;
})
}}
className="col-span-3"
/>
</div>
</div>
<DialogFooter className="flex flex-row justify-end">
<Button className="bg-red-700"
onClick={async () => {
calendar?.events?.remove(eventSelected!.id)
try {
const res = await request<undefined>(
`/events/${eventSelected!.id}/delete`,
{
method: "DELETE",
body: JSON.stringify(eventSelected),
requiresAuth: false,
csrfToken: false
},)
if (res.status === "Error") {
console.log("Error")
}
if (res.status === "Success") {
console.log("Success")
}
} catch (e) {
console.log(e)
}
}}
type="submit">
Supprimer
</Button>
<Button className="bg-blue-500"
onClick={async () => {
calendar?.events?.update(eventSelected!)
try {
const res = await request<undefined>(
`/events/${eventSelected!.id}/update`,
{
method: "PATCH",
body: JSON.stringify(eventSelected),
requiresAuth: true,
csrfToken: false
},)
if (res.status === "Error") {
console.log("Error")
}
if (res.status === "Success") {
console.log("Success")
}
} catch (e) {
console.log(e)
}
}}
type="submit">
Actualiser
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
if (isLoading) return <Loader2 className="animate-spin" />;
if (success)
return <Planning events={requestedEvents ?? []} mutate={mutate} />;
};
export default Planning;
export default Page;

View File

@@ -0,0 +1,238 @@
"use client";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { UpdateMemberDialog } from "./UpdateMemberDialog";
import { AddMemberDialog } from "./AddMemberDialog";
interface Member {
user_id: string;
firstname: string;
lastname: string;
email: string;
password: string;
phone: string;
role: string;
}
const initialMembers: Member[] = [
// Add some sample data here
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
{
user_id: "1",
firstname: "John",
lastname: "Doe",
email: "john@example.com",
password: "********",
phone: "1234567890",
role: "User",
},
// Add more sample members...
];
export function MembersTable() {
const [members, setMembers] = useState<Member[]>(initialMembers);
const [selectMode, setSelectMode] = useState(false);
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
const [updateDialogOpen, setUpdateDialogOpen] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [currentMember, setCurrentMember] = useState<Member | null>(null);
const toggleSelectMode = () => {
setSelectMode(!selectMode);
setSelectedMembers([]);
};
const toggleMemberSelection = (userId: string) => {
setSelectedMembers((prev) =>
prev.includes(userId)
? prev.filter((id) => id !== userId)
: [...prev, userId],
);
};
const handleUpdate = (member: Member) => {
setCurrentMember(member);
setUpdateDialogOpen(true);
};
const handleDelete = (userId: string) => {
setMembers((prev) =>
prev.filter((member) => member.user_id !== userId),
);
};
const handleAdd = (newMember: Member) => {
setMembers((prev) => [
...prev,
{ ...newMember, user_id: String(prev.length + 1) },
]);
};
return (
<div className="space-y-4">
<div className="flex justify-between">
<Button onClick={toggleSelectMode}>
{selectMode ? "Cancel Selection" : "Select"}
</Button>
<Button onClick={() => setAddDialogOpen(true)}>
Add New Member
</Button>
</div>
<div className="relative">
<ScrollArea className="h-[400px] rounded-md border">
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
{selectMode && (
<TableHead className="w-[50px]">
Select
</TableHead>
)}
<TableHead>User ID</TableHead>
<TableHead>First Name</TableHead>
<TableHead>Last Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Password</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.map((member) => (
<TableRow key={member.user_id}>
{selectMode && (
<TableCell>
<Checkbox
checked={selectedMembers.includes(
member.user_id,
)}
onCheckedChange={() =>
toggleMemberSelection(
member.user_id,
)
}
/>
</TableCell>
)}
<TableCell>{member.user_id}</TableCell>
<TableCell>{member.firstname}</TableCell>
<TableCell>{member.lastname}</TableCell>
<TableCell>{member.email}</TableCell>
<TableCell>{member.password}</TableCell>
<TableCell>{member.phone}</TableCell>
<TableCell>{member.role}</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
className="mr-2"
onClick={() => handleUpdate(member)}
>
Modify
</Button>
<Button
variant="destructive"
size="sm"
onClick={() =>
handleDelete(member.user_id)
}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
</div>
<UpdateMemberDialog
isOpen={updateDialogOpen}
onClose={() => setUpdateDialogOpen(false)}
member={currentMember}
onUpdate={(updatedMember) => {
setMembers((prev) =>
prev.map((m) =>
m.user_id === updatedMember.user_id
? updatedMember
: m,
),
);
setUpdateDialogOpen(false);
}}
/>
<AddMemberDialog
isOpen={addDialogOpen}
onClose={() => setAddDialogOpen(false)}
onAdd={handleAdd}
/>
</div>
);
}

View File

@@ -0,0 +1,356 @@
"use client";
import { ApiResponse, request, useApi } from "@/hooks/use-api";
import "@schedule-x/theme-shadcn/dist/index.css";
import { useNextCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { createEventsServicePlugin } from "@schedule-x/events-service";
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";
import { useEffect, useState } from "react";
import { format } from "date-fns";
import { Dialog, DialogProps } from "@radix-ui/react-dialog";
import {
DialogContent,
DialogDescription,
DialogFooter,
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";
const Planning: React.FC<{
events: CalendarEventExternal[];
mutate?: KeyedMutator<ApiResponse<CalendarEventExternal[]>>;
}> = ({ events, mutate }) => {
const plugins = [
createEventsServicePlugin(),
createDragAndDropPlugin(),
createResizePlugin(),
createEventRecurrencePlugin(),
];
const [eventSelected, setEventSelected] =
useState<CalendarEventExternal | null>(null);
const [newEvent, setNewEvent] = useState<Omit<
CalendarEventExternal,
"id"
> | null>(null);
const handleEventUpdate = async (eventSelected: CalendarEventExternal) => {
const event: CalendarEventExternal = {
...eventSelected,
start: `${new Date(eventSelected.start).toISOString()}`,
end: `${new Date(eventSelected.end).toISOString()}`,
};
try {
const res = await request<undefined>(`/events/${event.id}/update`, {
method: "PATCH",
body: event,
requiresAuth: true,
csrfToken: false,
});
if (res.status === "Error") {
console.log("Error");
}
if (res.status === "Success") {
calendar?.events?.update(eventSelected);
}
} catch (e) {
console.log(e);
}
};
const calendar = useNextCalendarApp(
{
theme: "shadcn",
views: [createViewDay(), createViewWeek()],
defaultView: "week",
isDark: true,
isResponsive: true,
locale: "fr-FR",
dayBoundaries: {
start: "06:00",
end: "00:00",
},
events: events.map((event) => ({
...event,
start: format(new Date(event.start), "yyyy-MM-dd HH:mm"),
end: format(new Date(event.end), "yyyy-MM-dd HH:mm"),
})),
callbacks: {
onEventClick(event, e) {
setEventSelected(event);
},
async onEventUpdate(event) {
await handleEventUpdate(event);
},
},
},
plugins,
);
useEffect(() => {
calendar?.events.getAll();
}, []);
const AddButton: React.FC = () => (
<Button onClick={() => setNewEvent({})} variant="outline">
Nouveau
</Button>
);
return (
<div>
<div className="m-8">
<AddButton />
<ScheduleXCalendar calendarApp={calendar} />
</div>
{newEvent && (
<EventDialog
open={newEvent !== null || false}
onOpenChange={(open) => {
setNewEvent((e) => (open ? e : null));
}}
event={newEvent}
onStartChange={(e) => {
const val = e.currentTarget.value;
setNewEvent((ev) => {
if (ev)
return {
...ev,
start: val,
};
return ev;
});
}}
onEndChange={(e) => {
const val = e.currentTarget.value;
setNewEvent((ev) => {
if (ev)
return {
...ev,
end: val,
};
return ev;
});
}}
onAdd={async () => {
try {
const event: Omit<CalendarEventExternal, "id"> = {
...newEvent,
start: `${new Date(newEvent.start).toISOString()}`,
end: `${new Date(newEvent.end).toISOString()}`,
};
const res = await request<undefined>(
`/events/new`,
{
method: "POST",
body: event,
requiresAuth: true,
csrfToken: false,
},
);
if (res.status === "Error") {
console.log("Error");
}
if (res.status === "Success") {
mutate?.();
console.log("Success");
}
} catch (e) {
console.log(e);
}
}}
/>
)}
{eventSelected && (
<EventDialog
open={eventSelected !== null || false}
onOpenChange={(open) => {
setEventSelected((e) => (open ? e : null));
}}
event={eventSelected}
onStartChange={(e) => {
const val = e.currentTarget.value;
setEventSelected((ev) => {
if (ev)
return {
...ev,
start: val,
};
return ev;
});
}}
onEndChange={(e) => {
const val = e.currentTarget.value;
setEventSelected((ev) => {
if (ev)
return {
...ev,
end: val,
};
return ev;
});
}}
onDelete={async () => {
calendar?.events?.remove(eventSelected.id);
try {
const res = await request<undefined>(
`/events/${eventSelected.id}/delete`,
{
method: "DELETE",
body: eventSelected,
requiresAuth: false,
csrfToken: false,
},
);
if (res.status === "Error") {
console.log("Error");
}
if (res.status === "Success") {
console.log("Success");
}
} catch (e) {
console.log(e);
}
setEventSelected(null);
}}
onUpdate={async () => {
await handleEventUpdate(eventSelected);
setEventSelected(null);
}}
/>
)}
</div>
);
};
const EventDialog: React.FC<
{
onEndChange: React.ChangeEventHandler<HTMLInputElement>;
onStartChange: React.ChangeEventHandler<HTMLInputElement>;
onDelete?: () => void;
onUpdate?: () => void;
onAdd?: () => void;
event: CalendarEventExternal | Omit<CalendarEventExternal, "id">;
} & DialogProps
> = ({
open,
onOpenChange,
onEndChange,
onStartChange,
onDelete,
onUpdate,
onAdd,
event,
}) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<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="start" className="text-right">
Début
</Label>
{/*<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[240px] pl-3 text-left font-normal",
!eventSelected?.start &&
"text-muted-foreground",
)}
>
{eventSelected?.start ? (
format(eventSelected?.start, "PPP")
) : (
<span>Choisissez une date.</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
>
<Calendar
mode="single"
selected={
new Date(
eventSelected?.start ??
Date.now(),
)
}
// onSelect={field.onChange}
disabled={(date) =>
date > new Date() ||
date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover> */}
<Input
id="start"
value={event.start || ""}
onChange={onStartChange}
className="col-span-3"
/>
</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"
/>
</div>
</div>
<DialogFooter className="flex flex-row justify-end">
{onUpdate && (
<Button
variant="outline"
onClick={() => onUpdate()}
type="submit"
>
Actualiser
</Button>
)}
{onDelete && (
<Button
variant="destructive"
onClick={() => onDelete()}
type="submit"
>
Supprimer
</Button>
)}
{onAdd && !onUpdate && !onDelete && (
<Button onClick={() => onAdd()} type="submit">
Créer
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default Planning;

View File

@@ -9,55 +9,47 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type IShortcode from "@/interfaces/IShortcode";
interface ShortcodeDialogProps {
onSave: (shortcode: Omit<IShortcode, "id">) => void;
onSave: (shortcode: IShortcode) => void;
open: boolean;
setOpen: () => void;
shortcode?: IShortcode;
}
export default function ShortcodeDialog({ onSave }: ShortcodeDialogProps) {
const [open, setOpen] = useState(false);
const [code, setCode] = useState("");
const [type, setType] = useState<"value" | "media">("value");
const [value, setValue] = useState("");
const [mediaId, setMediaId] = useState("");
export default function ShortcodeDialog({
onSave,
open,
setOpen,
shortcode,
}: ShortcodeDialogProps) {
const [_shortcode, setShortcode] = useState<IShortcode>(
shortcode ?? { code: "", type: "", id: 0 },
);
const handleSave = () => {
onSave({ code, type, value, media_id: mediaId });
setOpen(false);
onSave(_shortcode);
setOpen();
resetForm();
};
const resetForm = () => {
setCode("");
setType("value");
setValue("");
setMediaId("");
setShortcode({ code: "", type: "", id: 0 });
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Add New Shortcode</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add New Shortcode</DialogTitle>
<DialogTitle>Ajouter un nouveau shortcode</DialogTitle>
<DialogDescription>
Create a new shortcode here. Click save when you're
done.
Créer un nouveau shortcode ici. Cliquez enregistrer
quand vous avez fini.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
@@ -67,29 +59,41 @@ export default function ShortcodeDialog({ onSave }: ShortcodeDialogProps) {
</Label>
<Input
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
value={_shortcode.code}
onChange={(e) =>
setShortcode((p) => ({
...p,
code: e.target.value,
}))
}
className="col-span-3"
/>
</div>
<Tabs
defaultValue={type}
onValueChange={(v) => setType(v as "value" | "media")}
defaultValue={_shortcode.type}
onValueChange={(v) =>
setShortcode((p) => ({ ...p, type: v }))
}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="value">Value</TabsTrigger>
<TabsTrigger value="value">Valeur</TabsTrigger>
<TabsTrigger value="media">Media</TabsTrigger>
</TabsList>
<TabsContent value="value">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="value" className="text-right">
Value
Valeur
</Label>
<Input
id="value"
value={value}
onChange={(e) => setValue(e.target.value)}
value={_shortcode.value}
onChange={(e) =>
setShortcode((p) => ({
...p,
value: e.target.value,
}))
}
className="col-span-3"
/>
</div>
@@ -101,8 +105,13 @@ export default function ShortcodeDialog({ onSave }: ShortcodeDialogProps) {
</Label>
<Input
id="mediaId"
value={mediaId}
onChange={(e) => setMediaId(e.target.value)}
value={_shortcode.media_id}
onChange={(e) =>
setShortcode((p) => ({
...p,
media_id: e.target.value,
}))
}
className="col-span-3"
/>
</div>
@@ -111,7 +120,7 @@ export default function ShortcodeDialog({ onSave }: ShortcodeDialogProps) {
</div>
<DialogFooter>
<Button type="submit" onClick={handleSave}>
Save shortcode
Enregistrer
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -18,6 +18,7 @@ import {
import { MoreHorizontal } from "lucide-react";
import type IShortcode from "@/interfaces/IShortcode";
import ShortcodeDialog from "@/components/shortcode-dialogue";
import { useState } from "react";
interface ShortcodeTableProps {
shortcodes: IShortcode[];
@@ -32,10 +33,25 @@ export function ShortcodeTable({
onDelete,
onAdd,
}: ShortcodeTableProps) {
const [shortcodeSelected, setUpdateDialog] = useState<IShortcode | null>(
null,
);
const [addDialog, setAddDialog] = useState<boolean>(false);
return (
<div>
<div className="mb-4">
<ShortcodeDialog onSave={onAdd} />
<Button
onClick={() => {
setAddDialog(true);
}}
>
Ajouter
</Button>
<ShortcodeDialog
onSave={onAdd}
open={addDialog}
setOpen={() => setAddDialog(false)}
/>
</div>
<div className="rounded-md border">
<Table>
@@ -44,7 +60,7 @@ export function ShortcodeTable({
<TableHead>ID</TableHead>
<TableHead>Code</TableHead>
<TableHead>Type</TableHead>
<TableHead>Value</TableHead>
<TableHead>Valeur</TableHead>
<TableHead>Media ID</TableHead>
<TableHead className="w-[80px]">Actions</TableHead>
</TableRow>
@@ -69,7 +85,7 @@ export function ShortcodeTable({
className="h-8 w-8 p-0"
>
<span className="sr-only">
Open menu
Ouvrir le menu
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
@@ -77,17 +93,17 @@ export function ShortcodeTable({
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
onUpdate(shortcode)
setUpdateDialog(shortcode)
}
>
Update
Mettre à jour
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
onDelete(shortcode.code)
}
>
Delete
Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -96,6 +112,15 @@ export function ShortcodeTable({
))}
</TableBody>
</Table>
{shortcodeSelected && (
<ShortcodeDialog
onSave={onUpdate}
shortcode={shortcodeSelected}
open={shortcodeSelected ? true : false}
setOpen={() => setUpdateDialog(null)}
/>
)}
</div>
</div>
);

View File

@@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@@ -11,20 +11,26 @@
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@schedule-x/drag-and-drop": "^2.15.1",
"@schedule-x/event-modal": "^2.15.1",
"@schedule-x/event-recurrence": "^2.15.1",
"@schedule-x/events-service": "^2.14.3",
"@schedule-x/react": "^2.13.3",
"@schedule-x/resize": "^2.15.1",
"@schedule-x/theme-default": "^2.14.3",
"@schedule-x/theme-shadcn": "^2.14.3",
"class-variance-authority": "^0.7.1",
@@ -1067,6 +1073,36 @@
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
"integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
@@ -1588,6 +1624,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
"integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
@@ -1936,6 +2003,28 @@
"preact": "^10.19.2"
}
},
"node_modules/@schedule-x/drag-and-drop": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/@schedule-x/drag-and-drop/-/drag-and-drop-2.15.1.tgz",
"integrity": "sha512-CBT2AUgVfMTz0tGDk8U++gzx7cnl/WrvX3XBENJ+cSb7PqR/4nz8ONvv3smJGsEM/GR53Efv9gdeBIAln9Mt7w==",
"license": "MIT"
},
"node_modules/@schedule-x/event-modal": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/@schedule-x/event-modal/-/event-modal-2.15.1.tgz",
"integrity": "sha512-pSSI9W7JE7ClU3wXuPNbiirX4kVK6kZ40NryWmRyx1Unko6VmlLLEOeQrqkeEH6A89jTHUCgWyxZWzIdQCLHxQ==",
"license": "MIT",
"peerDependencies": {
"@preact/signals": "^1.1.5",
"preact": "^10.19.2"
}
},
"node_modules/@schedule-x/event-recurrence": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/@schedule-x/event-recurrence/-/event-recurrence-2.15.1.tgz",
"integrity": "sha512-jkkGzofxq4C1mnX4RCbIoA1e5Z4G3+riBALOEusd7AoBXGxUFBtks+f0S1YZH0Taa7WCD6b2RptihwoNbDP6Rg==",
"license": "MIT"
},
"node_modules/@schedule-x/events-service": {
"version": "2.14.3",
"resolved": "https://registry.npmjs.org/@schedule-x/events-service/-/events-service-2.14.3.tgz",
@@ -1953,6 +2042,11 @@
"react-dom": "^16.7.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@schedule-x/resize": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/@schedule-x/resize/-/resize-2.15.1.tgz",
"integrity": "sha512-NPTJ08ATPFcv38uO/9Of1Re1SiU7VZXvVgLVBZpOGGcwDBPt8FzD6LkWCQBgx6Bz+DLLtSd+QjEgt1tOhi2sJA=="
},
"node_modules/@schedule-x/theme-default": {
"version": "2.14.3",
"resolved": "https://registry.npmjs.org/@schedule-x/theme-default/-/theme-default-2.14.3.tgz",

View File

@@ -12,20 +12,26 @@
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@schedule-x/drag-and-drop": "^2.15.1",
"@schedule-x/event-modal": "^2.15.1",
"@schedule-x/event-recurrence": "^2.15.1",
"@schedule-x/events-service": "^2.14.3",
"@schedule-x/react": "^2.13.3",
"@schedule-x/resize": "^2.15.1",
"@schedule-x/theme-default": "^2.14.3",
"@schedule-x/theme-shadcn": "^2.14.3",
"class-variance-authority": "^0.7.1",

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "latosa-escrima.fr",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}