diff --git a/sigap-website/app/(pages)/(admin)/_components/app-sidebar.tsx b/sigap-website/app/(pages)/(admin)/_components/app-sidebar.tsx index a2ccd46..6e63b9a 100644 --- a/sigap-website/app/(pages)/(admin)/_components/app-sidebar.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/app-sidebar.tsx @@ -16,7 +16,7 @@ import { import { NavPreMain } from "./navigations/nav-pre-main"; import { navData } from "@/prisma/data/nav"; import { TeamSwitcher } from "../../../_components/team-switcher"; -import { useGetCurrentUserQuery } from "../dashboard/user-management/queries"; +import { useGetCurrentUserQuery } from "../dashboard/user-management/_queries/queries"; export function AppSidebar({ ...props }: React.ComponentProps) { diff --git a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx index 7d51026..320ac47 100644 --- a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx @@ -27,8 +27,8 @@ import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react"; import type { IUserSchema } from "@/src/entities/models/users/users.model"; // import { signOut } from "@/app/(pages)/(auth)/action"; import { SettingsDialog } from "../settings/setting-dialog"; -import { useSignOutHandler } from "@/app/(pages)/(auth)/handler"; import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog"; +import { useSignOutHandler } from "@/app/(pages)/(auth)/_handlers/use-sign-out"; export function NavUser({ user }: { user: IUserSchema | null }) { const { isMobile } = useSidebar(); diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/add-user-dialog.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/add-user-dialog.tsx index c508c13..5e7ad5f 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/add-user-dialog.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/add-user-dialog.tsx @@ -1,8 +1,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog" import { Button } from "@/app/_components/ui/button" import { Mail, Lock, Loader2 } from "lucide-react" -import { useAddUserDialogHandler } from "../handler" import { ReactHookFormField } from "@/app/_components/react-hook-form-field" +import { useAddUserDialogHandler } from "../_handlers/use-add-user-dialog" interface AddUserDialogProps { open: boolean diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/invite-user.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/invite-user.tsx index 3e21a63..157ee6c 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/invite-user.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/invite-user.tsx @@ -16,10 +16,10 @@ import { Textarea } from "@/app/_components/ui/textarea"; import { useMutation } from "@tanstack/react-query"; import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { toast } from "sonner"; -import { useInviteUserHandler } from "../handler"; import { ReactHookFormField } from "@/app/_components/react-hook-form-field"; import { Loader2, MailIcon } from "lucide-react"; import { Separator } from "@/app/_components/ui/separator"; +import { useInviteUserHandler } from "../_handlers/use-invite-user"; interface InviteUserDialogProps { diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx index 149b8e8..174d8b5 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/sheet.tsx @@ -39,9 +39,9 @@ import { } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { format } from "date-fns"; import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action"; -import { useUserDetailSheetHandlers } from "../handler"; import { IUserSchema } from "@/src/entities/models/users/users.model"; import { formatDate } from "@/app/_utils/common"; +import { useUserDetailSheetHandlers } from "../_handlers/use-detail-sheet"; interface UserDetailSheetProps { open: boolean; @@ -56,104 +56,6 @@ export function UserDetailSheet({ user, onUserUpdated, }: UserDetailSheetProps) { - // const [isDeleting, setIsDeleting] = useState(false); - // const [isLoading, setIsLoading] = useState({ - // deleteUser: false, - // sendPasswordRecovery: false, - // sendMagicLink: false, - // toggleBan: false, - // }); - - // const deleteUserMutation = useMutation({ - // mutationFn: () => deleteUser(user.id), - // onMutate: () => { - // setIsLoading((prev) => ({ ...prev, deleteUser: true })); - // setIsDeleting(true); - // }, - // onSuccess: () => { - // toast.success("User deleted successfully"); - // onUserUpdate(); - // onOpenChange(false); - // }, - // onError: () => { - // toast.error("Failed to delete user"); - // }, - // onSettled: () => { - // setIsLoading((prev) => ({ ...prev, deleteUser: false })); - // setIsDeleting(false); - // }, - // }); - - // const sendPasswordRecoveryMutation = useMutation({ - // mutationFn: () => { - // if (!user.email) { - // throw new Error("User does not have an email address"); - // } - // return sendPasswordRecovery(user.email); - // }, - // onMutate: () => { - // setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true })); - // }, - // onSuccess: () => { - // toast.success("Password recovery email sent"); - // }, - // onError: () => { - // toast.error("Failed to send password recovery email"); - // }, - // onSettled: () => { - // setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false })); - // }, - // }); - - // const sendMagicLinkMutation = useMutation({ - // mutationFn: () => { - // if (!user.email) { - // throw new Error("User does not have an email address"); - // } - // return sendMagicLink(user.email); - // }, - // onMutate: () => { - // setIsLoading((prev) => ({ ...prev, sendMagicLink: true })); - // }, - // onSuccess: () => { - // toast.success("Magic link sent successfully"); - // }, - // onError: () => { - // toast.error("Failed to send magic link"); - // }, - // onSettled: () => { - // setIsLoading((prev) => ({ ...prev, sendMagicLink: false })); - // }, - // }); - - // const toggleBanMutation = useMutation({ - // mutationFn: () => { - // if (user.banned_until) { - // return unbanUser(user.id); - // } else { - // const ban_duration = "7h"; // Example: Ban duration set to 7 days - // return banUser({ id: user.id, ban_duration }); - // } - // }, - // onMutate: () => { - // setIsLoading((prev) => ({ ...prev, toggleBan: true })); - // }, - // onSuccess: () => { - // toast.success("User ban status updated"); - // onUserUpdate(); - // }, - // onError: () => { - // toast.error("Failed to update user ban status"); - // }, - // onSettled: () => { - // setIsLoading((prev) => ({ ...prev, toggleBan: false })); - // }, - // }); - - // const handleCopyItem = (item: string) => { - // navigator.clipboard.writeText(item); - // toast.success("Copied to clipboard"); - // }; const { handleDeleteUser, @@ -364,7 +266,7 @@ export function UserDetailSheet({ - diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx index 87a096b..0d1ed22 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx @@ -28,9 +28,9 @@ import { InviteUserDialog } from "./invite-user"; import { AddUserDialog } from "./add-user-dialog"; import { UserDetailSheet } from "./sheet"; import { UserProfileSheet } from "./update-user"; -import { filterUsers, useUserManagementHandlers } from "../handler"; import { createUserColumns } from "./users-table"; -import { useGetUsersQuery } from "../queries"; +import { useGetUsersQuery } from "../_queries/queries"; +import { filterUsers, useUserManagementHandlers } from "../_handlers/use-user-management"; export default function UserManagement() { @@ -61,7 +61,7 @@ export default function UserManagement() { handleUserUpdate, clearFilters, getActiveFilterCount, - } = useUserManagementHandlers(refetch) + } = useUserManagementHandlers() // Apply filters to users const filteredUsers = useMemo(() => { diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-stats.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-stats.tsx index 4bec34d..aade99a 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-stats.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-stats.tsx @@ -3,7 +3,7 @@ import { Card, CardContent } from "@/app/_components/ui/card"; import { Users, UserCheck, UserX } from "lucide-react"; import { IUserSchema } from "@/src/entities/models/users/users.model"; -import { useGetUsersQuery } from "../queries"; +import { useGetUsersQuery } from "../_queries/queries"; function calculateUserStats(users: IUserSchema[] | undefined) { diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/users-table.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/users-table.tsx index d2747b3..ddf5505 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/users-table.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/users-table.tsx @@ -16,7 +16,7 @@ import { Input } from "@/app/_components/ui/input" import { Avatar } from "@/app/_components/ui/avatar" import Image from "next/image" import { Badge } from "@/app/_components/ui/badge" -import { useUserDetailSheetHandlers, useUsersHandlers } from "../handler" +import { useBanUserMutation, useDeleteUserMutation, useUnbanUserMutation } from "../_queries/mutations" export type UserTableColumn = ColumnDef @@ -26,11 +26,9 @@ export const createUserColumns = ( handleUserUpdate: (user: IUserSchema) => void, ): UserTableColumn[] => { - const { - deleteUser, - banUser, - unbanUser, - } = useUsersHandlers(); + const { mutateAsync: deleteUser } = useDeleteUserMutation(); + const { mutateAsync: banUser } = useBanUserMutation(); + const { mutateAsync: unbanUser } = useUnbanUserMutation(); return [ { @@ -329,9 +327,9 @@ export const createUserColumns = ( { if (row.original.banned_until != null) { - unbanUser(row.original.id) + unbanUser({ id: row.original.id }) } else { - banUser(row.original.id) + banUser({ id: row.original.id, ban_duration: "24h" }) } }} > diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-add-user-dialog.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-add-user-dialog.ts new file mode 100644 index 0000000..8a4041b --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-add-user-dialog.ts @@ -0,0 +1,73 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useCreateUserMutation } from "../_queries/mutations"; +import { CreateUserSchema, ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; + +export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: { + onUserAdded: () => void; + onOpenChange: (open: boolean) => void; +}) => { + + const queryClient = useQueryClient(); + + const { mutateAsync: createdUser, isPending } = useCreateUserMutation() + + const { + register, + handleSubmit, + reset, + formState: { errors: errors }, + setError, + getValues, + clearErrors, + watch, + } = useForm({ + resolver: zodResolver(CreateUserSchema), + defaultValues: { + email: "", + password: "", + email_confirm: true, + } + }); + + const emailConfirm = watch("email_confirm"); + + const onSubmit = handleSubmit(async (data) => { + + await createdUser(data, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + + onUserAdded(); + onOpenChange(false); + reset(); + }, + onError: (error) => { + reset(); + toast.error(error.message); + } + }) + + }); + + const handleOpenChange = (open: boolean) => { + if (!open) { + reset(); + } + onOpenChange(open); + }; + + return { + register, + handleSubmit: onSubmit, + reset, + errors, + isPending, + getValues, + clearErrors, + emailConfirm, + handleOpenChange, + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts new file mode 100644 index 0000000..cd8ae07 --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts @@ -0,0 +1,98 @@ +import { IUserSchema } from "@/src/entities/models/users/users.model"; +import { toast } from "sonner"; +import { useBanUserMutation, useDeleteUserMutation, useUnbanUserMutation } from "../_queries/mutations"; +import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from "@/app/(pages)/(auth)/_queries/mutations"; +import { ValidBanDuration } from "@/app/_lib/types/ban-duration"; +import { handleCopyItem } from "@/app/_utils/common"; +import { useQueryClient } from "@tanstack/react-query"; + +export const useUserDetailSheetHandlers = ({ open, user, onUserUpdated, onOpenChange }: { + open: boolean; + user: IUserSchema; + onUserUpdated: () => void; + onOpenChange: (open: boolean) => void; +}) => { + + const queryClient = useQueryClient(); + + const { mutateAsync: deleteUser, isPending: isDeletePending } = useDeleteUserMutation(); + const { mutateAsync: sendPasswordRecovery, isPending: isSendPasswordRecoveryPending } = useSendPasswordRecoveryMutation(); + const { mutateAsync: sendMagicLink, isPending: isSendMagicLinkPending } = useSendMagicLinkMutation(); + const { mutateAsync: banUser, isPending: isBanPending } = useBanUserMutation(); + const { mutateAsync: unbanUser, isPending: isUnbanPending } = useUnbanUserMutation(); + + const handleDeleteUser = async () => { + await deleteUser(user.id, { + onSuccess: () => { + onOpenChange(false); + } + }); + }; + + const handleSendPasswordRecovery = async () => { + if (!user.email) { + toast.error("User has no email address"); + return; + } + await sendPasswordRecovery(user.email); + }; + + const handleSendMagicLink = async () => { + if (!user.email) { + toast.error("User has no email address"); + return; + } + await sendMagicLink(user.email); + }; + + const handleBanUser = async (ban_duration: ValidBanDuration = "24h") => { + await banUser({ id: user.id, ban_duration: ban_duration }, { + onSuccess: () => { + onUserUpdated(); + } + }); + }; + + const handleUnbanUser = async () => { + await unbanUser({ id: user.id }, { + onSuccess: onUserUpdated + }); + }; + + const handleToggleBan = async (ban_duration: ValidBanDuration = "24h") => { + if (user.banned_until) { + await unbanUser({ id: user.id }, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + + toast(`${user.email} has been unbanned`); + onUserUpdated(); + } + }); + } else { + await banUser({ id: user.id, ban_duration: ban_duration }, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + + toast(`${user.email} has been banned`); + onUserUpdated(); + } + }); + } + }; + + return { + handleDeleteUser, + handleSendPasswordRecovery, + handleSendMagicLink, + handleBanUser, + handleUnbanUser, + handleToggleBan, + handleCopyItem, + isDeletePending, + isSendPasswordRecoveryPending, + isSendMagicLinkPending, + isBanPending, + isUnbanPending, + }; +}; \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-invite-user.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-invite-user.ts new file mode 100644 index 0000000..7b4d79f --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-invite-user.ts @@ -0,0 +1,70 @@ +import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from "@/src/entities/models/users/invite-user.model"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { useInviteUserMutation } from "../_queries/mutations"; + +export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: { + onUserInvited: () => void; + onOpenChange: (open: boolean) => void; +}) => { + + const queryClient = useQueryClient(); + const { mutateAsync: inviteUser, isPending } = useInviteUserMutation(); + + const { + register, + handleSubmit, + reset, + formState: { errors: errors }, + setError, + getValues, + clearErrors, + watch, + } = useForm({ + resolver: zodResolver(InviteUserSchema), + defaultValues: defaulIInviteUserSchemaValues + }) + + const onSubmit = handleSubmit(async (data) => { + + const { email } = data; + + await inviteUser(email, { + onSuccess: () => { + + queryClient.invalidateQueries({ queryKey: ["users"] }); + + toast.success("Invitation sent"); + + onUserInvited(); + onOpenChange(false); + reset(); + }, + onError: () => { + reset(); + toast.error("Failed to send invitation"); + }, + }); + }); + + const handleOpenChange = (open: boolean) => { + if (!open) { + reset(); + } + onOpenChange(open); + }; + + return { + register, + handleSubmit: onSubmit, + handleOpenChange, + reset, + getValues, + clearErrors, + watch, + errors, + isPending, + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-sheet.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-sheet.ts new file mode 100644 index 0000000..26a72ab --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-sheet.ts @@ -0,0 +1,71 @@ +import { IUpdateUserSchema, UpdateUserSchema } from "@/src/entities/models/users/update-user.model"; +import { IUserSchema } from "@/src/entities/models/users/users.model"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useUpdateUserMutation } from "../_queries/mutations"; + +export const useUserProfileSheetHandlers = ({ open, onOpenChange, userData, onUserUpdated }: { + open: boolean; + userData: IUserSchema; + onOpenChange: (open: boolean) => void; + onUserUpdated: () => void; +}) => { + + const { + mutateAsync: updateUser, + isPending, + } = useUpdateUserMutation() + + // Initialize form with user data + const form = useForm({ + resolver: zodResolver(UpdateUserSchema), + defaultValues: { + email: userData?.email || undefined, + encrypted_password: userData?.encrypted_password || undefined, + role: (userData?.role as "user" | "staff" | "admin") || "user", + phone: userData?.phone || undefined, + invited_at: userData?.invited_at || undefined, + confirmed_at: userData?.confirmed_at || undefined, + // recovery_sent_at: userData?.recovery_sent_at || undefined, + last_sign_in_at: userData?.last_sign_in_at || undefined, + created_at: userData?.created_at || undefined, + updated_at: userData?.updated_at || undefined, + is_anonymous: userData?.is_anonymous || false, + profile: { + // id: userData?.profile?.id || undefined, + // user_id: userData?.profile?.user_id || undefined, + avatar: userData?.profile?.avatar || undefined, + username: userData?.profile?.username || undefined, + first_name: userData?.profile?.first_name || undefined, + last_name: userData?.profile?.last_name || undefined, + bio: userData?.profile?.bio || undefined, + address: userData?.profile?.address || { + street: "", + city: "", + state: "", + country: "", + postal_code: "", + }, + birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined, + }, + }, + }) + + const handleUpdateUser = async () => { + await updateUser({ id: userData.id, data: form.getValues() }, { + onSuccess: () => { + onUserUpdated(); + onOpenChange(false); + }, + onError: () => { + onOpenChange(false); + }, + }); + } + + return { + handleUpdateUser, + form, + isPending, + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-user-management.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-user-management.ts new file mode 100644 index 0000000..3b9956b --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-user-management.ts @@ -0,0 +1,182 @@ +import { IUserFilterOptionsSchema, IUserSchema } from "@/src/entities/models/users/users.model" +import { useEffect, useState } from "react" + +export const useUserManagementHandlers = () => { + const [searchQuery, setSearchQuery] = useState("") + const [detailUser, setDetailUser] = useState(null) + const [updateUser, setUpdateUser] = useState(null) + const [isSheetOpen, setIsSheetOpen] = useState(false) + const [isUpdateOpen, setIsUpdateOpen] = useState(false) + const [isAddUserOpen, setIsAddUserOpen] = useState(false) + const [isInviteUserOpen, setIsInviteUserOpen] = useState(false) + + // Filter states + const [filters, setFilters] = useState({ + email: "", + phone: "", + lastSignIn: "", + createdAt: "", + status: [], + }) + + // Handle opening the detail sheet + const handleUserClick = (user: IUserSchema) => { + setDetailUser(user) + setIsSheetOpen(true) + } + + // Handle opening the update sheet + const handleUserUpdate = (user: IUserSchema) => { + setUpdateUser(user) + setIsUpdateOpen(true) + } + + // Close detail sheet when update sheet opens + useEffect(() => { + if (isUpdateOpen) { + setIsSheetOpen(false) + } + }, [isUpdateOpen]) + + // Reset detail user when sheet closes + useEffect(() => { + if (!isSheetOpen) { + // Use a small delay to prevent flickering if another sheet is opening + const timer = setTimeout(() => { + if (!isSheetOpen && !isUpdateOpen) { + setDetailUser(null) + } + }, 300) + return () => clearTimeout(timer) + } + }, [isSheetOpen, isUpdateOpen]) + + // Reset update user when update sheet closes + useEffect(() => { + if (!isUpdateOpen) { + // Use a small delay to prevent flickering if another sheet is opening + const timer = setTimeout(() => { + if (!isUpdateOpen) { + setUpdateUser(null) + } + }, 300) + return () => clearTimeout(timer) + } + }, [isUpdateOpen]) + + const clearFilters = () => { + setFilters({ + email: "", + phone: "", + lastSignIn: "", + createdAt: "", + status: [], + }) + } + + const getActiveFilterCount = () => { + return Object.values(filters).filter( + (value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0), + ).length + } + + return { + searchQuery, + setSearchQuery, + detailUser, + updateUser, + isSheetOpen, + setIsSheetOpen, + isUpdateOpen, + setIsUpdateOpen, + isAddUserOpen, + setIsAddUserOpen, + isInviteUserOpen, + setIsInviteUserOpen, + filters, + setFilters, + handleUserClick, + handleUserUpdate, + clearFilters, + getActiveFilterCount, + } +} + +export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => { + return users.filter((user) => { + + // Global search + if (searchQuery) { + const query = searchQuery.toLowerCase() + const matchesSearch = + user.email?.toLowerCase().includes(query) || + user.phone?.toLowerCase().includes(query) || + user.id.toLowerCase().includes(query) + + if (!matchesSearch) return false + } + + // Email filter + if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) { + return false + } + + // Phone filter + if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) { + return false + } + + // Last sign in filter + if (filters.lastSignIn) { + if (filters.lastSignIn === "never" && user.last_sign_in_at) { + return false + } else if (filters.lastSignIn === "today") { + const today = new Date() + today.setHours(0, 0, 0, 0) + const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null + if (!signInDate || signInDate < today) return false + } else if (filters.lastSignIn === "week") { + const weekAgo = new Date() + weekAgo.setDate(weekAgo.getDate() - 7) + const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null + if (!signInDate || signInDate < weekAgo) return false + } else if (filters.lastSignIn === "month") { + const monthAgo = new Date() + monthAgo.setMonth(monthAgo.getMonth() - 1) + const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null + if (!signInDate || signInDate < monthAgo) return false + } + } + + // Created at filter + if (filters.createdAt) { + if (filters.createdAt === "today") { + const today = new Date() + today.setHours(0, 0, 0, 0) + const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date() + if (createdAt < today) return false + } else if (filters.createdAt === "week") { + const weekAgo = new Date() + weekAgo.setDate(weekAgo.getDate() - 7) + const createdAt = user.created_at ? new Date(user.created_at) : new Date() + if (createdAt < weekAgo) return false + } else if (filters.createdAt === "month") { + const monthAgo = new Date() + monthAgo.setMonth(monthAgo.getMonth() - 1) + const createdAt = user.created_at ? new Date(user.created_at) : new Date() + if (createdAt < monthAgo) return false + } + } + + // Status filter + if (filters.status.length > 0) { + const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active" + + if (!filters.status.includes(userStatus)) { + return false + } + } + + return true + }) +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/mutations.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/mutations.ts new file mode 100644 index 0000000..7514d7a --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/mutations.ts @@ -0,0 +1,51 @@ +import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; +import { useMutation } from "@tanstack/react-query"; +import { banUser, createUser, deleteUser, inviteUser, unbanUser, updateUser } from "../action"; +import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; +import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"; +import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; +import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; +import { ValidBanDuration } from "@/app/_lib/types/ban-duration"; + +export const useCreateUserMutation = () => { + return useMutation({ + mutationKey: ["user", "create"], + mutationFn: (data: ICreateUserSchema) => createUser(data), + }) +} + +export const useUpdateUserMutation = () => { + return useMutation({ + mutationKey: ["user", "update"], + mutationFn: (args: { id: string; data: IUpdateUserSchema }) => updateUser(args.id, args.data) + }) +} + +export const useDeleteUserMutation = () => { + return useMutation({ + mutationKey: ["user", "delete"], + mutationFn: (id: string) => deleteUser(id), + }) +} + +export const useInviteUserMutation = () => { + return useMutation({ + mutationKey: ["user", "invite"], + mutationFn: (email: string) => inviteUser({ email }), + }) +} + + +export const useBanUserMutation = () => { + return useMutation({ + mutationKey: ["user", "ban"], + mutationFn: (args: { id: string; ban_duration: ValidBanDuration }) => banUser({ id: args.id }, { ban_duration: args.ban_duration }), + }) +} + +export const useUnbanUserMutation = () => { + return useMutation({ + mutationKey: ["user", "unban"], + mutationFn: (credential: ICredentialsUnbanUserSchema) => unbanUser(credential), + }) +} diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/queries.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/queries.ts new file mode 100644 index 0000000..ef164a4 --- /dev/null +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_queries/queries.ts @@ -0,0 +1,38 @@ +import { IUserSchema } from "@/src/entities/models/users/users.model"; +import { useQuery } from "@tanstack/react-query"; +import { getCurrentUser, getUserByEmail, getUserById, getUserByUsername, getUsers } from "../action"; + +export const useGetUsersQuery = () => { + return useQuery({ + queryKey: ["users"], + queryFn: () => getUsers() + }); +} + +export const useGetUserByEmailQuery = (email: string) => { + return useQuery({ + queryKey: ["user", "email", email], + queryFn: () => getUserByEmail({ email }), + }) +} + +export const useGetUserByIdQuery = (id: string) => { + return useQuery({ + queryKey: ["user", "id", id], + queryFn: () => getUserById({ id }), + }) +} + +export const useGetUserByUsernameQuery = (username: string) => { + return useQuery({ + queryKey: ["user", "username", username], + queryFn: () => getUserByUsername({ username }), + }) +} + +export const useGetCurrentUserQuery = () => { + return useQuery({ + queryKey: ["user", "current"], + queryFn: () => getCurrentUser(), + }) +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx index 2f7c645..dc652c0 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/handler.tsx @@ -1,684 +1,698 @@ -import { useEffect, useState } from 'react'; -import { useBanUserMutation, useCreateUserMutation, useDeleteUserMutation, useInviteUserMutation, useUnbanUserMutation, useUpdateUserMutation } from './queries'; -import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model'; -import { toast } from 'sonner'; -import { set } from 'date-fns'; -import { CreateUserSchema, defaulICreateUserSchemaValues, ICreateUserSchema } from '@/src/entities/models/users/create-user.model'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model'; -import { useQueryClient } from '@tanstack/react-query'; -import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from '@/app/(pages)/(auth)/queries'; -import { ValidBanDuration } from '@/app/_lib/types/ban-duration'; -import { IUpdateUserSchema, UpdateUserSchema } from '@/src/entities/models/users/update-user.model'; - -export const useUsersHandlers = () => { - const queryClient = useQueryClient(); - - // Core mutations - const { updateUser, isPending: isUpdatePending, errors: isUpdateError } = useUpdateUserMutation(); - const { deleteUser, isPending: isDeletePending } = useDeleteUserMutation(); - const { sendPasswordRecovery, isPending: isSendPasswordRecoveryPending } = useSendPasswordRecoveryMutation(); - const { sendMagicLink, isPending: isSendMagicLinkPending } = useSendMagicLinkMutation(); - const { banUser, isPending: isBanPending } = useBanUserMutation(); - const { unbanUser, isPending: isUnbanPending } = useUnbanUserMutation(); - - - /** - * update a user by ID - */ - - const handleUpdateUser = async (userId: string, data: IUpdateUserSchema, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - await updateUser({ id: userId, data }, { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }); - - toast.success("User updated successfully"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to update user"); - options?.onError?.(error); - }, - }); - } - - /** - * Deletes a user by ID - */ - const handleDeleteUser = async (userId: string, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - await deleteUser(userId, { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }); - toast.success("User deleted successfully"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to delete user"); - options?.onError?.(error); - }, - }); - }; - - /** - * Sends a password recovery email to the user - */ - const handleSendPasswordRecovery = async (email: string, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - if (!email) { - toast.error("No email address provided"); - options?.onError?.(new Error("No email address provided")); - return; - } - - await sendPasswordRecovery(email, { - onSuccess: () => { - toast.success("Recovery email sent"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to send recovery email"); - options?.onError?.(error); - }, - }); - }; - - /** - * Sends a magic link to the user's email - */ - const handleSendMagicLink = async (email: string, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - if (!email) { - toast.error("No email address provided"); - options?.onError?.(new Error("No email address provided")); - return; - } - - await sendMagicLink(email, { - onSuccess: () => { - toast.success("Magic link sent"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to send magic link"); - options?.onError?.(error); - }, - }); - }; - - /** - * Bans a user for the specified duration - */ - const handleBanUser = async (userId: string, banDuration: ValidBanDuration = "24h", options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - await banUser({ credential: { id: userId }, data: { ban_duration: banDuration } }, { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }); - toast.success("User banned successfully"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to ban user"); - options?.onError?.(error); - }, - }); - }; - - /** - * Unbans a user - */ - const handleUnbanUser = async (userId: string, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - await unbanUser({ id: userId }, { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }); - toast.success("User unbanned successfully"); - options?.onSuccess?.(); - }, - onError: (error) => { - toast.error("Failed to unban user"); - options?.onError?.(error); - }, - }); - }; - - /** - * Toggles a user's ban status - */ - const handleToggleBan = async (user: { id: string, banned_until?: ValidBanDuration }, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void, - banDuration?: ValidBanDuration - }) => { - if (user.banned_until) { - await handleUnbanUser(user.id, options); - } else { - await handleBanUser(user.id, options?.banDuration, options); - } - }; - - /** - * Copies text to clipboard - */ - const handleCopyItem = (item: string, options?: { - onSuccess?: () => void, - onError?: (error: unknown) => void - }) => { - if (!navigator.clipboard) { - const error = new Error("Clipboard not supported"); - toast.error("Clipboard not supported"); - options?.onError?.(error); - return; - } - - if (!item) { - const error = new Error("Nothing to copy"); - toast.error("Nothing to copy"); - options?.onError?.(error); - return; - } - - navigator.clipboard.writeText(item) - .then(() => { - toast.success("Copied to clipboard"); - options?.onSuccess?.(); - }) - .catch((error) => { - toast.error("Failed to copy to clipboard"); - options?.onError?.(error); - }); - }; - - return { - // Action handlers - updateUser: handleUpdateUser, - deleteUser: handleDeleteUser, - sendPasswordRecovery: handleSendPasswordRecovery, - sendMagicLink: handleSendMagicLink, - banUser: handleBanUser, - unbanUser: handleUnbanUser, - toggleBan: handleToggleBan, - copyToClipboard: handleCopyItem, - - // Loading states - isUpdatePending, - isDeletePending, - isSendPasswordRecoveryPending, - isSendMagicLinkPending, - isBanPending, - isUnbanPending, - - // Errors - isUpdateError, - }; -}; - -// Specific handler for the component - -export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: { - onUserAdded: () => void; - onOpenChange: (open: boolean) => void; -}) => { - - const queryClient = useQueryClient(); - const { createUser, isPending } = useCreateUserMutation(); - - const { - register, - handleSubmit, - reset, - formState: { errors: errors }, - setError, - getValues, - clearErrors, - watch, - } = useForm({ - resolver: zodResolver(CreateUserSchema), - defaultValues: { - email: "", - password: "", - email_confirm: true, - } - }); - - const emailConfirm = watch("email_confirm"); - - const onSubmit = handleSubmit(async (data) => { - - await createUser(data, { - onSuccess: () => { - - queryClient.invalidateQueries({ queryKey: ["users"] }); - - toast.success("User created successfully."); - - onUserAdded(); - onOpenChange(false); - reset(); - }, - onError: (error) => { - reset(); - toast.error(error.message); - }, - }); - - }); - - const handleOpenChange = (open: boolean) => { - if (!open) { - reset(); - } - onOpenChange(open); - }; - - return { - register, - handleSubmit: onSubmit, - reset, - errors, - isPending, - getValues, - clearErrors, - emailConfirm, - handleOpenChange, - }; -} - -export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: { - onUserInvited: () => void; - onOpenChange: (open: boolean) => void; -}) => { - - const queryClient = useQueryClient(); - const { inviteUser, isPending } = useInviteUserMutation(); - - const { - register, - handleSubmit, - reset, - formState: { errors: errors }, - setError, - getValues, - clearErrors, - watch, - } = useForm({ - resolver: zodResolver(InviteUserSchema), - defaultValues: defaulIInviteUserSchemaValues - }) - - const onSubmit = handleSubmit(async (data) => { - await inviteUser(data, { - onSuccess: () => { - - queryClient.invalidateQueries({ queryKey: ["users"] }); - - toast.success("Invitation sent"); - - onUserInvited(); - onOpenChange(false); - reset(); - }, - onError: () => { - reset(); - toast.error("Failed to send invitation"); - }, - }); - }); - - const handleOpenChange = (open: boolean) => { - if (!open) { - reset(); - } - onOpenChange(open); - }; - - return { - register, - handleSubmit: onSubmit, - handleOpenChange, - reset, - getValues, - clearErrors, - watch, - errors, - isPending, - }; -} - - -export const useUserDetailSheetHandlers = ({ open, user, onUserUpdated, onOpenChange }: { - open: boolean; - user: IUserSchema; - onUserUpdated: () => void; - onOpenChange: (open: boolean) => void; -}) => { - const { - deleteUser, - sendPasswordRecovery, - sendMagicLink, - banUser, - unbanUser, - toggleBan, - copyToClipboard, - isDeletePending, - isSendPasswordRecoveryPending, - isSendMagicLinkPending, - isBanPending, - isUnbanPending, - } = useUsersHandlers(); - - const handleDeleteUser = async () => { - await deleteUser(user.id, { - onSuccess: () => { - onOpenChange(false); - } - }); - }; - - const handleSendPasswordRecovery = async () => { - if (!user.email) { - toast.error("User has no email address"); - return; - } - await sendPasswordRecovery(user.email); - }; - - const handleSendMagicLink = async () => { - if (!user.email) { - toast.error("User has no email address"); - return; - } - await sendMagicLink(user.email); - }; - - const handleBanUser = async () => { - await banUser(user.id, "24h", { - onSuccess: onUserUpdated - }); - }; - - const handleUnbanUser = async () => { - await unbanUser(user.id, { - onSuccess: onUserUpdated - }); - }; - - const handleToggleBan = async () => { - await toggleBan({ id: user.id }, { - onSuccess: onUserUpdated - }); - }; - - return { - handleDeleteUser, - handleSendPasswordRecovery, - handleSendMagicLink, - handleBanUser, - handleUnbanUser, - handleToggleBan, - handleCopyItem: copyToClipboard, - isDeletePending, - isSendPasswordRecoveryPending, - isSendMagicLinkPending, - isBanPending, - isUnbanPending, - }; -}; - -export const useUserProfileSheetHandlers = ({ open, onOpenChange, userData, onUserUpdated }: { - open: boolean; - userData: IUserSchema; - onOpenChange: (open: boolean) => void; - onUserUpdated: () => void; -}) => { - - const { updateUser, isUpdatePending, isUpdateError } = useUsersHandlers(); - - // Initialize form with user data - const form = useForm({ - resolver: zodResolver(UpdateUserSchema), - defaultValues: { - email: userData?.email || undefined, - encrypted_password: userData?.encrypted_password || undefined, - role: (userData?.role as "user" | "staff" | "admin") || "user", - phone: userData?.phone || undefined, - invited_at: userData?.invited_at || undefined, - confirmed_at: userData?.confirmed_at || undefined, - // recovery_sent_at: userData?.recovery_sent_at || undefined, - last_sign_in_at: userData?.last_sign_in_at || undefined, - created_at: userData?.created_at || undefined, - updated_at: userData?.updated_at || undefined, - is_anonymous: userData?.is_anonymous || false, - profile: { - // id: userData?.profile?.id || undefined, - // user_id: userData?.profile?.user_id || undefined, - avatar: userData?.profile?.avatar || undefined, - username: userData?.profile?.username || undefined, - first_name: userData?.profile?.first_name || undefined, - last_name: userData?.profile?.last_name || undefined, - bio: userData?.profile?.bio || undefined, - address: userData?.profile?.address || { - street: "", - city: "", - state: "", - country: "", - postal_code: "", - }, - birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined, - }, - }, - }) - - const handleUpdateUser = async () => { - await updateUser(userData.id, form.getValues(), { - onSuccess: () => { - onUserUpdated(); - onOpenChange(false); - }, - onError: () => { - onOpenChange(false); - }, - }); - - } - - return { - handleUpdateUser, - form, - isUpdatePending, - }; -} - -export const useUserManagementHandlers = (refetch: () => void) => { - const [searchQuery, setSearchQuery] = useState("") - const [detailUser, setDetailUser] = useState(null) - const [updateUser, setUpdateUser] = useState(null) - const [isSheetOpen, setIsSheetOpen] = useState(false) - const [isUpdateOpen, setIsUpdateOpen] = useState(false) - const [isAddUserOpen, setIsAddUserOpen] = useState(false) - const [isInviteUserOpen, setIsInviteUserOpen] = useState(false) - - // Filter states - const [filters, setFilters] = useState({ - email: "", - phone: "", - lastSignIn: "", - createdAt: "", - status: [], - }) - - // Handle opening the detail sheet - const handleUserClick = (user: IUserSchema) => { - setDetailUser(user) - setIsSheetOpen(true) - } - - // Handle opening the update sheet - const handleUserUpdate = (user: IUserSchema) => { - setUpdateUser(user) - setIsUpdateOpen(true) - } - - // Close detail sheet when update sheet opens - useEffect(() => { - if (isUpdateOpen) { - setIsSheetOpen(false) - } - }, [isUpdateOpen]) - - // Reset detail user when sheet closes - useEffect(() => { - if (!isSheetOpen) { - // Use a small delay to prevent flickering if another sheet is opening - const timer = setTimeout(() => { - if (!isSheetOpen && !isUpdateOpen) { - setDetailUser(null) - } - }, 300) - return () => clearTimeout(timer) - } - }, [isSheetOpen, isUpdateOpen]) - - // Reset update user when update sheet closes - useEffect(() => { - if (!isUpdateOpen) { - // Use a small delay to prevent flickering if another sheet is opening - const timer = setTimeout(() => { - if (!isUpdateOpen) { - setUpdateUser(null) - } - }, 300) - return () => clearTimeout(timer) - } - }, [isUpdateOpen]) - - const clearFilters = () => { - setFilters({ - email: "", - phone: "", - lastSignIn: "", - createdAt: "", - status: [], - }) - } - - const getActiveFilterCount = () => { - return Object.values(filters).filter( - (value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0), - ).length - } - - return { - searchQuery, - setSearchQuery, - detailUser, - updateUser, - isSheetOpen, - setIsSheetOpen, - isUpdateOpen, - setIsUpdateOpen, - isAddUserOpen, - setIsAddUserOpen, - isInviteUserOpen, - setIsInviteUserOpen, - filters, - setFilters, - handleUserClick, - handleUserUpdate, - clearFilters, - getActiveFilterCount, - } -} - -export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => { - return users.filter((user) => { - - // Global search - if (searchQuery) { - const query = searchQuery.toLowerCase() - const matchesSearch = - user.email?.toLowerCase().includes(query) || - user.phone?.toLowerCase().includes(query) || - user.id.toLowerCase().includes(query) - - if (!matchesSearch) return false - } - - // Email filter - if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) { - return false - } - - // Phone filter - if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) { - return false - } - - // Last sign in filter - if (filters.lastSignIn) { - if (filters.lastSignIn === "never" && user.last_sign_in_at) { - return false - } else if (filters.lastSignIn === "today") { - const today = new Date() - today.setHours(0, 0, 0, 0) - const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null - if (!signInDate || signInDate < today) return false - } else if (filters.lastSignIn === "week") { - const weekAgo = new Date() - weekAgo.setDate(weekAgo.getDate() - 7) - const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null - if (!signInDate || signInDate < weekAgo) return false - } else if (filters.lastSignIn === "month") { - const monthAgo = new Date() - monthAgo.setMonth(monthAgo.getMonth() - 1) - const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null - if (!signInDate || signInDate < monthAgo) return false - } - } - - // Created at filter - if (filters.createdAt) { - if (filters.createdAt === "today") { - const today = new Date() - today.setHours(0, 0, 0, 0) - const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date() - if (createdAt < today) return false - } else if (filters.createdAt === "week") { - const weekAgo = new Date() - weekAgo.setDate(weekAgo.getDate() - 7) - const createdAt = user.created_at ? new Date(user.created_at) : new Date() - if (createdAt < weekAgo) return false - } else if (filters.createdAt === "month") { - const monthAgo = new Date() - monthAgo.setMonth(monthAgo.getMonth() - 1) - const createdAt = user.created_at ? new Date(user.created_at) : new Date() - if (createdAt < monthAgo) return false - } - } - - // Status filter - if (filters.status.length > 0) { - const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active" - - if (!filters.status.includes(userStatus)) { - return false - } - } - - return true - }) -} \ No newline at end of file +// import { useEffect, useState } from 'react'; +// import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model'; +// import { toast } from 'sonner'; +// import { set } from 'date-fns'; +// import { CreateUserSchema, defaulICreateUserSchemaValues, ICreateUserSchema } from '@/src/entities/models/users/create-user.model'; +// import { useForm } from 'react-hook-form'; +// import { zodResolver } from '@hookform/resolvers/zod'; +// import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model'; +// import { useQueryClient } from '@tanstack/react-query'; +// import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from '@/app/(pages)/(auth)/queries'; +// import { ValidBanDuration } from '@/app/_lib/types/ban-duration'; +// import { IUpdateUserSchema, UpdateUserSchema } from '@/src/entities/models/users/update-user.model'; +// import { useUsersAction } from './queries'; +// import { getUsersQuery, useGetUsersQuery, useUsersQuery } from './_queries/queries'; + +// export const useUsersHandlers = () => { +// const queryClient = useQueryClient(); + +// // Core mutations +// // const { updateUser, isPending: isUpdatePending, errors: isUpdateError } = useUpdateUserMutation(); +// // const { deleteUser, isPending: isDeletePending } = useDeleteUserMutation(); +// // const { sendPasswordRecovery, isPending: isSendPasswordRecoveryPending } = useSendPasswordRecoveryMutation(); +// // const { sendMagicLink, isPending: isSendMagicLinkPending } = useSendMagicLinkMutation(); +// // const { banUser, isPending: isBanPending } = useBanUserMutation(); +// // const { unbanUser, isPending: isUnbanPending } = useUnbanUserMutation(); + +// const { +// getCurrentUser, +// getUserById, +// getUserByEmail, +// getUserByUsername, +// createUser, +// inviteUser, +// updateUser, +// deleteUser, +// banUser, +// unbanUser +// } = useUsersAction(); + + +// /** +// * update a user by ID +// */ + +// const handleUpdateUser = async (userId: string, data: IUpdateUserSchema, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// await updateUser({ id: userId, data }, { +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["users"] }); + +// toast.success("User updated successfully"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to update user"); +// options?.onError?.(error); +// }, +// }); +// } + +// /** +// * Deletes a user by ID +// */ +// const handleDeleteUser = async (userId: string, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// await deleteUser(userId, { +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["users"] }); +// toast.success("User deleted successfully"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to delete user"); +// options?.onError?.(error); +// }, +// }); +// }; + +// /** +// * Sends a password recovery email to the user +// */ +// const handleSendPasswordRecovery = async (email: string, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// if (!email) { +// toast.error("No email address provided"); +// options?.onError?.(new Error("No email address provided")); +// return; +// } + +// await sendPasswordRecovery(email, { +// onSuccess: () => { +// toast.success("Recovery email sent"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to send recovery email"); +// options?.onError?.(error); +// }, +// }); +// }; + +// /** +// * Sends a magic link to the user's email +// */ +// const handleSendMagicLink = async (email: string, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// if (!email) { +// toast.error("No email address provided"); +// options?.onError?.(new Error("No email address provided")); +// return; +// } + +// await sendMagicLink(email, { +// onSuccess: () => { +// toast.success("Magic link sent"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to send magic link"); +// options?.onError?.(error); +// }, +// }); +// }; + +// /** +// * Bans a user for the specified duration +// */ +// const handleBanUser = async (userId: string, banDuration: ValidBanDuration = "24h", options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// await banUser({ credential: { id: userId }, data: { ban_duration: banDuration } }, { +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["users"] }); +// toast.success("User banned successfully"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to ban user"); +// options?.onError?.(error); +// }, +// }); +// }; + +// /** +// * Unbans a user +// */ +// const handleUnbanUser = async (userId: string, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// await unbanUser({ id: userId }, { +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["users"] }); +// toast.success("User unbanned successfully"); +// options?.onSuccess?.(); +// }, +// onError: (error) => { +// toast.error("Failed to unban user"); +// options?.onError?.(error); +// }, +// }); +// }; + +// /** +// * Toggles a user's ban status +// */ +// const handleToggleBan = async (user: { id: string, banned_until?: ValidBanDuration }, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void, +// banDuration?: ValidBanDuration +// }) => { +// if (user.banned_until) { +// await handleUnbanUser(user.id, options); +// } else { +// await handleBanUser(user.id, options?.banDuration, options); +// } +// }; + +// /** +// * Copies text to clipboard +// */ +// const handleCopyItem = (item: string, options?: { +// onSuccess?: () => void, +// onError?: (error: unknown) => void +// }) => { +// if (!navigator.clipboard) { +// const error = new Error("Clipboard not supported"); +// toast.error("Clipboard not supported"); +// options?.onError?.(error); +// return; +// } + +// if (!item) { +// const error = new Error("Nothing to copy"); +// toast.error("Nothing to copy"); +// options?.onError?.(error); +// return; +// } + +// navigator.clipboard.writeText(item) +// .then(() => { +// toast.success("Copied to clipboard"); +// options?.onSuccess?.(); +// }) +// .catch((error) => { +// toast.error("Failed to copy to clipboard"); +// options?.onError?.(error); +// }); +// }; + +// return { +// // Action handlers +// updateUser: handleUpdateUser, +// deleteUser: handleDeleteUser, +// sendPasswordRecovery: handleSendPasswordRecovery, +// sendMagicLink: handleSendMagicLink, +// banUser: handleBanUser, +// unbanUser: handleUnbanUser, +// toggleBan: handleToggleBan, +// copyToClipboard: handleCopyItem, + +// // Loading states +// isUpdatePending, +// isDeletePending, +// isSendPasswordRecoveryPending, +// isSendMagicLinkPending, +// isBanPending, +// isUnbanPending, + +// // Errors +// isUpdateError, +// }; +// }; + +// // Specific handler for the component + +// export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: { +// onUserAdded: () => void; +// onOpenChange: (open: boolean) => void; +// }) => { + +// const queryClient = useQueryClient(); +// const { createUser, isPending } = useCreateUserMutation(); + +// const { +// register, +// handleSubmit, +// reset, +// formState: { errors: errors }, +// setError, +// getValues, +// clearErrors, +// watch, +// } = useForm({ +// resolver: zodResolver(CreateUserSchema), +// defaultValues: { +// email: "", +// password: "", +// email_confirm: true, +// } +// }); + +// const emailConfirm = watch("email_confirm"); + +// const onSubmit = handleSubmit(async (data) => { + +// await createUser(data, { +// onSuccess: () => { + +// queryClient.invalidateQueries({ queryKey: ["users"] }); + +// toast.success("User created successfully."); + +// onUserAdded(); +// onOpenChange(false); +// reset(); +// }, +// onError: (error) => { +// reset(); +// toast.error(error.message); +// }, +// }); + +// }); + +// const handleOpenChange = (open: boolean) => { +// if (!open) { +// reset(); +// } +// onOpenChange(open); +// }; + +// return { +// register, +// handleSubmit: onSubmit, +// reset, +// errors, +// isPending, +// getValues, +// clearErrors, +// emailConfirm, +// handleOpenChange, +// }; +// } + +// export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: { +// onUserInvited: () => void; +// onOpenChange: (open: boolean) => void; +// }) => { + +// const queryClient = useQueryClient(); +// const { inviteUser, isPending } = useInviteUserMutation(); + +// const { +// register, +// handleSubmit, +// reset, +// formState: { errors: errors }, +// setError, +// getValues, +// clearErrors, +// watch, +// } = useForm({ +// resolver: zodResolver(InviteUserSchema), +// defaultValues: defaulIInviteUserSchemaValues +// }) + +// const onSubmit = handleSubmit(async (data) => { +// await inviteUser(data, { +// onSuccess: () => { + +// queryClient.invalidateQueries({ queryKey: ["users"] }); + +// toast.success("Invitation sent"); + +// onUserInvited(); +// onOpenChange(false); +// reset(); +// }, +// onError: () => { +// reset(); +// toast.error("Failed to send invitation"); +// }, +// }); +// }); + +// const handleOpenChange = (open: boolean) => { +// if (!open) { +// reset(); +// } +// onOpenChange(open); +// }; + +// return { +// register, +// handleSubmit: onSubmit, +// handleOpenChange, +// reset, +// getValues, +// clearErrors, +// watch, +// errors, +// isPending, +// }; +// } + + +// export const useUserDetailSheetHandlers = ({ open, user, onUserUpdated, onOpenChange }: { +// open: boolean; +// user: IUserSchema; +// onUserUpdated: () => void; +// onOpenChange: (open: boolean) => void; +// }) => { +// const { +// deleteUser, +// sendPasswordRecovery, +// sendMagicLink, +// banUser, +// unbanUser, +// toggleBan, +// copyToClipboard, +// isDeletePending, +// isSendPasswordRecoveryPending, +// isSendMagicLinkPending, +// isBanPending, +// isUnbanPending, +// } = useUsersHandlers(); + +// const handleDeleteUser = async () => { +// await deleteUser(user.id, { +// onSuccess: () => { +// onOpenChange(false); +// } +// }); +// }; + +// const handleSendPasswordRecovery = async () => { +// if (!user.email) { +// toast.error("User has no email address"); +// return; +// } +// await sendPasswordRecovery(user.email); +// }; + +// const handleSendMagicLink = async () => { +// if (!user.email) { +// toast.error("User has no email address"); +// return; +// } +// await sendMagicLink(user.email); +// }; + +// const handleBanUser = async () => { +// await banUser(user.id, "24h", { +// onSuccess: onUserUpdated +// }); +// }; + +// const handleUnbanUser = async () => { +// await unbanUser(user.id, { +// onSuccess: onUserUpdated +// }); +// }; + +// const handleToggleBan = async () => { +// await toggleBan({ id: user.id }, { +// onSuccess: onUserUpdated +// }); +// }; + +// return { +// handleDeleteUser, +// handleSendPasswordRecovery, +// handleSendMagicLink, +// handleBanUser, +// handleUnbanUser, +// handleToggleBan, +// handleCopyItem: copyToClipboard, +// isDeletePending, +// isSendPasswordRecoveryPending, +// isSendMagicLinkPending, +// isBanPending, +// isUnbanPending, +// }; +// }; + +// export const useUserProfileSheetHandlers = ({ open, onOpenChange, userData, onUserUpdated }: { +// open: boolean; +// userData: IUserSchema; +// onOpenChange: (open: boolean) => void; +// onUserUpdated: () => void; +// }) => { + +// const { updateUser, isUpdatePending, isUpdateError } = useUsersHandlers(); + +// // Initialize form with user data +// const form = useForm({ +// resolver: zodResolver(UpdateUserSchema), +// defaultValues: { +// email: userData?.email || undefined, +// encrypted_password: userData?.encrypted_password || undefined, +// role: (userData?.role as "user" | "staff" | "admin") || "user", +// phone: userData?.phone || undefined, +// invited_at: userData?.invited_at || undefined, +// confirmed_at: userData?.confirmed_at || undefined, +// // recovery_sent_at: userData?.recovery_sent_at || undefined, +// last_sign_in_at: userData?.last_sign_in_at || undefined, +// created_at: userData?.created_at || undefined, +// updated_at: userData?.updated_at || undefined, +// is_anonymous: userData?.is_anonymous || false, +// profile: { +// // id: userData?.profile?.id || undefined, +// // user_id: userData?.profile?.user_id || undefined, +// avatar: userData?.profile?.avatar || undefined, +// username: userData?.profile?.username || undefined, +// first_name: userData?.profile?.first_name || undefined, +// last_name: userData?.profile?.last_name || undefined, +// bio: userData?.profile?.bio || undefined, +// address: userData?.profile?.address || { +// street: "", +// city: "", +// state: "", +// country: "", +// postal_code: "", +// }, +// birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined, +// }, +// }, +// }) + +// const handleUpdateUser = async () => { +// await updateUser(userData.id, form.getValues(), { +// onSuccess: () => { +// onUserUpdated(); +// onOpenChange(false); +// }, +// onError: () => { +// onOpenChange(false); +// }, +// }); + +// } + +// return { +// handleUpdateUser, +// form, +// isUpdatePending, +// }; +// } + +// export const useUserManagementHandlers = (refetch: () => void) => { +// const [searchQuery, setSearchQuery] = useState("") +// const [detailUser, setDetailUser] = useState(null) +// const [updateUser, setUpdateUser] = useState(null) +// const [isSheetOpen, setIsSheetOpen] = useState(false) +// const [isUpdateOpen, setIsUpdateOpen] = useState(false) +// const [isAddUserOpen, setIsAddUserOpen] = useState(false) +// const [isInviteUserOpen, setIsInviteUserOpen] = useState(false) + +// // Filter states +// const [filters, setFilters] = useState({ +// email: "", +// phone: "", +// lastSignIn: "", +// createdAt: "", +// status: [], +// }) + +// // Handle opening the detail sheet +// const handleUserClick = (user: IUserSchema) => { +// setDetailUser(user) +// setIsSheetOpen(true) +// } + +// // Handle opening the update sheet +// const handleUserUpdate = (user: IUserSchema) => { +// setUpdateUser(user) +// setIsUpdateOpen(true) +// } + +// // Close detail sheet when update sheet opens +// useEffect(() => { +// if (isUpdateOpen) { +// setIsSheetOpen(false) +// } +// }, [isUpdateOpen]) + +// // Reset detail user when sheet closes +// useEffect(() => { +// if (!isSheetOpen) { +// // Use a small delay to prevent flickering if another sheet is opening +// const timer = setTimeout(() => { +// if (!isSheetOpen && !isUpdateOpen) { +// setDetailUser(null) +// } +// }, 300) +// return () => clearTimeout(timer) +// } +// }, [isSheetOpen, isUpdateOpen]) + +// // Reset update user when update sheet closes +// useEffect(() => { +// if (!isUpdateOpen) { +// // Use a small delay to prevent flickering if another sheet is opening +// const timer = setTimeout(() => { +// if (!isUpdateOpen) { +// setUpdateUser(null) +// } +// }, 300) +// return () => clearTimeout(timer) +// } +// }, [isUpdateOpen]) + +// const clearFilters = () => { +// setFilters({ +// email: "", +// phone: "", +// lastSignIn: "", +// createdAt: "", +// status: [], +// }) +// } + +// const getActiveFilterCount = () => { +// return Object.values(filters).filter( +// (value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0), +// ).length +// } + +// return { +// searchQuery, +// setSearchQuery, +// detailUser, +// updateUser, +// isSheetOpen, +// setIsSheetOpen, +// isUpdateOpen, +// setIsUpdateOpen, +// isAddUserOpen, +// setIsAddUserOpen, +// isInviteUserOpen, +// setIsInviteUserOpen, +// filters, +// setFilters, +// handleUserClick, +// handleUserUpdate, +// clearFilters, +// getActiveFilterCount, +// } +// } + +// export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => { +// return users.filter((user) => { + +// // Global search +// if (searchQuery) { +// const query = searchQuery.toLowerCase() +// const matchesSearch = +// user.email?.toLowerCase().includes(query) || +// user.phone?.toLowerCase().includes(query) || +// user.id.toLowerCase().includes(query) + +// if (!matchesSearch) return false +// } + +// // Email filter +// if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) { +// return false +// } + +// // Phone filter +// if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) { +// return false +// } + +// // Last sign in filter +// if (filters.lastSignIn) { +// if (filters.lastSignIn === "never" && user.last_sign_in_at) { +// return false +// } else if (filters.lastSignIn === "today") { +// const today = new Date() +// today.setHours(0, 0, 0, 0) +// const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null +// if (!signInDate || signInDate < today) return false +// } else if (filters.lastSignIn === "week") { +// const weekAgo = new Date() +// weekAgo.setDate(weekAgo.getDate() - 7) +// const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null +// if (!signInDate || signInDate < weekAgo) return false +// } else if (filters.lastSignIn === "month") { +// const monthAgo = new Date() +// monthAgo.setMonth(monthAgo.getMonth() - 1) +// const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null +// if (!signInDate || signInDate < monthAgo) return false +// } +// } + +// // Created at filter +// if (filters.createdAt) { +// if (filters.createdAt === "today") { +// const today = new Date() +// today.setHours(0, 0, 0, 0) +// const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date() +// if (createdAt < today) return false +// } else if (filters.createdAt === "week") { +// const weekAgo = new Date() +// weekAgo.setDate(weekAgo.getDate() - 7) +// const createdAt = user.created_at ? new Date(user.created_at) : new Date() +// if (createdAt < weekAgo) return false +// } else if (filters.createdAt === "month") { +// const monthAgo = new Date() +// monthAgo.setMonth(monthAgo.getMonth() - 1) +// const createdAt = user.created_at ? new Date(user.created_at) : new Date() +// if (createdAt < monthAgo) return false +// } +// } + +// // Status filter +// if (filters.status.length > 0) { +// const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active" + +// if (!filters.status.includes(userStatus)) { +// return false +// } +// } + +// return true +// }) +// } \ No newline at end of file diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts index a9b19b9..ae5a949 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/queries.ts @@ -1,212 +1,212 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { - banUser, - getCurrentUser, - getUserByEmail, - getUserById, - getUsers, - unbanUser, - inviteUser, - createUser, - updateUser, - deleteUser, - getUserByUsername -} from "./action"; -import { IUserSchema } from "@/src/entities/models/users/users.model"; -import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; -import { ICredentialsUnbanUserSchema, IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; -import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; -import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; -import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"; -import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByEmailSchema, IGetUserByIdSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model"; +// import { useMutation, useQuery } from "@tanstack/react-query"; +// import { +// banUser, +// getCurrentUser, +// getUserByEmail, +// getUserById, +// getUsers, +// unbanUser, +// inviteUser, +// createUser, +// updateUser, +// deleteUser, +// getUserByUsername +// } from "./action"; +// import { IUserSchema } from "@/src/entities/models/users/users.model"; +// import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; +// import { ICredentialsUnbanUserSchema, IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; +// import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; +// import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; +// import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"; +// import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByEmailSchema, IGetUserByIdSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model"; -const useUsersAction = () => { +// const useUsersAction = () => { - // For all users (no parameters needed) - const getUsersQuery = useQuery({ - queryKey: ["users"], - queryFn: async () => await getUsers() - }); +// // For all users (no parameters needed) +// const getUsersQuery = useQuery({ +// queryKey: ["users"], +// queryFn: () => getUsers() +// }); - // Current user query doesn't need parameters - const getCurrentUserQuery = useQuery({ - queryKey: ["user", "current"], - queryFn: async () => await getCurrentUser() - }); +// // Current user query doesn't need parameters +// const getCurrentUserQuery = useQuery({ +// queryKey: ["user", "current"], +// queryFn: () => getCurrentUser() +// }); - const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery({ - queryKey: ["user", "id", credential.id], - queryFn: async () => await getUserById(credential) - }); +// const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery({ +// queryKey: ["user", "id", credential.id], +// queryFn: () => getUserById(credential) +// }); - const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery({ - queryKey: ["user", "email", credential.email], - queryFn: async () => await getUserByEmail(credential) - }); +// const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery({ +// queryKey: ["user", "email", credential.email], +// queryFn: () => getUserByEmail(credential) +// }); - const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery({ - queryKey: ["user", "username", credential.username], - queryFn: async () => await getUserByUsername(credential) - }); +// const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery({ +// queryKey: ["user", "username", credential.username], +// queryFn: () => getUserByUsername(credential) +// }); - // Mutations that don't need dynamic parameters - const banUserMutation = useMutation({ - mutationKey: ["banUser"], - mutationFn: async ({ credential, data }: { credential: ICredentialsBanUserSchema; data: IBanUserSchema }) => await banUser(credential, data) - }); +// // Mutations that don't need dynamic parameters +// const banUserMutation = (credential: ICredentialsBanUserSchema, data: IBanUserSchema) => useMutation({ +// mutationKey: ["banUser"], +// mutationFn: () => banUser(credential, data) +// }); - const unbanUserMutation = useMutation({ - mutationKey: ["unbanUser"], - mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential) - }); +// const unbanUserMutation = useMutation({ +// mutationKey: ["unbanUser"], +// mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential) +// }); - // Create functions that return configured hooks - const inviteUserMutation = useMutation({ - mutationKey: ["inviteUser"], - mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential) - }); +// // Create functions that return configured hooks +// const inviteUserMutation = useMutation({ +// mutationKey: ["inviteUser"], +// mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential) +// }); - const createUserMutation = useMutation({ - mutationKey: ["createUser"], - mutationFn: async (data: ICreateUserSchema) => await createUser(data) - }); +// const createUserMutation = useMutation({ +// mutationKey: ["createUser"], +// mutationFn: async (data: ICreateUserSchema) => await createUser(data) +// }); - const updateUserMutation = useMutation({ - mutationKey: ["updateUser"], - mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data) - }); +// const updateUserMutation = useMutation({ +// mutationKey: ["updateUser"], +// mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data) +// }); - const deleteUserMutation = useMutation({ - mutationKey: ["deleteUser"], - mutationFn: async (id: string) => await deleteUser(id) - }); +// const deleteUserMutation = useMutation({ +// mutationKey: ["deleteUser"], +// mutationFn: async (id: string) => await deleteUser(id) +// }); - return { - getUsers: getUsersQuery, - getCurrentUser: getCurrentUserQuery, - getUserById: getUserByIdQuery, - getUserByEmailQuery, - getUserByUsernameQuery, - banUser: banUserMutation, - unbanUser: unbanUserMutation, - inviteUser: inviteUserMutation, - createUser: createUserMutation, - updateUser: updateUserMutation, - deleteUser: deleteUserMutation - }; -} +// return { +// getUsers: getUsersQuery, +// getCurrentUser: getCurrentUserQuery, +// getUserById: getUserByIdQuery, +// getUserByEmailQuery, +// getUserByUsernameQuery, +// banUser: banUserMutation, +// unbanUser: unbanUserMutation, +// inviteUser: inviteUserMutation, +// createUser: createUserMutation, +// updateUser: updateUserMutation, +// deleteUser: deleteUserMutation +// }; +// } -export const useGetUsersQuery = () => { - const { getUsers } = useUsersAction(); +// export const useGetUsersQuery = () => { +// const { getUsers } = useUsersAction(); - return { - data: getUsers.data, - isPending: getUsers.isPending, - error: getUsers.error, - refetch: getUsers.refetch, - }; -} +// return { +// data: getUsers.data, +// isPending: getUsers.isPending, +// error: getUsers.error, +// refetch: getUsers.refetch, +// }; +// } -export const useGetCurrentUserQuery = () => { - const { getCurrentUser } = useUsersAction(); +// export const useGetCurrentUserQuery = () => { +// const { getCurrentUser } = useUsersAction(); - return { - data: getCurrentUser.data, - isPending: getCurrentUser.isPending, - error: getCurrentUser.error, - refetch: getCurrentUser.refetch, - }; -} +// return { +// data: getCurrentUser.data, +// isPending: getCurrentUser.isPending, +// error: getCurrentUser.error, +// refetch: getCurrentUser.refetch, +// }; +// } -export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => { - const { getUserById } = useUsersAction(); +// export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => { +// const { getUserById } = useUsersAction(); - return { - data: getUserById(credential).data, - isPending: getUserById(credential).isPending, - error: getUserById(credential).error, - refetch: getUserById(credential).refetch, - }; -} +// return { +// data: getUserById(credential).data, +// isPending: getUserById(credential).isPending, +// error: getUserById(credential).error, +// refetch: getUserById(credential).refetch, +// }; +// } -export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => { - const { getUserByEmailQuery } = useUsersAction(); +// export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => { +// const { getUserByEmailQuery } = useUsersAction(); - return { - data: getUserByEmailQuery(credential).data, - isPending: getUserByEmailQuery(credential).isPending, - error: getUserByEmailQuery(credential).error, - refetch: getUserByEmailQuery(credential).refetch, - }; -} +// return { +// data: getUserByEmailQuery(credential).data, +// isPending: getUserByEmailQuery(credential).isPending, +// error: getUserByEmailQuery(credential).error, +// refetch: getUserByEmailQuery(credential).refetch, +// }; +// } -export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => { - const { getUserByUsernameQuery } = useUsersAction(); +// export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => { +// const { getUserByUsernameQuery } = useUsersAction(); - return { - data: getUserByUsernameQuery(credential).data, - isPending: getUserByUsernameQuery(credential).isPending, - error: getUserByUsernameQuery(credential).error, - refetch: getUserByUsernameQuery(credential).refetch, - }; -} +// return { +// data: getUserByUsernameQuery(credential).data, +// isPending: getUserByUsernameQuery(credential).isPending, +// error: getUserByUsernameQuery(credential).error, +// refetch: getUserByUsernameQuery(credential).refetch, +// }; +// } -export const useCreateUserMutation = () => { - const { createUser } = useUsersAction(); +// export const useCreateUserMutation = () => { +// const { createUser } = useUsersAction(); - return { - createUser: createUser.mutateAsync, - isPending: createUser.isPending, - errors: createUser.error, - } -} +// return { +// createUser: createUser.mutateAsync, +// isPending: createUser.isPending, +// errors: createUser.error, +// } +// } -export const useInviteUserMutation = () => { - const { inviteUser } = useUsersAction(); +// export const useInviteUserMutation = () => { +// const { inviteUser } = useUsersAction(); - return { - inviteUser: inviteUser.mutateAsync, - isPending: inviteUser.isPending, - errors: inviteUser.error, - } -} +// return { +// inviteUser: inviteUser.mutateAsync, +// isPending: inviteUser.isPending, +// errors: inviteUser.error, +// } +// } -export const useUpdateUserMutation = () => { - const { updateUser } = useUsersAction(); +// export const useUpdateUserMutation = () => { +// const { updateUser } = useUsersAction(); - return { - updateUser: updateUser.mutateAsync, - isPending: updateUser.isPending, - errors: updateUser.error, - } -} +// return { +// updateUser: updateUser.mutateAsync, +// isPending: updateUser.isPending, +// errors: updateUser.error, +// } +// } -export const useBanUserMutation = () => { - const { banUser } = useUsersAction(); +// export const useBanUserMutation = () => { +// const { banUser } = useUsersAction(); - return { - banUser: banUser.mutateAsync, - isPending: banUser.isPending, - errors: banUser.error, - } -} +// return { +// banUser: banUser.mutateAsync, +// isPending: banUser.isPending, +// errors: banUser.error, +// } +// } -export const useUnbanUserMutation = () => { - const { unbanUser } = useUsersAction(); +// export const useUnbanUserMutation = () => { +// const { unbanUser } = useUsersAction(); - return { - unbanUser: unbanUser.mutateAsync, - isPending: unbanUser.isPending, - errors: unbanUser.error, - } -} +// return { +// unbanUser: unbanUser.mutateAsync, +// isPending: unbanUser.isPending, +// errors: unbanUser.error, +// } +// } -export const useDeleteUserMutation = () => { - const { deleteUser } = useUsersAction(); +// export const useDeleteUserMutation = () => { +// const { deleteUser } = useUsersAction(); - return { - deleteUser: deleteUser.mutateAsync, - isPending: deleteUser.isPending, - errors: deleteUser.error, - } -} \ No newline at end of file +// return { +// deleteUser: deleteUser.mutateAsync, +// isPending: deleteUser.isPending, +// errors: deleteUser.error, +// } +// } \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx index df18463..699755b 100644 --- a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx @@ -10,7 +10,7 @@ import { FormField } from "@/app/_components/form-field"; // import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller"; import { useState } from "react"; import { signIn } from "../action"; -import { useSignInHandler } from "../handler"; +import { useSignInHandler } from "../_handlers/use-sign-in"; export function SignInForm({ className, @@ -33,7 +33,7 @@ export function SignInForm({ // setLoading(false); // }; - const { isPending, handleSignIn, error, errors, clearError } = useSignInHandler(); + const { register, isPending, handleSignIn, error, errors } = useSignInHandler(); return (
@@ -81,9 +81,7 @@ export function SignInForm({ label="Email" input={ {} diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts new file mode 100644 index 0000000..b35b081 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts @@ -0,0 +1,80 @@ +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { useSignInMutation } from "../_queries/mutations"; +import { toast } from "sonner"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { ISignInPasswordlessSchema, SignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"; +import { zodResolver } from "@hookform/resolvers/zod"; + +export function useSignInHandler() { + const { mutateAsync: signIn, isPending, error: errors } = useSignInMutation(); + const { router } = useNavigations(); + + const [error, setError] = useState(); + + const { + register, + reset, + formState: { errors: formErrors }, + setError: setFormError, + } = useForm({ + defaultValues: { + email: "", + }, + resolver: zodResolver(SignInPasswordlessSchema), + }) + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (isPending) return; + + setError(undefined); + + const formData = new FormData(event.currentTarget); + const email = formData.get('email')?.toString(); + + const res = await signIn(formData); + + if (!res?.error) { + toast('An email has been sent to you. Please check your inbox.'); + if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); + } else { + setError(res.error); + } + + + }; + + // const onSubmit = handleSubmit(async (data) => { + // if (isPending) return; + + // console.log(data); + + // setError(undefined); + + // const { email } = data; + + // const formData = new FormData(); + // formData.append('email', email); + + // const res = await signIn(formData); + + // if (!res?.error) { + // toast('An email has been sent to you. Please check your inbox.'); + // router.push(`/verify-otp?email=${encodeURIComponent(email)}`); + // } else { + // setError(res.error); + // } + // }) + + return { + // formData, + // handleChange, + reset, + register, + handleSignIn: handleSubmit, + error, + isPending, + errors: !!error || errors, + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-out.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-out.ts new file mode 100644 index 0000000..afbc9dc --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-out.ts @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { useSignOutMutation } from "../_queries/mutations"; +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { toast } from "sonner"; +import { AuthenticationError } from "@/src/entities/errors/auth"; + +export function useSignOutHandler() { + const { mutateAsync: signOut, isPending, error: errors } = useSignOutMutation(); + const { router } = useNavigations(); + const [error, setError] = useState(); + + const handleSignOut = async () => { + if (isPending) return; + + setError(undefined); + + await signOut(undefined, { + onSuccess: () => { + toast.success('You have been signed out successfully'); + router.push('/sign-in'); + }, + onError: (error) => { + if (error instanceof AuthenticationError) { + setError(error.message); + toast.error(error.message); + } + }, + }); + }; + + return { + handleSignOut, + error, + isPending: isPending, + errors: !!error || errors, + clearError: () => setError(undefined), + }; +} diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-verify-otp.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-verify-otp.ts new file mode 100644 index 0000000..6294ce1 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-verify-otp.ts @@ -0,0 +1,81 @@ +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { IVerifyOtpSchema, verifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { useVerifyOtpMutation } from "../_queries/mutations"; + +export function useVerifyOtpHandler(email: string) { + const { mutateAsync: verifyOtp, isPending } = useVerifyOtpMutation(); + const { router } = useNavigations(); + const [error, setError] = useState(); + + const { + register, + handleSubmit: hookFormSubmit, + control, + formState: { errors }, + setValue, + reset + } = useForm({ + resolver: zodResolver(verifyOtpSchema), + defaultValues: { + email, + token: '', + }, + }); + + const handleOtpChange = ( + value: string, + onChange: (value: string) => void + ) => { + onChange(value); + + if (value.length === 6) { + handleSubmit(); + } + + // Clear error when user starts typing + if (error) { + setError(undefined); + } + }; + + const handleSubmit = hookFormSubmit(async (data) => { + if (isPending) return; + + setError(undefined); + + // Create FormData object + const formData = new FormData(); + formData.append('email', data.email); + formData.append('token', data.token); + + await verifyOtp(formData, { + onSuccess: () => { + toast.success('OTP verified successfully'); + // Navigate to dashboard on success + router.push('/dashboard'); + }, + onError: (error) => { + setError(error.message); + }, + }); + }); + + return { + register, + control, + handleVerifyOtp: handleSubmit, + handleOtpChange, + errors: { + ...errors, + token: error ? { message: error } : errors.token, + }, + isPending: isPending, + clearError: () => setError(undefined), + reset, + }; +} + diff --git a/sigap-website/app/(pages)/(auth)/_queries/mutations.ts b/sigap-website/app/(pages)/(auth)/_queries/mutations.ts new file mode 100644 index 0000000..9d4ee98 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_queries/mutations.ts @@ -0,0 +1,37 @@ +import { useMutation } from "@tanstack/react-query" +import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from "../action" + +export const useSignInMutation = () => { + return useMutation({ + mutationKey: ["signIn"], + mutationFn: async (formData: FormData) => await signIn(formData), + }) +} + +export const useSignOutMutation = () => { + return useMutation({ + mutationKey: ["signOut"], + mutationFn: async () => await signOut(), + }) +} + +export const useSendMagicLinkMutation = () => { + return useMutation({ + mutationKey: ["sendMagicLink"], + mutationFn: async (email: string) => await sendMagicLink(email), + }) +} + +export const useSendPasswordRecoveryMutation = () => { + return useMutation({ + mutationKey: ["sendPasswordRecovery"], + mutationFn: async (email: string) => await sendPasswordRecovery(email), + }) +} + +export const useVerifyOtpMutation = () => { + return useMutation({ + mutationKey: ["verifyOtp"], + mutationFn: async (formData: FormData) => await verifyOtp(formData), + }) +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/handler.tsx b/sigap-website/app/(pages)/(auth)/handler.tsx index 5fc335b..7817f5a 100644 --- a/sigap-website/app/(pages)/(auth)/handler.tsx +++ b/sigap-website/app/(pages)/(auth)/handler.tsx @@ -1,161 +1,161 @@ -import { AuthenticationError } from "@/src/entities/errors/auth"; -import { useState } from "react"; -import { useAuthActions } from './queries'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod';; -import { toast } from 'sonner'; -import { useNavigations } from '@/app/_hooks/use-navigations'; -import { - IVerifyOtpSchema, - verifyOtpSchema, -} from '@/src/entities/models/auth/verify-otp.model'; +// import { AuthenticationError } from "@/src/entities/errors/auth"; +// import { useState } from "react"; +// import { useAuthActions } from './queries'; +// import { useForm } from 'react-hook-form'; +// import { zodResolver } from '@hookform/resolvers/zod';; +// import { toast } from 'sonner'; +// import { useNavigations } from '@/app/_hooks/use-navigations'; +// import { +// IVerifyOtpSchema, +// verifyOtpSchema, +// } from '@/src/entities/models/auth/verify-otp.model'; -/** - * Hook untuk menangani proses sign in - * - * @returns {Object} Object berisi handler dan state untuk form sign in - * @example - * const { handleSubmit, isPending, error } = useSignInHandler(); - *
...
- */ -export function useSignInHandler() { - const { signIn } = useAuthActions(); - const { router } = useNavigations(); +// /** +// * Hook untuk menangani proses sign in +// * +// * @returns {Object} Object berisi handler dan state untuk form sign in +// * @example +// * const { handleSubmit, isPending, error } = useSignInHandler(); +// *
...
+// */ +// export function useSignInHandler() { +// const { signIn } = useAuthActions(); +// const { router } = useNavigations(); - const [error, setError] = useState(); +// const [error, setError] = useState(); - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - if (signIn.isPending) return; +// const handleSubmit = async (event: React.FormEvent) => { +// event.preventDefault(); +// if (signIn.isPending) return; - setError(undefined); +// setError(undefined); - const formData = new FormData(event.currentTarget); - const email = formData.get('email')?.toString(); +// const formData = new FormData(event.currentTarget); +// const email = formData.get('email')?.toString(); - const res = await signIn.mutateAsync(formData); +// const res = await signIn.mutateAsync(formData); - if (!res?.error) { - toast('An email has been sent to you. Please check your inbox.'); - if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); - } else { - setError(res.error); - } +// if (!res?.error) { +// toast('An email has been sent to you. Please check your inbox.'); +// if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); +// } else { +// setError(res.error); +// } - }; +// }; - return { - // formData, - // handleChange, - handleSignIn: handleSubmit, - error, - isPending: signIn.isPending, - errors: !!error || signIn.error, - clearError: () => setError(undefined), - }; -} +// return { +// // formData, +// // handleChange, +// handleSignIn: handleSubmit, +// error, +// isPending: signIn.isPending, +// errors: !!error || signIn.error, +// clearError: () => setError(undefined), +// }; +// } -export function useVerifyOtpHandler(email: string) { - const { router } = useNavigations(); - const { verifyOtp } = useAuthActions(); - const [error, setError] = useState(); +// export function useVerifyOtpHandler(email: string) { +// const { router } = useNavigations(); +// const { verifyOtp } = useAuthActions(); +// const [error, setError] = useState(); - const { - register, - handleSubmit: hookFormSubmit, - control, - formState: { errors }, - setValue, - } = useForm({ - resolver: zodResolver(verifyOtpSchema), - defaultValues: { - email, - token: '', - }, - }); +// const { +// register, +// handleSubmit: hookFormSubmit, +// control, +// formState: { errors }, +// setValue, +// } = useForm({ +// resolver: zodResolver(verifyOtpSchema), +// defaultValues: { +// email, +// token: '', +// }, +// }); - const handleOtpChange = ( - value: string, - onChange: (value: string) => void - ) => { - onChange(value); +// const handleOtpChange = ( +// value: string, +// onChange: (value: string) => void +// ) => { +// onChange(value); - if (value.length === 6) { - handleSubmit(); - } +// if (value.length === 6) { +// handleSubmit(); +// } - // Clear error when user starts typing - if (error) { - setError(undefined); - } - }; +// // Clear error when user starts typing +// if (error) { +// setError(undefined); +// } +// }; - const handleSubmit = hookFormSubmit(async (data) => { - if (verifyOtp.isPending) return; +// const handleSubmit = hookFormSubmit(async (data) => { +// if (verifyOtp.isPending) return; - setError(undefined); +// setError(undefined); - // Create FormData object - const formData = new FormData(); - formData.append('email', data.email); - formData.append('token', data.token); +// // Create FormData object +// const formData = new FormData(); +// formData.append('email', data.email); +// formData.append('token', data.token); - await verifyOtp.mutateAsync(formData, { - onSuccess: () => { - toast.success('OTP verified successfully'); - // Navigate to dashboard on success - router.push('/dashboard'); - }, - onError: (error) => { - setError(error.message); - }, - }); - }); +// await verifyOtp.mutateAsync(formData, { +// onSuccess: () => { +// toast.success('OTP verified successfully'); +// // Navigate to dashboard on success +// router.push('/dashboard'); +// }, +// onError: (error) => { +// setError(error.message); +// }, +// }); +// }); - return { - register, - control, - handleVerifyOtp: handleSubmit, - handleOtpChange, - errors: { - ...errors, - token: error ? { message: error } : errors.token, - }, - isPending: verifyOtp.isPending, - clearError: () => setError(undefined), - }; -} +// return { +// register, +// control, +// handleVerifyOtp: handleSubmit, +// handleOtpChange, +// errors: { +// ...errors, +// token: error ? { message: error } : errors.token, +// }, +// isPending: verifyOtp.isPending, +// clearError: () => setError(undefined), +// }; +// } -export function useSignOutHandler() { - const { signOut } = useAuthActions(); - const { router } = useNavigations(); - const [error, setError] = useState(); +// export function useSignOutHandler() { +// const { signOut } = useAuthActions(); +// const { router } = useNavigations(); +// const [error, setError] = useState(); - const handleSignOut = async () => { - if (signOut.isPending) return; +// const handleSignOut = async () => { +// if (signOut.isPending) return; - setError(undefined); +// setError(undefined); - await signOut.mutateAsync(undefined, { - onSuccess: () => { - toast.success('You have been signed out successfully'); - router.push('/sign-in'); - }, - onError: (error) => { - if (error instanceof AuthenticationError) { - setError(error.message); - toast.error(error.message); - } - }, - }); - }; +// await signOut.mutateAsync(undefined, { +// onSuccess: () => { +// toast.success('You have been signed out successfully'); +// router.push('/sign-in'); +// }, +// onError: (error) => { +// if (error instanceof AuthenticationError) { +// setError(error.message); +// toast.error(error.message); +// } +// }, +// }); +// }; - return { - handleSignOut, - error, - isPending: signOut.isPending, - errors: !!error || signOut.error, - clearError: () => setError(undefined), - }; -} +// return { +// handleSignOut, +// error, +// isPending: signOut.isPending, +// errors: !!error || signOut.error, +// clearError: () => setError(undefined), +// }; +// } diff --git a/sigap-website/app/(pages)/(auth)/queries.ts b/sigap-website/app/(pages)/(auth)/queries.ts index c80b775..2196f00 100644 --- a/sigap-website/app/(pages)/(auth)/queries.ts +++ b/sigap-website/app/(pages)/(auth)/queries.ts @@ -1,89 +1,89 @@ -import { useMutation } from '@tanstack/react-query'; -import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action'; +// import { useMutation } from '@tanstack/react-query'; +// import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action'; -export function useAuthActions() { - // Sign In Mutation - const signInMutation = useMutation({ - mutationKey: ["signIn"], - mutationFn: async (formData: FormData) => await signIn(formData) - }); +// export function useAuthActions() { +// // Sign In Mutation +// const signInMutation = useMutation({ +// mutationKey: ["signIn"], +// mutationFn: async (formData: FormData) => await signIn(formData) +// }); - // Verify OTP Mutation - const verifyOtpMutation = useMutation({ - mutationKey: ["verifyOtp"], - mutationFn: async (formData: FormData) => await verifyOtp(formData) - }); +// // Verify OTP Mutation +// const verifyOtpMutation = useMutation({ +// mutationKey: ["verifyOtp"], +// mutationFn: async (formData: FormData) => await verifyOtp(formData) +// }); - const signOutMutation = useMutation({ - mutationKey: ["signOut"], - mutationFn: async () => await signOut() - }); +// const signOutMutation = useMutation({ +// mutationKey: ["signOut"], +// mutationFn: async () => await signOut() +// }); - const sendMagicLinkMutation = useMutation({ - mutationKey: ["sendMagicLink"], - mutationFn: async (email: string) => await sendMagicLink(email) - }); +// const sendMagicLinkMutation = useMutation({ +// mutationKey: ["sendMagicLink"], +// mutationFn: async (email: string) => await sendMagicLink(email) +// }); - const sendPasswordRecoveryMutation = useMutation({ - mutationKey: ["sendPasswordRecovery"], - mutationFn: async (email: string) => await sendPasswordRecovery(email) - }); +// const sendPasswordRecoveryMutation = useMutation({ +// mutationKey: ["sendPasswordRecovery"], +// mutationFn: async (email: string) => await sendPasswordRecovery(email) +// }); - return { - signIn: signInMutation, - verifyOtp: verifyOtpMutation, - signOut: signOutMutation, - sendMagicLink: sendMagicLinkMutation, - sendPasswordRecovery: sendPasswordRecoveryMutation - }; -} +// return { +// signIn: signInMutation, +// verifyOtp: verifyOtpMutation, +// signOut: signOutMutation, +// sendMagicLink: sendMagicLinkMutation, +// sendPasswordRecovery: sendPasswordRecoveryMutation +// }; +// } -export const useSignInMutation = () => { - const { signIn } = useAuthActions(); +// export const useSignInMutation = () => { +// const { signIn } = useAuthActions(); - return { - signIn: signIn.mutateAsync, - isPending: signIn.isPending, - error: signIn.error, - }; -} +// return { +// signIn: signIn.mutateAsync, +// isPending: signIn.isPending, +// error: signIn.error, +// }; +// } -export const useVerifyOtpMutation = () => { - const { verifyOtp } = useAuthActions(); +// export const useVerifyOtpMutation = () => { +// const { verifyOtp } = useAuthActions(); - return { - verifyOtp: verifyOtp.mutateAsync, - isPending: verifyOtp.isPending, - error: verifyOtp.error, - } -} +// return { +// verifyOtp: verifyOtp.mutateAsync, +// isPending: verifyOtp.isPending, +// error: verifyOtp.error, +// } +// } -export const useSignOutMutation = () => { - const { signOut } = useAuthActions(); +// export const useSignOutMutation = () => { +// const { signOut } = useAuthActions(); - return { - signOut: signOut.mutateAsync, - isPending: signOut.isPending - } -} +// return { +// signOut: signOut.mutateAsync, +// isPending: signOut.isPending +// } +// } -export const useSendMagicLinkMutation = () => { - const { sendMagicLink } = useAuthActions(); +// export const useSendMagicLinkMutation = () => { +// const { sendMagicLink } = useAuthActions(); - return { - sendMagicLink: sendMagicLink.mutateAsync, - isPending: sendMagicLink.isPending, - error: sendMagicLink.error, - } -} +// return { +// sendMagicLink: sendMagicLink.mutateAsync, +// isPending: sendMagicLink.isPending, +// error: sendMagicLink.error, +// } +// } -export const useSendPasswordRecoveryMutation = () => { - const { sendPasswordRecovery } = useAuthActions(); +// export const useSendPasswordRecoveryMutation = () => { +// const { sendPasswordRecovery } = useAuthActions(); - return { - sendPasswordRecovery: sendPasswordRecovery.mutateAsync, - isPending: sendPasswordRecovery.isPending, - error: sendPasswordRecovery.error, - } -} +// return { +// sendPasswordRecovery: sendPasswordRecovery.mutateAsync, +// isPending: sendPasswordRecovery.isPending, +// error: sendPasswordRecovery.error, +// } +// } diff --git a/sigap-website/app/_utils/common.ts b/sigap-website/app/_utils/common.ts index bb6ff0a..5e3b0d5 100644 --- a/sigap-website/app/_utils/common.ts +++ b/sigap-website/app/_utils/common.ts @@ -1,6 +1,7 @@ import { format } from "date-fns"; import { redirect } from "next/navigation"; import { DateFormatOptions, DateFormatPattern } from "../_lib/types/date-format.interface"; +import { toast } from "sonner"; /** * Redirects to a specified path with an encoded message as a query parameter. @@ -117,4 +118,33 @@ export const formatDate = ( return locale ? format(dateObj, formatPattern, { locale }) : format(dateObj, formatPattern); +}; + +export const handleCopyItem = (item: string, options?: { + onSuccess?: () => void, + onError?: (error: unknown) => void +}) => { + if (!navigator.clipboard) { + const error = new Error("Clipboard not supported"); + toast.error("Clipboard not supported"); + options?.onError?.(error); + return; + } + + if (!item) { + const error = new Error("Nothing to copy"); + toast.error("Nothing to copy"); + options?.onError?.(error); + return; + } + + navigator.clipboard.writeText(item) + .then(() => { + toast.success("Copied to clipboard"); + options?.onSuccess?.(); + }) + .catch((error) => { + toast.error("Failed to copy to clipboard"); + options?.onError?.(error); + }); }; \ No newline at end of file