feat(users-management): add users management module

Introduce a new users management module that includes features for creating, reading, updating, and deleting users. The module includes components for user tables, modals for user actions, and hooks for managing user state and interactions. This replaces the previous admin page with a more comprehensive and modular approach to user management.
This commit is contained in:
Daniel Luiz Alves
2025-04-14 10:30:45 -03:00
parent ab6c634782
commit e03ca7e0dc
17 changed files with 837 additions and 22 deletions

View File

@@ -1,16 +0,0 @@
"use client";
import { ProtectedRoute } from "@/components/auth/protected-route";
function AdminPage() {
return (
<ProtectedRoute requireAdmin>
<div>
<h1>Admin Dashboard</h1>
{/* Your admin content */}
</div>
</ProtectedRoute>
);
}
export default AdminPage;

View File

@@ -68,7 +68,7 @@ export function PasswordForm({
<div className="flex justify-end">
<Button className="mt-4 font-semibold" variant="default" disabled={isSubmitting} type="submit">
{!isSubmitting && <IconLock className="mr-2 h-4 w-4" />}
{!isSubmitting && <IconLock className="h-4 w-4" />}
{t("profile.password.updateButton")}
</Button>
</div>

View File

@@ -48,7 +48,7 @@ export function ProfilePicture({ userData, onImageChange, onImageRemove }: Profi
: ""}
</AvatarFallback>
</Avatar>
<DropdownMenu>
<DropdownMenu >
<DropdownMenuTrigger asChild>
<Button
size="icon"
@@ -61,12 +61,12 @@ export function ProfilePicture({ userData, onImageChange, onImageRemove }: Profi
<DropdownMenuContent>
{!!userData?.image ? (
<DropdownMenuItem className="text-destructive" onClick={onImageRemove}>
<IconTrash className="mr-2 h-4 w-4" />
<IconTrash className="h-4 w-4" />
{t("profile.picture.removePhoto")}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={handleAvatarClick}>
<IconCamera className="mr-2 h-4 w-4" />
<IconCamera className="h-4 w-4" />
{t("profile.picture.uploadPhoto")}
</DropdownMenuItem>
)}

View File

@@ -0,0 +1,62 @@
import { UserActionsDropdownProps } from "../types";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { useTranslations } from "next-intl";
import {
IconDotsVertical,
IconEdit,
IconTrash,
IconCheck,
IconBan
} from "@tabler/icons-react";
export function UserActionsDropdown({
user,
isCurrentUser,
onEdit,
onDelete,
onToggleStatus,
}: UserActionsDropdownProps) {
const t = useTranslations();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={isCurrentUser ? "hidden" : ""}
disabled={isCurrentUser}
>
<IconDotsVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onEdit(user)}>
<IconEdit className="mr-2 h-4 w-4" />
{t("users.actions.edit")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onToggleStatus(user)}>
{user.isActive ? (
<IconBan className="mr-2 h-4 w-4" />
) : (
<IconCheck className="mr-2 h-4 w-4" />
)}
{user.isActive ? t("users.actions.deactivate") : t("users.actions.activate")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(user)}
className="text-destructive"
>
<IconTrash className="mr-2 h-4 w-4" />
{t("users.actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,41 @@
import { UserDeleteModalProps } from "../types";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
} from "@/components/ui/dialog";
import { useTranslations } from "next-intl";
export function UserDeleteModal({ isOpen, onClose, user, onConfirm }: UserDeleteModalProps) {
const t = useTranslations();
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader className="flex flex-col gap-1">
{t("users.delete.title")}
</DialogHeader>
<div className="py-4">
{user && (
<p>
{t("users.delete.confirmation", {
firstName: user.firstName,
lastName: user.lastName,
})}
</p>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
{t("common.cancel")}
</Button>
<Button variant="destructive" onClick={onConfirm}>
{t("users.delete.confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,142 @@
import { UserFormModalProps } from "../types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslations } from "next-intl";
import { IconDeviceFloppy } from "@tabler/icons-react";
import { Label } from "@/components/ui/label";
import { FormMessage } from "@/components/ui/form";
export function UserFormModal({ isOpen, onClose, modalMode, selectedUser, formMethods, onSubmit }: UserFormModalProps) {
const t = useTranslations();
const {
register,
formState: { errors, isSubmitting },
} = formMethods;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<form onSubmit={formMethods.handleSubmit(onSubmit)}>
<DialogHeader>
{modalMode === "create" ? t("users.form.titleCreate") : t("users.form.titleEdit")}
</DialogHeader>
<div className="py-4">
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>{t("users.form.firstName")}</Label>
<Input
{...register("firstName")}
className={errors.firstName ? "border-destructive" : ""}
/>
{errors.firstName && (
<FormMessage>{errors.firstName.message}</FormMessage>
)}
</div>
<div className="space-y-2">
<Label>{t("users.form.lastName")}</Label>
<Input
{...register("lastName")}
className={errors.lastName ? "border-destructive" : ""}
/>
{errors.lastName && (
<FormMessage>{errors.lastName.message}</FormMessage>
)}
</div>
</div>
<div className="space-y-2">
<Label>{t("users.form.username")}</Label>
<Input
{...register("username")}
className={errors.username ? "border-destructive" : ""}
/>
{errors.username && (
<FormMessage>{errors.username.message}</FormMessage>
)}
</div>
<div className="space-y-2">
<Label>{t("users.form.email")}</Label>
<Input
{...register("email")}
type="email"
className={errors.email ? "border-destructive" : ""}
/>
{errors.email && (
<FormMessage>{errors.email.message}</FormMessage>
)}
</div>
<div className="space-y-2">
<Label>
{modalMode === "create"
? t("users.form.password")
: t("users.form.newPassword")}
</Label>
<Input
{...register("password")}
type="password"
className={errors.password ? "border-destructive" : ""}
placeholder={
modalMode === "edit" ? t("users.form.passwordPlaceholder") : undefined
}
/>
{errors.password && (
<FormMessage>{errors.password.message}</FormMessage>
)}
</div>
{modalMode === "edit" && (
<div className="space-y-2">
<Label>{t("users.form.role")}</Label>
<Select
defaultValue={selectedUser?.isAdmin ? "true" : "false"}
{...register("isAdmin")}
>
<SelectTrigger className={errors.isAdmin ? "border-destructive" : ""}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="false">{t("users.form.roleUser")}</SelectItem>
<SelectItem value="true">{t("users.form.roleAdmin")}</SelectItem>
</SelectContent>
</Select>
{errors.isAdmin && (
<FormMessage>{errors.isAdmin.message}</FormMessage>
)}
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} type="button">
{t("common.cancel")}
</Button>
<Button disabled={isSubmitting} type="submit">
{modalMode === "create" ? (
""
) : (
<IconDeviceFloppy className="mr-2 h-4 w-4" />
)}
{modalMode === "create" ? t("users.form.create") : t("users.form.save")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,42 @@
import { UserManagementModalsProps } from "../types";
import { UserDeleteModal } from "./user-delete-modal";
import { UserFormModal } from "./user-form-modal";
import { UserStatusModal } from "./user-status-modal";
export function UserManagementModals({
modals,
selectedUser,
deleteModalUser,
statusModalUser,
onSubmit,
onDelete,
onToggleStatus,
formMethods,
}: UserManagementModalsProps) {
return (
<>
<UserFormModal
formMethods={formMethods}
isOpen={modals.isOpen}
modalMode={modals.modalMode}
selectedUser={selectedUser}
onClose={modals.onClose}
onSubmit={onSubmit}
/>
<UserDeleteModal
isOpen={modals.isDeleteModalOpen}
user={deleteModalUser}
onClose={modals.onDeleteModalClose}
onConfirm={onDelete}
/>
<UserStatusModal
isOpen={modals.isStatusModalOpen}
user={statusModalUser}
onClose={modals.onStatusModalClose}
onConfirm={onToggleStatus}
/>
</>
);
}

View File

@@ -0,0 +1,46 @@
import { UserStatusModalProps } from "../types";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
} from "@/components/ui/dialog";
import { useTranslations } from "next-intl";
export function UserStatusModal({ isOpen, onClose, user, onConfirm }: UserStatusModalProps) {
const t = useTranslations();
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader className="flex flex-col gap-1">
{t("users.status.title")}
</DialogHeader>
<div className="py-4">
{user && (
<p>
{t("users.status.confirmation", {
action: user.isActive ? t("users.status.deactivate") : t("users.status.activate"),
firstName: user.firstName,
lastName: user.lastName,
})}
</p>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
{t("common.cancel")}
</Button>
<Button
variant={user?.isActive ? "destructive" : "default"}
onClick={onConfirm}
>
{user?.isActive ? t("users.status.deactivate") : t("users.status.activate")}{" "}
{t("users.status.user")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,54 @@
import { UsersHeaderProps } from "../types";
import Link from "next/link";
import { IconLayoutDashboard, IconUsers, IconUserPlus } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
export function UsersHeader({ onCreateUser }: UsersHeaderProps) {
const t = useTranslations();
return (
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<IconUsers className="text-2xl" />
<h1 className="text-2xl font-bold">{t("users.header.title")}</h1>
</div>
<Button
className="font-semibold"
onClick={onCreateUser}
>
<IconUserPlus size={18} className="mr-2" />
{t("users.header.addUser")}
</Button>
</div>
<Separator />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/dashboard" className="flex items-center">
<IconLayoutDashboard size={20} className="mr-2" />
{t("common.dashboard")}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<span className="flex items-center gap-2">
<IconUsers size={20} /> {t("users.header.management")}
</span>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
);
}

View File

@@ -0,0 +1,84 @@
import { UsersTableProps } from "../types";
import { UserActionsDropdown } from "./user-actions-dropdown";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useTranslations } from "next-intl";
export function UsersTable({ users, currentUser, onEdit, onDelete, onToggleStatus }: UsersTableProps) {
const t = useTranslations();
const isCurrentUser = (userId: string) => currentUser?.id === userId;
return (
<div className="rounded-lg shadow-sm overflow-hidden border">
<Table>
<TableHeader>
<TableRow className="border-b-0">
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("users.table.user")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("users.table.email")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("users.table.status")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("users.table.role")}
</TableHead>
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("users.table.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} className="hover:bg-muted/50 transition-colors border-0">
<TableCell className="h-12 px-4">
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12">
<AvatarImage src={user.image || ""} alt={user.username} />
<AvatarFallback className="bg-primary/10 text-primary font-bold text-xl">
{user.firstName[0]}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{`${user.firstName} ${user.lastName}`}</p>
<p className="text-sm text-muted-foreground">{user.username}</p>
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">{user.email}</TableCell>
<TableCell className="h-12 px-4">
<Badge variant={user.isActive ? "default" : "destructive"}>
{user.isActive ? t("users.table.active") : t("users.table.inactive")}
</Badge>
</TableCell>
<TableCell className="h-12 px-4">
<Badge variant={user.isAdmin ? "destructive" : "secondary"}>
{user.isAdmin ? t("users.table.admin") : t("users.table.userr")}
</Badge>
</TableCell>
<TableCell className="h-12 px-4">
<UserActionsDropdown
isCurrentUser={isCurrentUser(user.id)}
user={user}
onDelete={onDelete}
onEdit={onEdit}
onToggleStatus={onToggleStatus}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,180 @@
"use client"
import { useAuth } from "@/contexts/auth-context";
import { useDisclosure } from "@/hooks/use-disclosure";
import { listUsers, registerUser, updateUser, deleteUser, activateUser, deactivateUser } from "@/http/endpoints";
import type { ListUsers200Item } from "@/http/models";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { Resolver, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const createSchemas = (t: (key: string) => string) => ({
userSchema: z.object({
firstName: z.string().min(1, t("validation.firstNameRequired")),
lastName: z.string().min(1, t("validation.lastNameRequired")),
username: z
.string()
.min(3, t("validation.usernameLength"))
.regex(/^[^\s]+$/, t("validation.usernameSpaces")),
email: z.string().email(t("validation.invalidEmail")),
password: z.string().min(8, t("validation.passwordLength")).or(z.literal("")),
isAdmin: z
.union([z.enum(["true", "false"]), z.boolean()])
.transform((val) => (typeof val === "string" ? val === "true" : val))
.optional(),
}),
});
export type UserFormData = z.infer<ReturnType<typeof createSchemas>["userSchema"]>;
export function useUserManagement() {
const t = useTranslations();
const { userSchema } = createSchemas(t);
const [users, setUsers] = useState<ListUsers200Item[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<ListUsers200Item | null>(null);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [deleteModalUser, setDeleteModalUser] = useState<ListUsers200Item | null>(null);
const [statusModalUser, setStatusModalUser] = useState<ListUsers200Item | null>(null);
const { user: currentUser } = useAuth();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isDeleteModalOpen, onOpen: onDeleteModalOpen, onClose: onDeleteModalClose } = useDisclosure();
const { isOpen: isStatusModalOpen, onOpen: onStatusModalOpen, onClose: onStatusModalClose } = useDisclosure();
const formMethods = useForm<UserFormData>({
resolver: zodResolver(userSchema) as Resolver<UserFormData>,
});
const loadUsers = async () => {
try {
const response = await listUsers();
setUsers(response.data);
} catch (error) {
toast.error(t("users.errors.loadFailed"));
console.error(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadUsers();
}, []);
const handleCreateUser = () => {
setModalMode("create");
setSelectedUser(null);
formMethods.reset({});
onOpen();
};
const handleEditUser = (user: ListUsers200Item) => {
setModalMode("edit");
setSelectedUser(user);
formMethods.reset({
firstName: user.firstName,
lastName: user.lastName,
username: user.username,
email: user.email,
isAdmin: user.isAdmin,
password: "",
});
onOpen();
};
const onSubmit = async (data: UserFormData) => {
try {
if (modalMode === "create") {
await registerUser(data);
toast.success(t("users.messages.createSuccess"));
} else {
if (!selectedUser) return;
const updateData = {
...data,
id: selectedUser.id,
} as { id: string } & Partial<typeof data>;
if (!data.password || data.password.trim() === "") {
delete updateData.password;
}
await updateUser(updateData);
toast.success(t("users.messages.updateSuccess"));
}
onClose();
loadUsers();
} catch (error) {
toast.error(t("users.errors.submitFailed", { mode: t(`users.modes.${modalMode}`) }));
console.error(error);
}
};
const handleDeleteUser = async () => {
if (!deleteModalUser) return;
try {
await deleteUser(deleteModalUser.id);
toast.success(t("users.messages.deleteSuccess"));
loadUsers();
onDeleteModalClose();
} catch (error) {
toast.error(t("users.errors.deleteFailed"));
console.error(error);
}
};
const handleToggleUserStatus = async () => {
if (!statusModalUser) return;
try {
if (statusModalUser.isActive) {
await deactivateUser(statusModalUser.id);
toast.success(t("users.messages.deactivateSuccess"));
} else {
await activateUser(statusModalUser.id);
toast.success(t("users.messages.activateSuccess"));
}
loadUsers();
onStatusModalClose();
} catch (error) {
toast.error(t("users.errors.statusUpdateFailed"));
console.error(error);
}
};
return {
users,
isLoading,
currentUser,
selectedUser,
deleteModalUser,
statusModalUser,
modals: {
isOpen,
onOpen,
onClose,
modalMode,
isDeleteModalOpen,
onDeleteModalOpen,
onDeleteModalClose,
isStatusModalOpen,
onStatusModalOpen,
onStatusModalClose,
setDeleteModalUser,
setStatusModalUser,
},
handleCreateUser,
handleEditUser,
handleDeleteUser,
handleToggleUserStatus,
onSubmit,
formMethods,
};
}

View File

@@ -0,0 +1,18 @@
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
interface LayoutProps {
children: React.ReactNode;
}
export async function generateMetadata(): Promise<Metadata> {
const translate = await getTranslations();
return {
title: translate("settings.pageTitle"),
};
}
export default function UsersManagementLayout({ children }: LayoutProps) {
return <>{children}</>;
}

View File

@@ -0,0 +1,72 @@
"use client";
import { ProtectedRoute } from "@/components/auth/protected-route";
import { UserManagementModals } from "./components/user-management-modals";
import { UsersHeader } from "./components/users-header";
import { UsersTable } from "./components/users-table";
import { useUserManagement } from "./hooks/use-user-management";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { Navbar } from "@/components/layout/navbar";
import { DefaultFooter } from "@/components/ui/default-footer";
export default function AdminAreaPage() {
const {
users,
isLoading,
currentUser,
modals,
selectedUser,
deleteModalUser,
statusModalUser,
handleCreateUser,
handleEditUser,
handleDeleteUser,
handleToggleUserStatus,
onSubmit,
formMethods,
} = useUserManagement();
if (isLoading) {
return <LoadingScreen />;
}
return (
<ProtectedRoute requireAdmin>
<div className="w-full h-screen flex flex-col">
<Navbar />
<div className="flex-1 max-w-7xl mx-auto w-full py-8 px-6">
<div className="flex flex-col gap-8">
<UsersHeader onCreateUser={handleCreateUser} />
<UsersTable
currentUser={currentUser}
users={users}
onDelete={(user) => {
modals.setDeleteModalUser(user);
modals.onDeleteModalOpen();
}}
onEdit={handleEditUser}
onToggleStatus={(user) => {
modals.setStatusModalUser(user);
modals.onStatusModalOpen();
}}
/>
</div>
</div>
<DefaultFooter />
<UserManagementModals
deleteModalUser={deleteModalUser}
formMethods={formMethods}
modals={modals}
selectedUser={selectedUser}
statusModalUser={statusModalUser}
onDelete={handleDeleteUser}
onSubmit={onSubmit}
onToggleStatus={handleToggleUserStatus}
/>
</div>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,74 @@
import { UserFormData } from "../hooks/use-user-management";
import type { ListUsers200Item } from "@/http/models";
import { UseFormReturn } from "react-hook-form";
export interface UserActionsDropdownProps {
user: ListUsers200Item;
isCurrentUser: boolean;
onEdit: (user: ListUsers200Item) => void;
onDelete: (user: ListUsers200Item) => void;
onToggleStatus: (user: ListUsers200Item) => void;
}
export interface UserDeleteModalProps {
isOpen: boolean;
onClose: () => void;
user: ListUsers200Item | null;
onConfirm: () => Promise<void>;
}
export interface UserFormModalProps {
isOpen: boolean;
onClose: () => void;
modalMode: "create" | "edit";
selectedUser: ListUsers200Item | null;
formMethods: UseFormReturn<UserFormData>;
onSubmit: (data: UserFormData) => Promise<void>;
}
export interface UserManagementModalsProps {
modals: {
isOpen: boolean;
onClose: () => void;
modalMode: "create" | "edit";
isDeleteModalOpen: boolean;
onDeleteModalClose: () => void;
isStatusModalOpen: boolean;
onStatusModalClose: () => void;
};
selectedUser: ListUsers200Item | null;
deleteModalUser: ListUsers200Item | null;
statusModalUser: ListUsers200Item | null;
onSubmit: (data: UserFormData) => Promise<void>;
onDelete: () => Promise<void>;
onToggleStatus: () => Promise<void>;
formMethods: UseFormReturn<UserFormData>;
}
export interface UserStatusModalProps {
isOpen: boolean;
onClose: () => void;
user: ListUsers200Item | null;
onConfirm: () => Promise<void>;
}
export interface UsersHeaderProps {
onCreateUser: () => void;
}
export interface AuthUser {
id: string;
firstName: string;
lastName: string;
username: string;
email: string;
image: string | null;
}
export interface UsersTableProps {
users: ListUsers200Item[];
currentUser: AuthUser | null;
onEdit: (user: ListUsers200Item) => void;
onDelete: (user: ListUsers200Item) => void;
onToggleStatus: (user: ListUsers200Item) => void;
}

View File

@@ -77,7 +77,7 @@ export function Navbar() {
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/admin" className="flex items-center gap-2 cursor-pointer">
<Link href="/users-management" className="flex items-center gap-2 cursor-pointer">
<IconUsers className="h-4 w-4" />
{t("navbar.usersManagement")}
</Link>

View File

@@ -57,7 +57,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground cursor-pointer data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -0,0 +1,16 @@
import { useState } from "react";
export function useDisclosure(initial = false) {
const [isOpen, setIsOpen] = useState(initial);
const onOpen = () => setIsOpen(true);
const onClose = () => setIsOpen(false);
const onToggle = () => setIsOpen(!isOpen);
return {
isOpen,
onOpen,
onClose,
onToggle,
};
}