feat(admin): add edit dialogs for users, contents, and categories
- Implemented edit functionality for users, contents, and categories, including modals for updating records. - Enhanced table actions with edit buttons alongside delete. - Improved user, content, and category fetching with `useCallback` to optimize re-renders. - Added skeleton loaders and UI updates for better user experience.
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { Edit, Plus, Trash2 } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -12,24 +15,57 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
|
import { CategoryDialog } from "./category-dialog";
|
||||||
|
|
||||||
export default function AdminCategoriesPage() {
|
export default function AdminCategoriesPage() {
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<Category | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchCategories = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
CategoryService.getAll()
|
CategoryService.getAll()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCategories();
|
||||||
|
}, [fetchCategories]);
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!confirm("Êtes-vous sûr de vouloir supprimer cette catégorie ?")) return;
|
||||||
|
try {
|
||||||
|
await CategoryService.remove(id);
|
||||||
|
setCategories(categories.filter((c) => c.id !== id));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (category: Category) => {
|
||||||
|
setSelectedCategory(category);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
setSelectedCategory(null);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-3xl font-bold tracking-tight">
|
<h2 className="text-3xl font-bold tracking-tight">
|
||||||
Catégories ({categories.length})
|
Catégories ({categories.length})
|
||||||
</h2>
|
</h2>
|
||||||
|
<Button onClick={handleCreate}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> Ajouter une catégorie
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -38,6 +74,7 @@ export default function AdminCategoriesPage() {
|
|||||||
<TableHead>Nom</TableHead>
|
<TableHead>Nom</TableHead>
|
||||||
<TableHead>Slug</TableHead>
|
<TableHead>Slug</TableHead>
|
||||||
<TableHead>Description</TableHead>
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead className="w-[100px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -54,11 +91,14 @@ export default function AdminCategoriesPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Skeleton className="h-4 w-[250px]" />
|
<Skeleton className="h-4 w-[250px]" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : categories.length === 0 ? (
|
) : categories.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-center h-24">
|
<TableCell colSpan={4} className="text-center h-24">
|
||||||
Aucune catégorie trouvée.
|
Aucune catégorie trouvée.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -66,18 +106,55 @@ export default function AdminCategoriesPage() {
|
|||||||
categories.map((category) => (
|
categories.map((category) => (
|
||||||
<TableRow key={category.id}>
|
<TableRow key={category.id}>
|
||||||
<TableCell className="font-medium whitespace-nowrap">
|
<TableCell className="font-medium whitespace-nowrap">
|
||||||
{category.name}
|
<div className="flex items-center gap-2">
|
||||||
|
{category.iconUrl && (
|
||||||
|
<div className="relative h-6 w-6">
|
||||||
|
<Image
|
||||||
|
src={category.iconUrl}
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
className="rounded object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{category.name}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap">{category.slug}</TableCell>
|
<TableCell className="whitespace-nowrap">{category.slug}</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{category.description || "Aucune description"}
|
{category.description || "Aucune description"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleEdit(category)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(category.id)}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<CategoryDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
category={selectedCategory}
|
||||||
|
onSuccess={fetchCategories}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import { Download, Eye, Image as ImageIcon, Trash2, Video } from "lucide-react";
|
import {
|
||||||
import { useEffect, useState } from "react";
|
Download,
|
||||||
|
Edit,
|
||||||
|
Eye,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Trash2,
|
||||||
|
Video,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -17,13 +24,17 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
import type { Content } from "@/types/content";
|
import type { Content } from "@/types/content";
|
||||||
|
import { ContentEditDialog } from "./content-edit-dialog";
|
||||||
|
|
||||||
export default function AdminContentsPage() {
|
export default function AdminContentsPage() {
|
||||||
const [contents, setContents] = useState<Content[]>([]);
|
const [contents, setContents] = useState<Content[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [selectedContent, setSelectedContent] = useState<Content | null>(null);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchContents = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
ContentService.getExplore({ limit: 20 })
|
ContentService.getExplore({ limit: 20 })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setContents(res.data);
|
setContents(res.data);
|
||||||
@@ -33,6 +44,10 @@ export default function AdminContentsPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchContents();
|
||||||
|
}, [fetchContents]);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (!confirm("Êtes-vous sûr de vouloir supprimer ce contenu ?")) return;
|
if (!confirm("Êtes-vous sûr de vouloir supprimer ce contenu ?")) return;
|
||||||
|
|
||||||
@@ -45,6 +60,11 @@ export default function AdminContentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEdit = (content: Content) => {
|
||||||
|
setSelectedContent(content);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -61,7 +81,7 @@ export default function AdminContentsPage() {
|
|||||||
<TableHead>Auteur</TableHead>
|
<TableHead>Auteur</TableHead>
|
||||||
<TableHead>Stats</TableHead>
|
<TableHead>Stats</TableHead>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead>Date</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[100px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -84,11 +104,14 @@ export default function AdminContentsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Skeleton className="h-4 w-[100px]" />
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : contents.length === 0 ? (
|
) : contents.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center h-24">
|
<TableCell colSpan={6} className="text-center h-24">
|
||||||
Aucun contenu trouvé.
|
Aucun contenu trouvé.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -132,14 +155,23 @@ export default function AdminContentsPage() {
|
|||||||
{format(new Date(content.createdAt), "dd/MM/yyyy", { locale: fr })}
|
{format(new Date(content.createdAt), "dd/MM/yyyy", { locale: fr })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
onClick={() => handleDelete(content.id)}
|
size="icon"
|
||||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
onClick={() => handleEdit(content)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(content.id)}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
@@ -147,6 +179,12 @@ export default function AdminContentsPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<ContentEditDialog
|
||||||
|
content={selectedContent}
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
onSuccess={fetchContents}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Edit, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -17,13 +17,17 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
import type { User } from "@/types/user";
|
import type { User } from "@/types/user";
|
||||||
|
import { UserEditDialog } from "./user-edit-dialog";
|
||||||
|
|
||||||
export default function AdminUsersPage() {
|
export default function AdminUsersPage() {
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchUsers = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
UserService.getUsersAdmin()
|
UserService.getUsersAdmin()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setUsers(res.data);
|
setUsers(res.data);
|
||||||
@@ -35,6 +39,10 @@ export default function AdminUsersPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const handleDelete = async (uuid: string) => {
|
const handleDelete = async (uuid: string) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
@@ -52,6 +60,11 @@ export default function AdminUsersPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEdit = (user: User) => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -68,7 +81,7 @@ export default function AdminUsersPage() {
|
|||||||
<TableHead>Rôle</TableHead>
|
<TableHead>Rôle</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Date d'inscription</TableHead>
|
<TableHead>Date d'inscription</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[100px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -91,11 +104,14 @@ export default function AdminUsersPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Skeleton className="h-4 w-[100px]" />
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : users.length === 0 ? (
|
) : users.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center h-24">
|
<TableCell colSpan={6} className="text-center h-24">
|
||||||
Aucun utilisateur trouvé.
|
Aucun utilisateur trouvé.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -113,7 +129,15 @@ export default function AdminUsersPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={user.status === "active" ? "success" : "destructive"}>
|
<Badge
|
||||||
|
variant={
|
||||||
|
user.status === "active"
|
||||||
|
? "success"
|
||||||
|
: user.status === "suspended"
|
||||||
|
? "destructive"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
>
|
||||||
{user.status}
|
{user.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -121,14 +145,19 @@ export default function AdminUsersPage() {
|
|||||||
{format(new Date(user.createdAt), "PPP", { locale: fr })}
|
{format(new Date(user.createdAt), "PPP", { locale: fr })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="ghost"
|
<Button variant="ghost" size="icon" onClick={() => handleEdit(user)}>
|
||||||
size="icon"
|
<Edit className="h-4 w-4" />
|
||||||
onClick={() => handleDelete(user.uuid)}
|
</Button>
|
||||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
<Trash2 className="h-4 w-4" />
|
size="icon"
|
||||||
</Button>
|
onClick={() => handleDelete(user.uuid)}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
@@ -136,6 +165,12 @@ export default function AdminUsersPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<UserEditDialog
|
||||||
|
user={selectedUser}
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
onSuccess={fetchUsers}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user