Handling CRUD for members in frontend

This commit is contained in:
cdricms
2025-01-28 13:29:33 +01:00
parent ac7e97527f
commit b723479ec8
5 changed files with 280 additions and 170 deletions

View File

@@ -20,9 +20,9 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
}) })
if count == 0 { if count == 0 {
core.JSONError{ core.JSONSuccess{
Status: core.Error, Status: core.Success,
Message: "Not users.", Message: "No users.",
}.Respond(w, http.StatusNotFound) }.Respond(w, http.StatusNotFound)
return return
} }
@@ -35,7 +35,6 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
// TODO : Remove password
core.JSONSuccess{ core.JSONSuccess{
Status: core.Success, Status: core.Success,
Message: "Users found.", Message: "Users found.",

View File

@@ -0,0 +1,10 @@
"use server";
import MembersTable from "@/components/members-table";
export default async function Page({}) {
return (
<div className="container mx-auto px-4 py-10">
<MembersTable />
</div>
);
}

View File

@@ -54,6 +54,10 @@ const data = {
icon: Users, icon: Users,
isActive: true, isActive: true,
items: [ items: [
{
title: "Liste des membres",
url: "/dashboard/members",
},
{ {
title: "Création d'un membre", title: "Création d'un membre",
url: "/dashboard/members/new", url: "/dashboard/members/new",

View File

@@ -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<typeof memberSchema>;
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<Member>({
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{member
? "Mis à jour du membre"
: "Créer un nouveau membre"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
{(
[
"userId",
"firstname",
"lastname",
"email",
"password",
"phone",
"role",
] as const
).map((field) => (
<FormField
key={field}
control={form.control}
name={field}
render={({ field }) => (
<FormItem>
<FormLabel>
{field.name
.charAt(0)
.toUpperCase() +
field.name.slice(1)}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={
field.name === "userId"
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<DialogFooter>
<Button type="submit">
{member ? "Actualiser" : "Ajouter"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@@ -12,93 +12,28 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { UpdateMemberDialog } from "./UpdateMemberDialog"; import MemberDialog, { Member } from "./member-dialog";
import { AddMemberDialog } from "./AddMemberDialog"; import * as z from "zod";
import { request, useApi } from "@/hooks/use-api";
import {
CircleX,
Loader2,
Trash2,
UserRoundPen,
UserRoundPlus,
} from "lucide-react";
interface Member { export default function MembersTable() {
user_id: string; const {
firstname: string; data: members,
lastname: string; error,
email: string; mutate,
password: string; success,
phone: string; isLoading,
role: string; } = useApi<Member[]>("/users", undefined, true, false);
}
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 [selectMode, setSelectMode] = useState(false);
const [selectedMembers, setSelectedMembers] = useState<string[]>([]); const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
const [updateDialogOpen, setUpdateDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [currentMember, setCurrentMember] = useState<Member | null>(null); const [currentMember, setCurrentMember] = useState<Member | null>(null);
const toggleSelectMode = () => { const toggleSelectMode = () => {
@@ -114,124 +49,134 @@ export function MembersTable() {
); );
}; };
const handleUpdate = (member: Member) => { const handleOpenDialog = (member: Member | null) => {
setCurrentMember(member); setCurrentMember(member);
setUpdateDialogOpen(true); setDialogOpen(true);
}; };
const handleDelete = (userId: string) => { const handleSaveMember = async (savedMember: Member) => {
setMembers((prev) => if (savedMember.userId) {
prev.filter((member) => member.user_id !== userId), const res = await request<unknown>(
); `/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<unknown>("/users/new", {
body: savedMember,
method: "POST",
requiresAuth: true,
});
if (res.status === "Success") mutate();
}
}; };
const handleAdd = (newMember: Member) => { const handleDelete = async (userId: string) => {
setMembers((prev) => [ const res = await request<unknown>(`/users/${userId}/delete`, {
...prev, method: "DELETE",
{ ...newMember, user_id: String(prev.length + 1) }, requiresAuth: true,
]); });
if (res.status === "Success") mutate();
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between"> <div className="flex justify-between">
<Button onClick={toggleSelectMode}> <Button onClick={toggleSelectMode}>
{selectMode ? "Cancel Selection" : "Select"} {selectMode ? <CircleX /> : "Selectionner"}
</Button> </Button>
<Button onClick={() => setAddDialogOpen(true)}> <Button onClick={() => handleOpenDialog(null)}>
Add New Member <UserRoundPlus />
</Button> </Button>
</div> </div>
<div className="relative"> <div className="relative">
<ScrollArea className="h-[400px] rounded-md border"> <ScrollArea className="h-full rounded-md border">
<Table> <Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm"> <TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow> <TableRow>
{selectMode && ( {selectMode && (
<TableHead className="w-[50px]"> <TableHead className="w-[50px]">
Select Selectionner
</TableHead> </TableHead>
)} )}
<TableHead>User ID</TableHead> <TableHead>Prénom</TableHead>
<TableHead>First Name</TableHead> <TableHead>Nom</TableHead>
<TableHead>Last Name</TableHead>
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Password</TableHead> <TableHead>Téléphone</TableHead>
<TableHead>Phone</TableHead> <TableHead>Rôle</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
Actions Actions
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{members.map((member) => ( {isLoading && <Loader2 className="animate-spin" />}
<TableRow key={member.user_id}> {members &&
{selectMode && ( members.map((member) => (
<TableRow key={member.userId}>
{selectMode && (
<TableCell>
<Checkbox
checked={selectedMembers.includes(
member.userId!,
)}
onCheckedChange={() =>
toggleMemberSelection(
member.userId!,
)
}
/>
</TableCell>
)}
<TableCell> <TableCell>
<Checkbox {member.firstname}
checked={selectedMembers.includes(
member.user_id,
)}
onCheckedChange={() =>
toggleMemberSelection(
member.user_id,
)
}
/>
</TableCell> </TableCell>
)} <TableCell>{member.lastname}</TableCell>
<TableCell>{member.user_id}</TableCell> <TableCell>{member.email}</TableCell>
<TableCell>{member.firstname}</TableCell> <TableCell>{member.phone}</TableCell>
<TableCell>{member.lastname}</TableCell> <TableCell>{member.role}</TableCell>
<TableCell>{member.email}</TableCell> <TableCell className="text-right">
<TableCell>{member.password}</TableCell> <Button
<TableCell>{member.phone}</TableCell> variant="outline"
<TableCell>{member.role}</TableCell> size="sm"
<TableCell className="text-right"> className="mr-2"
<Button onClick={() =>
variant="outline" handleOpenDialog(member)
size="sm" }
className="mr-2" >
onClick={() => handleUpdate(member)} <UserRoundPen />
> </Button>
Modify <Button
</Button> variant="destructive"
<Button size="sm"
variant="destructive" onClick={() =>
size="sm" handleDelete(member.userId!)
onClick={() => }
handleDelete(member.user_id) >
} <Trash2 />
> </Button>
Delete </TableCell>
</Button> </TableRow>
</TableCell> ))}
</TableRow>
))}
</TableBody> </TableBody>
</Table> </Table>
</ScrollArea> </ScrollArea>
</div> </div>
<UpdateMemberDialog <MemberDialog
isOpen={updateDialogOpen} isOpen={dialogOpen}
onClose={() => setUpdateDialogOpen(false)} onClose={() => setDialogOpen(false)}
member={currentMember} member={currentMember}
onUpdate={(updatedMember) => { onSave={handleSaveMember}
setMembers((prev) =>
prev.map((m) =>
m.user_id === updatedMember.user_id
? updatedMember
: m,
),
);
setUpdateDialogOpen(false);
}}
/>
<AddMemberDialog
isOpen={addDialogOpen}
onClose={() => setAddDialogOpen(false)}
onAdd={handleAdd}
/> />
</div> </div>
); );