From b723479ec83aa3ba9feb4b383a449dc775ee94b4 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:29:33 +0100 Subject: [PATCH 1/4] Handling CRUD for members in frontend --- backend/api/get_users.go | 7 +- .../app/(auth)/dashboard/members/page.tsx | 10 + frontend/components/app-sidebar.tsx | 4 + frontend/components/member-dialog.tsx | 152 ++++++++++ frontend/components/members-table.tsx | 277 +++++++----------- 5 files changed, 280 insertions(+), 170 deletions(-) create mode 100644 frontend/app/(auth)/dashboard/members/page.tsx create mode 100644 frontend/components/member-dialog.tsx diff --git a/backend/api/get_users.go b/backend/api/get_users.go index 707807e..f577cba 100644 --- a/backend/api/get_users.go +++ b/backend/api/get_users.go @@ -20,9 +20,9 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) { }) if count == 0 { - core.JSONError{ - Status: core.Error, - Message: "Not users.", + core.JSONSuccess{ + Status: core.Success, + Message: "No users.", }.Respond(w, http.StatusNotFound) return } @@ -35,7 +35,6 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) { return } - // TODO : Remove password core.JSONSuccess{ Status: core.Success, Message: "Users found.", diff --git a/frontend/app/(auth)/dashboard/members/page.tsx b/frontend/app/(auth)/dashboard/members/page.tsx new file mode 100644 index 0000000..771ed9d --- /dev/null +++ b/frontend/app/(auth)/dashboard/members/page.tsx @@ -0,0 +1,10 @@ +"use server"; +import MembersTable from "@/components/members-table"; + +export default async function Page({}) { + return ( +
+ +
+ ); +} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index dd6266a..3efcdfb 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -54,6 +54,10 @@ const data = { icon: Users, isActive: true, items: [ + { + title: "Liste des membres", + url: "/dashboard/members", + }, { title: "Création d'un membre", url: "/dashboard/members/new", diff --git a/frontend/components/member-dialog.tsx b/frontend/components/member-dialog.tsx new file mode 100644 index 0000000..9539c55 --- /dev/null +++ b/frontend/components/member-dialog.tsx @@ -0,0 +1,152 @@ +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +const memberSchema = z.object({ + userId: z.string().optional(), + firstname: z.string().min(1, "Prénom est requis."), + lastname: z.string().min(1, "Nom de famille est requis."), + email: z.string().email("Adresse email invalide."), + password: z + .string() + .min(6, "Le mot de passe doit avoir au moins 6 caractères.") + .optional(), + phone: z.string().regex(/^\d{10}$/, "Le numéro doit avoir 10 chiffres."), + role: z.string().min(1, "Le rôle est requis."), +}); + +const updateMemberSchema = memberSchema.partial(); + +export type Member = z.infer; + +interface MemberDialogProps { + isOpen: boolean; + onClose: () => void; + member: Member | null; + onSave: (member: Member) => void; +} + +export default function MemberDialog({ + isOpen, + onClose, + member, + onSave, +}: MemberDialogProps) { + const schema = member?.userId ? updateMemberSchema : memberSchema; + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: member?.userId + ? member + : { + userId: "", + firstname: "", + lastname: "", + email: "", + password: "", + phone: "", + role: "", + }, + }); + + useEffect(() => { + if (member) { + form.reset(member); + } else { + form.reset({ + userId: "", + firstname: "", + lastname: "", + email: "", + password: "", + phone: "", + role: "", + }); + } + }, [member, form]); + + const onSubmit = (data: Member) => { + onSave(data); + onClose(); + }; + + return ( + + + + + {member + ? "Mis à jour du membre" + : "Créer un nouveau membre"} + + +
+ + {( + [ + "userId", + "firstname", + "lastname", + "email", + "password", + "phone", + "role", + ] as const + ).map((field) => ( + ( + + + {field.name + .charAt(0) + .toUpperCase() + + field.name.slice(1)} + + + + + + + )} + /> + ))} + + + + + +
+
+ ); +} diff --git a/frontend/components/members-table.tsx b/frontend/components/members-table.tsx index 399ec59..01d334b 100644 --- a/frontend/components/members-table.tsx +++ b/frontend/components/members-table.tsx @@ -12,93 +12,28 @@ import { 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"; +import MemberDialog, { Member } from "./member-dialog"; +import * as z from "zod"; +import { request, useApi } from "@/hooks/use-api"; +import { + CircleX, + Loader2, + Trash2, + UserRoundPen, + UserRoundPlus, +} from "lucide-react"; -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(initialMembers); +export default function MembersTable() { + const { + data: members, + error, + mutate, + success, + isLoading, + } = useApi("/users", undefined, true, false); const [selectMode, setSelectMode] = useState(false); const [selectedMembers, setSelectedMembers] = useState([]); - const [updateDialogOpen, setUpdateDialogOpen] = useState(false); - const [addDialogOpen, setAddDialogOpen] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); const [currentMember, setCurrentMember] = useState(null); const toggleSelectMode = () => { @@ -114,124 +49,134 @@ export function MembersTable() { ); }; - const handleUpdate = (member: Member) => { + const handleOpenDialog = (member: Member | null) => { setCurrentMember(member); - setUpdateDialogOpen(true); + setDialogOpen(true); }; - const handleDelete = (userId: string) => { - setMembers((prev) => - prev.filter((member) => member.user_id !== userId), - ); + const handleSaveMember = async (savedMember: Member) => { + if (savedMember.userId) { + const res = await request( + `/users/${savedMember.userId}/update`, + { + body: savedMember, + requiresAuth: true, + method: "PATCH", + }, + ); + if (res.status === "Success") mutate(); + // Update existing member + // setMembers((prev) => + // prev.map((m) => + // m.user_id === savedMember.user_id ? savedMember : m, + // ), + // ); + } else { + const res = await request("/users/new", { + body: savedMember, + method: "POST", + requiresAuth: true, + }); + if (res.status === "Success") mutate(); + } }; - const handleAdd = (newMember: Member) => { - setMembers((prev) => [ - ...prev, - { ...newMember, user_id: String(prev.length + 1) }, - ]); + const handleDelete = async (userId: string) => { + const res = await request(`/users/${userId}/delete`, { + method: "DELETE", + requiresAuth: true, + }); + if (res.status === "Success") mutate(); }; return (
-
- + {selectMode && ( - Select + Selectionner )} - User ID - First Name - Last Name + Prénom + Nom Email - Password - Phone - Role + Téléphone + Rôle Actions - {members.map((member) => ( - - {selectMode && ( + {isLoading && } + {members && + members.map((member) => ( + + {selectMode && ( + + + toggleMemberSelection( + member.userId!, + ) + } + /> + + )} - - toggleMemberSelection( - member.user_id, - ) - } - /> + {member.firstname} - )} - {member.user_id} - {member.firstname} - {member.lastname} - {member.email} - {member.password} - {member.phone} - {member.role} - - - - - - ))} + {member.lastname} + {member.email} + {member.phone} + {member.role} + + + + + + ))}
- setUpdateDialogOpen(false)} + setDialogOpen(false)} member={currentMember} - onUpdate={(updatedMember) => { - setMembers((prev) => - prev.map((m) => - m.user_id === updatedMember.user_id - ? updatedMember - : m, - ), - ); - setUpdateDialogOpen(false); - }} - /> - setAddDialogOpen(false)} - onAdd={handleAdd} + onSave={handleSaveMember} />
); From 74118a5aa6e07721d66113818a291aeb4fa84568 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:55:19 +0100 Subject: [PATCH 2/4] Changed form in users list and removed dashboard/users/new --- .../app/(auth)/dashboard/members/new/page.tsx | 233 ------------------ frontend/components/app-sidebar.tsx | 4 - frontend/components/member-dialog.tsx | 180 ++++++++++++-- frontend/components/members-table.tsx | 1 + 4 files changed, 155 insertions(+), 263 deletions(-) delete mode 100644 frontend/app/(auth)/dashboard/members/new/page.tsx diff --git a/frontend/app/(auth)/dashboard/members/new/page.tsx b/frontend/app/(auth)/dashboard/members/new/page.tsx deleted file mode 100644 index 33cc8b8..0000000 --- a/frontend/app/(auth)/dashboard/members/new/page.tsx +++ /dev/null @@ -1,233 +0,0 @@ -"use client"; - -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; - -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import useApiMutation from "@/hooks/use-api"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -const formSchema = z.object({ - firstname: z.string().min(2, { message: "Prénom trop court." }), - lastname: z.string().min(2, { message: "Nom trop court." }), - role: z.enum(["admin", "user"], { message: "Rôle invalide." }), - phone: z - .string() - .max(10, { message: "Un numéro de téléphone à 10 chiffres." }) - .optional(), - email: z.string().email({ message: "Email invalide." }) -}); - -export default function CreateMemberForm() { - const { - trigger - } = useApiMutation( - "/users/new", - { onSuccess: () => console.log("Member created") }, - "POST", - true, - false - ) - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - firstname: "", - lastname: "", - role: "user", - phone: "", - email: "", - }, - }); - - async function onSubmit(values: z.infer) { - try { - const res = await trigger(values) - if (!res) throw new Error("The server hasn't responded."); - if (res.status === "Error") throw new Error(res.message); - } catch (error) { - console.error("Form submission error", error); - } - console.log("submited"); - } - - return ( -
- - - Création de membre - - Remplissez les différents champs pour créer un membre. - Un code sera envoyé par mail à l'utilisateur. - Il pourra ensuite modifier le mot de passe de son compte avec ce code. - - - -
- -
- {/* Firstname Field */} - ( - - - Prénom - - - - - - - )} - /> -
- {/* Firstname Field */} - ( - - - Nom - - - - - - - )} - /> - - {/* Email Field */} - ( - - - Email - - - - - - - )} - /> - - ( - - - Role - - - - - - - )} - /> - - {/* Phone Field */} - ( - - - Phone number - - - - - - - )} - /> - - -
-
-
- -
-
-
- ); -} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index 3efcdfb..06db639 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -58,10 +58,6 @@ const data = { title: "Liste des membres", url: "/dashboard/members", }, - { - title: "Création d'un membre", - url: "/dashboard/members/new", - }, ], }, { diff --git a/frontend/components/member-dialog.tsx b/frontend/components/member-dialog.tsx index 9539c55..730fb4c 100644 --- a/frontend/components/member-dialog.tsx +++ b/frontend/components/member-dialog.tsx @@ -11,7 +11,6 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Form, FormControl, @@ -20,6 +19,15 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "./ui/select"; const memberSchema = z.object({ userId: z.string().optional(), @@ -94,51 +102,171 @@ export default function MemberDialog({ {member - ? "Mis à jour du membre" + ? "Mise à jour du membre" : "Créer un nouveau membre"}
- {( - [ - "userId", - "firstname", - "lastname", - "email", - "password", - "phone", - "role", - ] as const - ).map((field) => ( +
+ {/* Firstname Field */} ( - - - {field.name - .charAt(0) - .toUpperCase() + - field.name.slice(1)} + + + Prénom )} /> - ))} +
+ {/* Firstname Field */} + ( + + + Nom + + + + + + + )} + /> + + {/* Email Field */} + ( + + + Email + + + + + + + )} + /> + {/* Password Field */} + {!member?.userId && ( + ( + + + Mot de passe + + + + + + + )} + /> + )} + + ( + + + Role + + + + + + + )} + /> + + {/* Phone Field */} + ( + + + Phone number + + + + + + + )} + /> +
+