refactor handler and queries

This commit is contained in:
vergiLgood1 2025-03-28 22:37:51 +07:00
parent e84a6f52c0
commit 2faf6ce83e
27 changed files with 1959 additions and 1198 deletions

View File

@ -16,7 +16,7 @@ import {
import { NavPreMain } from "./navigations/nav-pre-main"; import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/prisma/data/nav"; import { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../../../_components/team-switcher"; 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<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {

View File

@ -27,8 +27,8 @@ import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
import type { IUserSchema } from "@/src/entities/models/users/users.model"; import type { IUserSchema } from "@/src/entities/models/users/users.model";
// import { signOut } from "@/app/(pages)/(auth)/action"; // import { signOut } from "@/app/(pages)/(auth)/action";
import { SettingsDialog } from "../settings/setting-dialog"; 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 { 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 }) { export function NavUser({ user }: { user: IUserSchema | null }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();

View File

@ -1,8 +1,8 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog"
import { Button } from "@/app/_components/ui/button" import { Button } from "@/app/_components/ui/button"
import { Mail, Lock, Loader2 } from "lucide-react" import { Mail, Lock, Loader2 } from "lucide-react"
import { useAddUserDialogHandler } from "../handler"
import { ReactHookFormField } from "@/app/_components/react-hook-form-field" import { ReactHookFormField } from "@/app/_components/react-hook-form-field"
import { useAddUserDialogHandler } from "../_handlers/use-add-user-dialog"
interface AddUserDialogProps { interface AddUserDialogProps {
open: boolean open: boolean

View File

@ -16,10 +16,10 @@ import { Textarea } from "@/app/_components/ui/textarea";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { toast } from "sonner"; import { toast } from "sonner";
import { useInviteUserHandler } from "../handler";
import { ReactHookFormField } from "@/app/_components/react-hook-form-field"; import { ReactHookFormField } from "@/app/_components/react-hook-form-field";
import { Loader2, MailIcon } from "lucide-react"; import { Loader2, MailIcon } from "lucide-react";
import { Separator } from "@/app/_components/ui/separator"; import { Separator } from "@/app/_components/ui/separator";
import { useInviteUserHandler } from "../_handlers/use-invite-user";
interface InviteUserDialogProps { interface InviteUserDialogProps {

View File

@ -39,9 +39,9 @@ import {
} from "@/app/(pages)/(admin)/dashboard/user-management/action"; } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { format } from "date-fns"; import { format } from "date-fns";
import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action"; import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action";
import { useUserDetailSheetHandlers } from "../handler";
import { IUserSchema } from "@/src/entities/models/users/users.model"; import { IUserSchema } from "@/src/entities/models/users/users.model";
import { formatDate } from "@/app/_utils/common"; import { formatDate } from "@/app/_utils/common";
import { useUserDetailSheetHandlers } from "../_handlers/use-detail-sheet";
interface UserDetailSheetProps { interface UserDetailSheetProps {
open: boolean; open: boolean;
@ -56,104 +56,6 @@ export function UserDetailSheet({
user, user,
onUserUpdated, onUserUpdated,
}: UserDetailSheetProps) { }: 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 { const {
handleDeleteUser, handleDeleteUser,
@ -364,7 +266,7 @@ export function UserDetailSheet({
<Button <Button
variant={user.banned_until ? "outline" : "outline"} variant={user.banned_until ? "outline" : "outline"}
size="sm" size="sm"
onClick={handleToggleBan} onClick={() => handleToggleBan()}
disabled={isBanPending || isUnbanPending} disabled={isBanPending || isUnbanPending}
> >
{isBanPending || isUnbanPending ? ( {isBanPending || isUnbanPending ? (

View File

@ -18,7 +18,7 @@ import { useMutation } from "@tanstack/react-query"
import { updateUser } from "../action" import { updateUser } from "../action"
import { toast } from "sonner" import { toast } from "sonner"
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model" import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model"
import { useUserProfileSheetHandlers } from "../handler" import { useUserProfileSheetHandlers } from "../_handlers/use-profile-sheet"
type UserProfileFormValues = z.infer<typeof UpdateUserSchema> type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
@ -34,7 +34,7 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
const { const {
form, form,
handleUpdateUser, handleUpdateUser,
isUpdatePending, isPending,
} = useUserProfileSheetHandlers({ open, userData, onOpenChange, onUserUpdated }) } = useUserProfileSheetHandlers({ open, userData, onOpenChange, onUserUpdated })
return ( return (
@ -214,14 +214,14 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
type="button" type="button"
variant="outline" variant="outline"
size="xs" size="xs"
onClick={() => !isUpdatePending && onOpenChange(false)} onClick={() => !isPending && onOpenChange(false)}
disabled={isUpdatePending} disabled={isPending}
> >
Cancel Cancel
</Button> </Button>
<Button size="xs" type="submit" disabled={isUpdatePending}> <Button size="xs" type="submit" disabled={isPending}>
{isUpdatePending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />} {isPending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
{isUpdatePending ? "Saving..." : "Save"} {isPending ? "Saving..." : "Save"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -28,9 +28,9 @@ import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog"; import { AddUserDialog } from "./add-user-dialog";
import { UserDetailSheet } from "./sheet"; import { UserDetailSheet } from "./sheet";
import { UserProfileSheet } from "./update-user"; import { UserProfileSheet } from "./update-user";
import { filterUsers, useUserManagementHandlers } from "../handler";
import { createUserColumns } from "./users-table"; 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() { export default function UserManagement() {
@ -61,7 +61,7 @@ export default function UserManagement() {
handleUserUpdate, handleUserUpdate,
clearFilters, clearFilters,
getActiveFilterCount, getActiveFilterCount,
} = useUserManagementHandlers(refetch) } = useUserManagementHandlers()
// Apply filters to users // Apply filters to users
const filteredUsers = useMemo(() => { const filteredUsers = useMemo(() => {

View File

@ -3,7 +3,7 @@
import { Card, CardContent } from "@/app/_components/ui/card"; import { Card, CardContent } from "@/app/_components/ui/card";
import { Users, UserCheck, UserX } from "lucide-react"; import { Users, UserCheck, UserX } from "lucide-react";
import { IUserSchema } from "@/src/entities/models/users/users.model"; import { IUserSchema } from "@/src/entities/models/users/users.model";
import { useGetUsersQuery } from "../queries"; import { useGetUsersQuery } from "../_queries/queries";
function calculateUserStats(users: IUserSchema[] | undefined) { function calculateUserStats(users: IUserSchema[] | undefined) {

View File

@ -16,7 +16,7 @@ import { Input } from "@/app/_components/ui/input"
import { Avatar } from "@/app/_components/ui/avatar" import { Avatar } from "@/app/_components/ui/avatar"
import Image from "next/image" import Image from "next/image"
import { Badge } from "@/app/_components/ui/badge" import { Badge } from "@/app/_components/ui/badge"
import { useUserDetailSheetHandlers, useUsersHandlers } from "../handler" import { useBanUserMutation, useDeleteUserMutation, useUnbanUserMutation } from "../_queries/mutations"
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema> export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
@ -26,11 +26,9 @@ export const createUserColumns = (
handleUserUpdate: (user: IUserSchema) => void, handleUserUpdate: (user: IUserSchema) => void,
): UserTableColumn[] => { ): UserTableColumn[] => {
const { const { mutateAsync: deleteUser } = useDeleteUserMutation();
deleteUser, const { mutateAsync: banUser } = useBanUserMutation();
banUser, const { mutateAsync: unbanUser } = useUnbanUserMutation();
unbanUser,
} = useUsersHandlers();
return [ return [
{ {
@ -329,9 +327,9 @@ export const createUserColumns = (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (row.original.banned_until != null) { if (row.original.banned_until != null) {
unbanUser(row.original.id) unbanUser({ id: row.original.id })
} else { } else {
banUser(row.original.id) banUser({ id: row.original.id, ban_duration: "24h" })
} }
}} }}
> >

View File

@ -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<ICreateUserSchema>({
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,
};
}

View File

@ -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,
};
};

View File

@ -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<IInviteUserSchema>({
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,
};
}

View File

@ -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<IUpdateUserSchema>({
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,
};
}

View File

@ -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<IUserSchema | null>(null)
const [updateUser, setUpdateUser] = useState<IUserSchema | null>(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<IUserFilterOptionsSchema>({
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
})
}

View File

@ -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),
})
}

View File

@ -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<IUserSchema[]>({
queryKey: ["users"],
queryFn: () => getUsers()
});
}
export const useGetUserByEmailQuery = (email: string) => {
return useQuery<IUserSchema>({
queryKey: ["user", "email", email],
queryFn: () => getUserByEmail({ email }),
})
}
export const useGetUserByIdQuery = (id: string) => {
return useQuery<IUserSchema>({
queryKey: ["user", "id", id],
queryFn: () => getUserById({ id }),
})
}
export const useGetUserByUsernameQuery = (username: string) => {
return useQuery<IUserSchema>({
queryKey: ["user", "username", username],
queryFn: () => getUserByUsername({ username }),
})
}
export const useGetCurrentUserQuery = () => {
return useQuery<IUserSchema>({
queryKey: ["user", "current"],
queryFn: () => getCurrentUser(),
})
}

View File

@ -1,212 +1,212 @@
import { useMutation, useQuery } from "@tanstack/react-query"; // import { useMutation, useQuery } from "@tanstack/react-query";
import { // import {
banUser, // banUser,
getCurrentUser, // getCurrentUser,
getUserByEmail, // getUserByEmail,
getUserById, // getUserById,
getUsers, // getUsers,
unbanUser, // unbanUser,
inviteUser, // inviteUser,
createUser, // createUser,
updateUser, // updateUser,
deleteUser, // deleteUser,
getUserByUsername // getUserByUsername
} from "./action"; // } from "./action";
import { IUserSchema } from "@/src/entities/models/users/users.model"; // import { IUserSchema } from "@/src/entities/models/users/users.model";
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.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 { ICredentialsUnbanUserSchema, IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; // import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; // import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-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 { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByEmailSchema, IGetUserByIdSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model";
const useUsersAction = () => { // const useUsersAction = () => {
// For all users (no parameters needed) // // For all users (no parameters needed)
const getUsersQuery = useQuery<IUserSchema[]>({ // const getUsersQuery = useQuery<IUserSchema[]>({
queryKey: ["users"], // queryKey: ["users"],
queryFn: async () => await getUsers() // queryFn: () => getUsers()
}); // });
// Current user query doesn't need parameters // // Current user query doesn't need parameters
const getCurrentUserQuery = useQuery<IUserSchema>({ // const getCurrentUserQuery = useQuery<IUserSchema>({
queryKey: ["user", "current"], // queryKey: ["user", "current"],
queryFn: async () => await getCurrentUser() // queryFn: () => getCurrentUser()
}); // });
const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery<IUserSchema>({ // const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery<IUserSchema>({
queryKey: ["user", "id", credential.id], // queryKey: ["user", "id", credential.id],
queryFn: async () => await getUserById(credential) // queryFn: () => getUserById(credential)
}); // });
const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery<IUserSchema>({ // const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery<IUserSchema>({
queryKey: ["user", "email", credential.email], // queryKey: ["user", "email", credential.email],
queryFn: async () => await getUserByEmail(credential) // queryFn: () => getUserByEmail(credential)
}); // });
const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery<IUserSchema>({ // const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery<IUserSchema>({
queryKey: ["user", "username", credential.username], // queryKey: ["user", "username", credential.username],
queryFn: async () => await getUserByUsername(credential) // queryFn: () => getUserByUsername(credential)
}); // });
// Mutations that don't need dynamic parameters // // Mutations that don't need dynamic parameters
const banUserMutation = useMutation({ // const banUserMutation = (credential: ICredentialsBanUserSchema, data: IBanUserSchema) => useMutation({
mutationKey: ["banUser"], // mutationKey: ["banUser"],
mutationFn: async ({ credential, data }: { credential: ICredentialsBanUserSchema; data: IBanUserSchema }) => await banUser(credential, data) // mutationFn: () => banUser(credential, data)
}); // });
const unbanUserMutation = useMutation({ // const unbanUserMutation = useMutation({
mutationKey: ["unbanUser"], // mutationKey: ["unbanUser"],
mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential) // mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential)
}); // });
// Create functions that return configured hooks // // Create functions that return configured hooks
const inviteUserMutation = useMutation({ // const inviteUserMutation = useMutation({
mutationKey: ["inviteUser"], // mutationKey: ["inviteUser"],
mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential) // mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential)
}); // });
const createUserMutation = useMutation({ // const createUserMutation = useMutation({
mutationKey: ["createUser"], // mutationKey: ["createUser"],
mutationFn: async (data: ICreateUserSchema) => await createUser(data) // mutationFn: async (data: ICreateUserSchema) => await createUser(data)
}); // });
const updateUserMutation = useMutation({ // const updateUserMutation = useMutation({
mutationKey: ["updateUser"], // mutationKey: ["updateUser"],
mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data) // mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data)
}); // });
const deleteUserMutation = useMutation({ // const deleteUserMutation = useMutation({
mutationKey: ["deleteUser"], // mutationKey: ["deleteUser"],
mutationFn: async (id: string) => await deleteUser(id) // mutationFn: async (id: string) => await deleteUser(id)
}); // });
return { // return {
getUsers: getUsersQuery, // getUsers: getUsersQuery,
getCurrentUser: getCurrentUserQuery, // getCurrentUser: getCurrentUserQuery,
getUserById: getUserByIdQuery, // getUserById: getUserByIdQuery,
getUserByEmailQuery, // getUserByEmailQuery,
getUserByUsernameQuery, // getUserByUsernameQuery,
banUser: banUserMutation, // banUser: banUserMutation,
unbanUser: unbanUserMutation, // unbanUser: unbanUserMutation,
inviteUser: inviteUserMutation, // inviteUser: inviteUserMutation,
createUser: createUserMutation, // createUser: createUserMutation,
updateUser: updateUserMutation, // updateUser: updateUserMutation,
deleteUser: deleteUserMutation // deleteUser: deleteUserMutation
}; // };
} // }
export const useGetUsersQuery = () => { // export const useGetUsersQuery = () => {
const { getUsers } = useUsersAction(); // const { getUsers } = useUsersAction();
return { // return {
data: getUsers.data, // data: getUsers.data,
isPending: getUsers.isPending, // isPending: getUsers.isPending,
error: getUsers.error, // error: getUsers.error,
refetch: getUsers.refetch, // refetch: getUsers.refetch,
}; // };
} // }
export const useGetCurrentUserQuery = () => { // export const useGetCurrentUserQuery = () => {
const { getCurrentUser } = useUsersAction(); // const { getCurrentUser } = useUsersAction();
return { // return {
data: getCurrentUser.data, // data: getCurrentUser.data,
isPending: getCurrentUser.isPending, // isPending: getCurrentUser.isPending,
error: getCurrentUser.error, // error: getCurrentUser.error,
refetch: getCurrentUser.refetch, // refetch: getCurrentUser.refetch,
}; // };
} // }
export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => { // export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => {
const { getUserById } = useUsersAction(); // const { getUserById } = useUsersAction();
return { // return {
data: getUserById(credential).data, // data: getUserById(credential).data,
isPending: getUserById(credential).isPending, // isPending: getUserById(credential).isPending,
error: getUserById(credential).error, // error: getUserById(credential).error,
refetch: getUserById(credential).refetch, // refetch: getUserById(credential).refetch,
}; // };
} // }
export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => { // export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => {
const { getUserByEmailQuery } = useUsersAction(); // const { getUserByEmailQuery } = useUsersAction();
return { // return {
data: getUserByEmailQuery(credential).data, // data: getUserByEmailQuery(credential).data,
isPending: getUserByEmailQuery(credential).isPending, // isPending: getUserByEmailQuery(credential).isPending,
error: getUserByEmailQuery(credential).error, // error: getUserByEmailQuery(credential).error,
refetch: getUserByEmailQuery(credential).refetch, // refetch: getUserByEmailQuery(credential).refetch,
}; // };
} // }
export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => { // export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => {
const { getUserByUsernameQuery } = useUsersAction(); // const { getUserByUsernameQuery } = useUsersAction();
return { // return {
data: getUserByUsernameQuery(credential).data, // data: getUserByUsernameQuery(credential).data,
isPending: getUserByUsernameQuery(credential).isPending, // isPending: getUserByUsernameQuery(credential).isPending,
error: getUserByUsernameQuery(credential).error, // error: getUserByUsernameQuery(credential).error,
refetch: getUserByUsernameQuery(credential).refetch, // refetch: getUserByUsernameQuery(credential).refetch,
}; // };
} // }
export const useCreateUserMutation = () => { // export const useCreateUserMutation = () => {
const { createUser } = useUsersAction(); // const { createUser } = useUsersAction();
return { // return {
createUser: createUser.mutateAsync, // createUser: createUser.mutateAsync,
isPending: createUser.isPending, // isPending: createUser.isPending,
errors: createUser.error, // errors: createUser.error,
} // }
} // }
export const useInviteUserMutation = () => { // export const useInviteUserMutation = () => {
const { inviteUser } = useUsersAction(); // const { inviteUser } = useUsersAction();
return { // return {
inviteUser: inviteUser.mutateAsync, // inviteUser: inviteUser.mutateAsync,
isPending: inviteUser.isPending, // isPending: inviteUser.isPending,
errors: inviteUser.error, // errors: inviteUser.error,
} // }
} // }
export const useUpdateUserMutation = () => { // export const useUpdateUserMutation = () => {
const { updateUser } = useUsersAction(); // const { updateUser } = useUsersAction();
return { // return {
updateUser: updateUser.mutateAsync, // updateUser: updateUser.mutateAsync,
isPending: updateUser.isPending, // isPending: updateUser.isPending,
errors: updateUser.error, // errors: updateUser.error,
} // }
} // }
export const useBanUserMutation = () => { // export const useBanUserMutation = () => {
const { banUser } = useUsersAction(); // const { banUser } = useUsersAction();
return { // return {
banUser: banUser.mutateAsync, // banUser: banUser.mutateAsync,
isPending: banUser.isPending, // isPending: banUser.isPending,
errors: banUser.error, // errors: banUser.error,
} // }
} // }
export const useUnbanUserMutation = () => { // export const useUnbanUserMutation = () => {
const { unbanUser } = useUsersAction(); // const { unbanUser } = useUsersAction();
return { // return {
unbanUser: unbanUser.mutateAsync, // unbanUser: unbanUser.mutateAsync,
isPending: unbanUser.isPending, // isPending: unbanUser.isPending,
errors: unbanUser.error, // errors: unbanUser.error,
} // }
} // }
export const useDeleteUserMutation = () => { // export const useDeleteUserMutation = () => {
const { deleteUser } = useUsersAction(); // const { deleteUser } = useUsersAction();
return { // return {
deleteUser: deleteUser.mutateAsync, // deleteUser: deleteUser.mutateAsync,
isPending: deleteUser.isPending, // isPending: deleteUser.isPending,
errors: deleteUser.error, // errors: deleteUser.error,
} // }
} // }

View File

@ -10,7 +10,7 @@ import { FormField } from "@/app/_components/form-field";
// import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller"; // import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller";
import { useState } from "react"; import { useState } from "react";
import { signIn } from "../action"; import { signIn } from "../action";
import { useSignInHandler } from "../handler"; import { useSignInHandler } from "../_handlers/use-sign-in";
export function SignInForm({ export function SignInForm({
className, className,
@ -33,7 +33,7 @@ export function SignInForm({
// setLoading(false); // setLoading(false);
// }; // };
const { isPending, handleSignIn, error, errors, clearError } = useSignInHandler(); const { register, isPending, handleSignIn, error, errors } = useSignInHandler();
return ( return (
<div> <div>
@ -81,9 +81,7 @@ export function SignInForm({
label="Email" label="Email"
input={ input={
<Input <Input
id="email" {...register("email")}
type="email"
name="email"
placeholder="you@example.com" placeholder="you@example.com"
className={`bg-[#1C1C1C] border-gray-800`} className={`bg-[#1C1C1C] border-gray-800`}
error={!!errors} error={!!errors}

View File

@ -16,8 +16,8 @@ import {
import { cn } from "@/app/_lib/utils"; import { cn } from "@/app/_lib/utils";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useVerifyOtpHandler } from "../handler";
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
import { useVerifyOtpHandler } from "../_handlers/use-verify-otp";
interface VerifyOtpFormProps extends React.HTMLAttributes<HTMLDivElement> {} interface VerifyOtpFormProps extends React.HTMLAttributes<HTMLDivElement> {}

View File

@ -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<string>();
const {
register,
reset,
formState: { errors: formErrors },
setError: setFormError,
} = useForm<ISignInPasswordlessSchema>({
defaultValues: {
email: "",
},
resolver: zodResolver(SignInPasswordlessSchema),
})
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
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,
};
}

View File

@ -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<string>();
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),
};
}

View File

@ -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<string>();
const {
register,
handleSubmit: hookFormSubmit,
control,
formState: { errors },
setValue,
reset
} = useForm<IVerifyOtpSchema>({
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,
};
}

View File

@ -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),
})
}

View File

@ -1,161 +1,161 @@
import { AuthenticationError } from "@/src/entities/errors/auth"; // import { AuthenticationError } from "@/src/entities/errors/auth";
import { useState } from "react"; // import { useState } from "react";
import { useAuthActions } from './queries'; // import { useAuthActions } from './queries';
import { useForm } from 'react-hook-form'; // import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';; // import { zodResolver } from '@hookform/resolvers/zod';;
import { toast } from 'sonner'; // import { toast } from 'sonner';
import { useNavigations } from '@/app/_hooks/use-navigations'; // import { useNavigations } from '@/app/_hooks/use-navigations';
import { // import {
IVerifyOtpSchema, // IVerifyOtpSchema,
verifyOtpSchema, // verifyOtpSchema,
} from '@/src/entities/models/auth/verify-otp.model'; // } from '@/src/entities/models/auth/verify-otp.model';
/** // /**
* Hook untuk menangani proses sign in // * Hook untuk menangani proses sign in
* // *
* @returns {Object} Object berisi handler dan state untuk form sign in // * @returns {Object} Object berisi handler dan state untuk form sign in
* @example // * @example
* const { handleSubmit, isPending, error } = useSignInHandler(); // * const { handleSubmit, isPending, error } = useSignInHandler();
* <form onSubmit={handleSubmit}>...</form> // * <form onSubmit={handleSubmit}>...</form>
*/ // */
export function useSignInHandler() { // export function useSignInHandler() {
const { signIn } = useAuthActions(); // const { signIn } = useAuthActions();
const { router } = useNavigations(); // const { router } = useNavigations();
const [error, setError] = useState<string>(); // const [error, setError] = useState<string>();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { // const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // event.preventDefault();
if (signIn.isPending) return; // if (signIn.isPending) return;
setError(undefined); // setError(undefined);
const formData = new FormData(event.currentTarget); // const formData = new FormData(event.currentTarget);
const email = formData.get('email')?.toString(); // const email = formData.get('email')?.toString();
const res = await signIn.mutateAsync(formData); // const res = await signIn.mutateAsync(formData);
if (!res?.error) { // if (!res?.error) {
toast('An email has been sent to you. Please check your inbox.'); // toast('An email has been sent to you. Please check your inbox.');
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); // if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
} else { // } else {
setError(res.error); // setError(res.error);
} // }
}; // };
return { // return {
// formData, // // formData,
// handleChange, // // handleChange,
handleSignIn: handleSubmit, // handleSignIn: handleSubmit,
error, // error,
isPending: signIn.isPending, // isPending: signIn.isPending,
errors: !!error || signIn.error, // errors: !!error || signIn.error,
clearError: () => setError(undefined), // clearError: () => setError(undefined),
}; // };
} // }
export function useVerifyOtpHandler(email: string) { // export function useVerifyOtpHandler(email: string) {
const { router } = useNavigations(); // const { router } = useNavigations();
const { verifyOtp } = useAuthActions(); // const { verifyOtp } = useAuthActions();
const [error, setError] = useState<string>(); // const [error, setError] = useState<string>();
const { // const {
register, // register,
handleSubmit: hookFormSubmit, // handleSubmit: hookFormSubmit,
control, // control,
formState: { errors }, // formState: { errors },
setValue, // setValue,
} = useForm<IVerifyOtpSchema>({ // } = useForm<IVerifyOtpSchema>({
resolver: zodResolver(verifyOtpSchema), // resolver: zodResolver(verifyOtpSchema),
defaultValues: { // defaultValues: {
email, // email,
token: '', // token: '',
}, // },
}); // });
const handleOtpChange = ( // const handleOtpChange = (
value: string, // value: string,
onChange: (value: string) => void // onChange: (value: string) => void
) => { // ) => {
onChange(value); // onChange(value);
if (value.length === 6) { // if (value.length === 6) {
handleSubmit(); // handleSubmit();
} // }
// Clear error when user starts typing // // Clear error when user starts typing
if (error) { // if (error) {
setError(undefined); // setError(undefined);
} // }
}; // };
const handleSubmit = hookFormSubmit(async (data) => { // const handleSubmit = hookFormSubmit(async (data) => {
if (verifyOtp.isPending) return; // if (verifyOtp.isPending) return;
setError(undefined); // setError(undefined);
// Create FormData object // // Create FormData object
const formData = new FormData(); // const formData = new FormData();
formData.append('email', data.email); // formData.append('email', data.email);
formData.append('token', data.token); // formData.append('token', data.token);
await verifyOtp.mutateAsync(formData, { // await verifyOtp.mutateAsync(formData, {
onSuccess: () => { // onSuccess: () => {
toast.success('OTP verified successfully'); // toast.success('OTP verified successfully');
// Navigate to dashboard on success // // Navigate to dashboard on success
router.push('/dashboard'); // router.push('/dashboard');
}, // },
onError: (error) => { // onError: (error) => {
setError(error.message); // setError(error.message);
}, // },
}); // });
}); // });
return { // return {
register, // register,
control, // control,
handleVerifyOtp: handleSubmit, // handleVerifyOtp: handleSubmit,
handleOtpChange, // handleOtpChange,
errors: { // errors: {
...errors, // ...errors,
token: error ? { message: error } : errors.token, // token: error ? { message: error } : errors.token,
}, // },
isPending: verifyOtp.isPending, // isPending: verifyOtp.isPending,
clearError: () => setError(undefined), // clearError: () => setError(undefined),
}; // };
} // }
export function useSignOutHandler() { // export function useSignOutHandler() {
const { signOut } = useAuthActions(); // const { signOut } = useAuthActions();
const { router } = useNavigations(); // const { router } = useNavigations();
const [error, setError] = useState<string>(); // const [error, setError] = useState<string>();
const handleSignOut = async () => { // const handleSignOut = async () => {
if (signOut.isPending) return; // if (signOut.isPending) return;
setError(undefined); // setError(undefined);
await signOut.mutateAsync(undefined, { // await signOut.mutateAsync(undefined, {
onSuccess: () => { // onSuccess: () => {
toast.success('You have been signed out successfully'); // toast.success('You have been signed out successfully');
router.push('/sign-in'); // router.push('/sign-in');
}, // },
onError: (error) => { // onError: (error) => {
if (error instanceof AuthenticationError) { // if (error instanceof AuthenticationError) {
setError(error.message); // setError(error.message);
toast.error(error.message); // toast.error(error.message);
} // }
}, // },
}); // });
}; // };
return { // return {
handleSignOut, // handleSignOut,
error, // error,
isPending: signOut.isPending, // isPending: signOut.isPending,
errors: !!error || signOut.error, // errors: !!error || signOut.error,
clearError: () => setError(undefined), // clearError: () => setError(undefined),
}; // };
} // }

View File

@ -1,89 +1,89 @@
import { useMutation } from '@tanstack/react-query'; // import { useMutation } from '@tanstack/react-query';
import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action'; // import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action';
export function useAuthActions() { // export function useAuthActions() {
// Sign In Mutation // // Sign In Mutation
const signInMutation = useMutation({ // const signInMutation = useMutation({
mutationKey: ["signIn"], // mutationKey: ["signIn"],
mutationFn: async (formData: FormData) => await signIn(formData) // mutationFn: async (formData: FormData) => await signIn(formData)
}); // });
// Verify OTP Mutation // // Verify OTP Mutation
const verifyOtpMutation = useMutation({ // const verifyOtpMutation = useMutation({
mutationKey: ["verifyOtp"], // mutationKey: ["verifyOtp"],
mutationFn: async (formData: FormData) => await verifyOtp(formData) // mutationFn: async (formData: FormData) => await verifyOtp(formData)
}); // });
const signOutMutation = useMutation({ // const signOutMutation = useMutation({
mutationKey: ["signOut"], // mutationKey: ["signOut"],
mutationFn: async () => await signOut() // mutationFn: async () => await signOut()
}); // });
const sendMagicLinkMutation = useMutation({ // const sendMagicLinkMutation = useMutation({
mutationKey: ["sendMagicLink"], // mutationKey: ["sendMagicLink"],
mutationFn: async (email: string) => await sendMagicLink(email) // mutationFn: async (email: string) => await sendMagicLink(email)
}); // });
const sendPasswordRecoveryMutation = useMutation({ // const sendPasswordRecoveryMutation = useMutation({
mutationKey: ["sendPasswordRecovery"], // mutationKey: ["sendPasswordRecovery"],
mutationFn: async (email: string) => await sendPasswordRecovery(email) // mutationFn: async (email: string) => await sendPasswordRecovery(email)
}); // });
return { // return {
signIn: signInMutation, // signIn: signInMutation,
verifyOtp: verifyOtpMutation, // verifyOtp: verifyOtpMutation,
signOut: signOutMutation, // signOut: signOutMutation,
sendMagicLink: sendMagicLinkMutation, // sendMagicLink: sendMagicLinkMutation,
sendPasswordRecovery: sendPasswordRecoveryMutation // sendPasswordRecovery: sendPasswordRecoveryMutation
}; // };
} // }
export const useSignInMutation = () => { // export const useSignInMutation = () => {
const { signIn } = useAuthActions(); // const { signIn } = useAuthActions();
return { // return {
signIn: signIn.mutateAsync, // signIn: signIn.mutateAsync,
isPending: signIn.isPending, // isPending: signIn.isPending,
error: signIn.error, // error: signIn.error,
}; // };
} // }
export const useVerifyOtpMutation = () => { // export const useVerifyOtpMutation = () => {
const { verifyOtp } = useAuthActions(); // const { verifyOtp } = useAuthActions();
return { // return {
verifyOtp: verifyOtp.mutateAsync, // verifyOtp: verifyOtp.mutateAsync,
isPending: verifyOtp.isPending, // isPending: verifyOtp.isPending,
error: verifyOtp.error, // error: verifyOtp.error,
} // }
} // }
export const useSignOutMutation = () => { // export const useSignOutMutation = () => {
const { signOut } = useAuthActions(); // const { signOut } = useAuthActions();
return { // return {
signOut: signOut.mutateAsync, // signOut: signOut.mutateAsync,
isPending: signOut.isPending // isPending: signOut.isPending
} // }
} // }
export const useSendMagicLinkMutation = () => { // export const useSendMagicLinkMutation = () => {
const { sendMagicLink } = useAuthActions(); // const { sendMagicLink } = useAuthActions();
return { // return {
sendMagicLink: sendMagicLink.mutateAsync, // sendMagicLink: sendMagicLink.mutateAsync,
isPending: sendMagicLink.isPending, // isPending: sendMagicLink.isPending,
error: sendMagicLink.error, // error: sendMagicLink.error,
} // }
} // }
export const useSendPasswordRecoveryMutation = () => { // export const useSendPasswordRecoveryMutation = () => {
const { sendPasswordRecovery } = useAuthActions(); // const { sendPasswordRecovery } = useAuthActions();
return { // return {
sendPasswordRecovery: sendPasswordRecovery.mutateAsync, // sendPasswordRecovery: sendPasswordRecovery.mutateAsync,
isPending: sendPasswordRecovery.isPending, // isPending: sendPasswordRecovery.isPending,
error: sendPasswordRecovery.error, // error: sendPasswordRecovery.error,
} // }
} // }

View File

@ -1,6 +1,7 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { DateFormatOptions, DateFormatPattern } from "../_lib/types/date-format.interface"; 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. * Redirects to a specified path with an encoded message as a query parameter.
@ -117,4 +118,33 @@ export const formatDate = (
return locale return locale
? format(dateObj, formatPattern, { locale }) ? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern); : 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);
});
}; };