Refactor CRUD Users
This commit is contained in:
parent
179ddf6999
commit
e84a6f52c0
|
@ -39,122 +39,134 @@ 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 { formatDate } from "@/app/_utils/common";
|
||||||
|
|
||||||
interface UserDetailSheetProps {
|
interface UserDetailSheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
user: IUserSchema;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
user: any;
|
onUserUpdated: () => void;
|
||||||
onUserUpdate: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserDetailSheet({
|
export function UserDetailSheet({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
user,
|
user,
|
||||||
onUserUpdate,
|
onUserUpdated,
|
||||||
}: UserDetailSheetProps) {
|
}: UserDetailSheetProps) {
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
// const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState({
|
// const [isLoading, setIsLoading] = useState({
|
||||||
deleteUser: false,
|
// deleteUser: false,
|
||||||
sendPasswordRecovery: false,
|
// sendPasswordRecovery: false,
|
||||||
sendMagicLink: false,
|
// sendMagicLink: false,
|
||||||
toggleBan: false,
|
// toggleBan: false,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const deleteUserMutation = useMutation({
|
// const deleteUserMutation = useMutation({
|
||||||
mutationFn: () => deleteUser(user.id),
|
// mutationFn: () => deleteUser(user.id),
|
||||||
onMutate: () => {
|
// onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
// setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
||||||
setIsDeleting(true);
|
// setIsDeleting(true);
|
||||||
},
|
// },
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
toast.success("User deleted successfully");
|
// toast.success("User deleted successfully");
|
||||||
onUserUpdate();
|
// onUserUpdate();
|
||||||
onOpenChange(false);
|
// onOpenChange(false);
|
||||||
},
|
// },
|
||||||
onError: () => {
|
// onError: () => {
|
||||||
toast.error("Failed to delete user");
|
// toast.error("Failed to delete user");
|
||||||
},
|
// },
|
||||||
onSettled: () => {
|
// onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
|
// setIsLoading((prev) => ({ ...prev, deleteUser: false }));
|
||||||
setIsDeleting(false);
|
// setIsDeleting(false);
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const sendPasswordRecoveryMutation = useMutation({
|
// const sendPasswordRecoveryMutation = useMutation({
|
||||||
mutationFn: () => {
|
// mutationFn: () => {
|
||||||
if (!user.email) {
|
// if (!user.email) {
|
||||||
throw new Error("User does not have an email address");
|
// throw new Error("User does not have an email address");
|
||||||
}
|
// }
|
||||||
return sendPasswordRecovery(user.email);
|
// return sendPasswordRecovery(user.email);
|
||||||
},
|
// },
|
||||||
onMutate: () => {
|
// onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
// setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
||||||
},
|
// },
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
toast.success("Password recovery email sent");
|
// toast.success("Password recovery email sent");
|
||||||
},
|
// },
|
||||||
onError: () => {
|
// onError: () => {
|
||||||
toast.error("Failed to send password recovery email");
|
// toast.error("Failed to send password recovery email");
|
||||||
},
|
// },
|
||||||
onSettled: () => {
|
// onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
|
// setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const sendMagicLinkMutation = useMutation({
|
// const sendMagicLinkMutation = useMutation({
|
||||||
mutationFn: () => {
|
// mutationFn: () => {
|
||||||
if (!user.email) {
|
// if (!user.email) {
|
||||||
throw new Error("User does not have an email address");
|
// throw new Error("User does not have an email address");
|
||||||
}
|
// }
|
||||||
return sendMagicLink(user.email);
|
// return sendMagicLink(user.email);
|
||||||
},
|
// },
|
||||||
onMutate: () => {
|
// onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
// setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
||||||
},
|
// },
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
toast.success("Magic link sent successfully");
|
// toast.success("Magic link sent successfully");
|
||||||
},
|
// },
|
||||||
onError: () => {
|
// onError: () => {
|
||||||
toast.error("Failed to send magic link");
|
// toast.error("Failed to send magic link");
|
||||||
},
|
// },
|
||||||
onSettled: () => {
|
// onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
|
// setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const toggleBanMutation = useMutation({
|
// const toggleBanMutation = useMutation({
|
||||||
mutationFn: () => {
|
// mutationFn: () => {
|
||||||
if (user.banned_until) {
|
// if (user.banned_until) {
|
||||||
return unbanUser(user.id);
|
// return unbanUser(user.id);
|
||||||
} else {
|
// } else {
|
||||||
const ban_duration = "7h"; // Example: Ban duration set to 7 days
|
// const ban_duration = "7h"; // Example: Ban duration set to 7 days
|
||||||
return banUser(user.id, ban_duration);
|
// return banUser({ id: user.id, ban_duration });
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
onMutate: () => {
|
// onMutate: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
// setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
||||||
},
|
// },
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
toast.success("User ban status updated");
|
// toast.success("User ban status updated");
|
||||||
onUserUpdate();
|
// onUserUpdate();
|
||||||
},
|
// },
|
||||||
onError: () => {
|
// onError: () => {
|
||||||
toast.error("Failed to update user ban status");
|
// toast.error("Failed to update user ban status");
|
||||||
},
|
// },
|
||||||
onSettled: () => {
|
// onSettled: () => {
|
||||||
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
|
// setIsLoading((prev) => ({ ...prev, toggleBan: false }));
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const handleCopyItem = (item: string) => {
|
// const handleCopyItem = (item: string) => {
|
||||||
navigator.clipboard.writeText(item);
|
// navigator.clipboard.writeText(item);
|
||||||
toast.success("Copied to clipboard");
|
// toast.success("Copied to clipboard");
|
||||||
};
|
// };
|
||||||
|
|
||||||
const formatDate = (date: string | undefined | null) => {
|
const {
|
||||||
return date ? format(new Date(date), "PPpp") : "-";
|
handleDeleteUser,
|
||||||
};
|
handleSendPasswordRecovery,
|
||||||
|
handleSendMagicLink,
|
||||||
|
handleCopyItem,
|
||||||
|
handleToggleBan,
|
||||||
|
isBanPending,
|
||||||
|
isUnbanPending,
|
||||||
|
isDeletePending,
|
||||||
|
isSendPasswordRecoveryPending,
|
||||||
|
isSendMagicLinkPending,
|
||||||
|
} = useUserDetailSheetHandlers({ open, user, onUserUpdated, onOpenChange });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
@ -166,7 +178,7 @@ export function UserDetailSheet({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
onClick={() => handleCopyItem(user.email)}
|
onClick={() => handleCopyItem(user.email ?? "")}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -203,15 +215,13 @@ export function UserDetailSheet({
|
||||||
|
|
||||||
<div className="flex justify-between items-center py-1">
|
<div className="flex justify-between items-center py-1">
|
||||||
<span className="text-muted-foreground">Created at</span>
|
<span className="text-muted-foreground">Created at</span>
|
||||||
<span>{new Date(user.created_at).toLocaleString()}</span>
|
<span>{formatDate(user.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center py-1">
|
<div className="flex justify-between items-center py-1">
|
||||||
<span className="text-muted-foreground">Updated at</span>
|
<span className="text-muted-foreground">Updated at</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(
|
{formatDate(user.updated_at)}
|
||||||
user.updated_at || user.created_at
|
|
||||||
).toLocaleString()}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -224,7 +234,7 @@ export function UserDetailSheet({
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
Confirmation sent at
|
Confirmation sent at
|
||||||
</span>
|
</span>
|
||||||
<span>{formatDate(user.email_confirmation_sent_at)}</span>
|
<span>{formatDate(user.email_confirmed_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center py-1">
|
<div className="flex justify-between items-center py-1">
|
||||||
|
@ -284,10 +294,10 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => sendPasswordRecoveryMutation.mutate()}
|
onClick={handleSendPasswordRecovery}
|
||||||
disabled={isLoading.sendPasswordRecovery || !user.email}
|
disabled={isSendPasswordRecoveryPending}
|
||||||
>
|
>
|
||||||
{isLoading.sendPasswordRecovery ? (
|
{isSendPasswordRecoveryPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
Sending...
|
Sending...
|
||||||
|
@ -313,10 +323,10 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => sendMagicLinkMutation.mutate()}
|
onClick={handleSendMagicLink}
|
||||||
disabled={isLoading.sendMagicLink || !user.email}
|
disabled={isSendMagicLinkPending}
|
||||||
>
|
>
|
||||||
{isLoading.sendMagicLink ? (
|
{isSendMagicLinkPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
Sending...
|
Sending...
|
||||||
|
@ -354,10 +364,10 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant={user.banned_until ? "outline" : "outline"}
|
variant={user.banned_until ? "outline" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggleBanMutation.mutate()}
|
onClick={handleToggleBan}
|
||||||
disabled={isLoading.toggleBan}
|
disabled={isBanPending || isUnbanPending}
|
||||||
>
|
>
|
||||||
{isLoading.toggleBan ? (
|
{isBanPending || isUnbanPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
{user.banned_until ? "Unbanning..." : "Banning..."}
|
{user.banned_until ? "Unbanning..." : "Banning..."}
|
||||||
|
@ -383,9 +393,9 @@ export function UserDetailSheet({
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={isDeleting}
|
disabled={isDeletePending}
|
||||||
>
|
>
|
||||||
{isDeleting ? (
|
{isDeletePending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
Deleting...
|
Deleting...
|
||||||
|
@ -412,11 +422,11 @@ export function UserDetailSheet({
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => deleteUserMutation.mutate()}
|
onClick={handleDeleteUser}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
disabled={isDeleting}
|
disabled={isDeletePending}
|
||||||
>
|
>
|
||||||
{isDeleting ? (
|
{isDeletePending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
Deleting...
|
Deleting...
|
||||||
|
|
|
@ -18,78 +18,24 @@ 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"
|
||||||
|
|
||||||
type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
|
type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
|
||||||
|
|
||||||
interface UserProfileSheetProps {
|
interface UserProfileSheetProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
userData: IUserSchema
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
userData?: IUserSchema
|
|
||||||
onUserUpdated: () => void
|
onUserUpdated: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
|
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
|
||||||
// Initialize form with user data
|
|
||||||
const form = useForm<UserProfileFormValues>({
|
|
||||||
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 { mutate: updateUserMutation, isPending } = useMutation({
|
const {
|
||||||
mutationKey: ["updateUser"],
|
form,
|
||||||
mutationFn: (data: UserProfileFormValues) => {
|
handleUpdateUser,
|
||||||
if (!userData?.id) {
|
isUpdatePending,
|
||||||
throw new Error("User ID is required")
|
} = useUserProfileSheetHandlers({ open, userData, onOpenChange, onUserUpdated })
|
||||||
}
|
|
||||||
return updateUser(userData.id, data)
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast("Failed to update user")
|
|
||||||
onOpenChange(false)
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast("User updated")
|
|
||||||
onUserUpdated()
|
|
||||||
onOpenChange(false)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onSubmit(data: UserProfileFormValues) {
|
|
||||||
try {
|
|
||||||
await updateUserMutation(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving user profile:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
@ -100,7 +46,7 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(handleUpdateUser)} className="space-y-6">
|
||||||
{/* User Information Section */}
|
{/* User Information Section */}
|
||||||
<FormSection
|
<FormSection
|
||||||
title="User Information"
|
title="User Information"
|
||||||
|
@ -113,6 +59,7 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
|
||||||
control={form.control}
|
control={form.control}
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
rows={4}
|
rows={4}
|
||||||
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
<FormFieldWrapper
|
<FormFieldWrapper
|
||||||
name="phone"
|
name="phone"
|
||||||
|
@ -120,6 +67,8 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
|
||||||
type="tel"
|
type="tel"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
placeholder="+1234567890"
|
placeholder="+1234567890"
|
||||||
|
disabled={true}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<FormFieldWrapper
|
<FormFieldWrapper
|
||||||
name="role"
|
name="role"
|
||||||
|
@ -265,14 +214,14 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => !isPending && onOpenChange(false)}
|
onClick={() => !isUpdatePending && onOpenChange(false)}
|
||||||
disabled={isPending}
|
disabled={isUpdatePending}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="xs" type="submit" disabled={isPending}>
|
<Button size="xs" type="submit" disabled={isUpdatePending}>
|
||||||
{isPending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
|
{isUpdatePending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
|
||||||
{isPending ? "Saving..." : "Save"}
|
{isUpdatePending ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -143,7 +143,7 @@ export default function UserManagement() {
|
||||||
user={detailUser}
|
user={detailUser}
|
||||||
open={isSheetOpen}
|
open={isSheetOpen}
|
||||||
onOpenChange={setIsSheetOpen}
|
onOpenChange={setIsSheetOpen}
|
||||||
onUserUpdate={() => { }}
|
onUserUpdated={() => { }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddUserDialog
|
<AddUserDialog
|
||||||
|
|
|
@ -16,6 +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"
|
||||||
|
|
||||||
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
|
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
|
||||||
|
|
||||||
|
@ -24,6 +25,13 @@ export const createUserColumns = (
|
||||||
setFilters: (filters: IUserFilterOptionsSchema) => void,
|
setFilters: (filters: IUserFilterOptionsSchema) => void,
|
||||||
handleUserUpdate: (user: IUserSchema) => void,
|
handleUserUpdate: (user: IUserSchema) => void,
|
||||||
): UserTableColumn[] => {
|
): UserTableColumn[] => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
deleteUser,
|
||||||
|
banUser,
|
||||||
|
unbanUser,
|
||||||
|
} = useUsersHandlers();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "email",
|
id: "email",
|
||||||
|
@ -313,16 +321,18 @@ export const createUserColumns = (
|
||||||
Update
|
Update
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => deleteUser(row.original.id)}
|
||||||
/* handle delete */
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2 text-destructive" />
|
<Trash2 className="h-4 w-4 mr-2 text-destructive" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
/* handle ban */
|
if (row.original.banned_until != null) {
|
||||||
|
unbanUser(row.original.id)
|
||||||
|
} else {
|
||||||
|
banUser(row.original.id)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||||
|
|
|
@ -14,19 +14,21 @@ import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/e
|
||||||
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 } from '@/src/entities/models/users/read-user.model';
|
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema } from '@/src/entities/models/users/read-user.model';
|
||||||
|
import { ICredentialsUnbanUserSchema } from '@/src/entities/models/users/unban-user.model';
|
||||||
|
|
||||||
export async function banUser(id: string, ban_duration: IBanDuration) {
|
export async function banUser(credential: ICredentialsBanUserSchema, data: IBanUserSchema) {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'banUser',
|
'unbanUser',
|
||||||
{ recordResponse: true },
|
{ recordResponse: true },
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const banUserController = getInjection('IBanUserController');
|
const banUserController = getInjection('IBanUserController');
|
||||||
await banUserController({ id }, { ban_duration });
|
await banUserController({ id: credential.id }, { ban_duration: data.ban_duration });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof InputParseError) {
|
if (err instanceof InputParseError) {
|
||||||
// return {
|
// return {
|
||||||
|
@ -40,7 +42,7 @@ export async function banUser(id: string, ban_duration: IBanDuration) {
|
||||||
// return {
|
// return {
|
||||||
// error: 'Must be logged in to create a user.',
|
// error: 'Must be logged in to create a user.',
|
||||||
// };
|
// };
|
||||||
throw new UnauthenticatedError('Must be logged in to ban a user.');
|
throw new UnauthenticatedError('Must be logged in to unban a user.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof AuthenticationError) {
|
if (err instanceof AuthenticationError) {
|
||||||
|
@ -63,7 +65,8 @@ export async function banUser(id: string, ban_duration: IBanDuration) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unbanUser(id: string) {
|
|
||||||
|
export async function unbanUser(credential: ICredentialsUnbanUserSchema) {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'unbanUser',
|
'unbanUser',
|
||||||
|
@ -71,7 +74,7 @@ export async function unbanUser(id: string) {
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const unbanUserController = getInjection('IUnbanUserController');
|
const unbanUserController = getInjection('IUnbanUserController');
|
||||||
await unbanUserController({ id });
|
await unbanUserController(credential);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -134,7 +137,7 @@ export async function getCurrentUser() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserById(id: string) {
|
export async function getUserById(credential: ICredentialGetUserByIdSchema) {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'getUserById',
|
'getUserById',
|
||||||
|
@ -142,7 +145,7 @@ export async function getUserById(id: string) {
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const getUserByIdController = getInjection('IGetUserByIdController');
|
const getUserByIdController = getInjection('IGetUserByIdController');
|
||||||
return await getUserByIdController({ id });
|
return await getUserByIdController(credential);
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -230,7 +233,7 @@ export async function getUserByEmail(credential: ICredentialGetUserByEmailSchema
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserByUsername(username: string) {
|
export async function getUserByUsername(credential: ICredentialGetUserByUsernameSchema) {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'getUserByUsername',
|
'getUserByUsername',
|
||||||
|
@ -240,7 +243,7 @@ export async function getUserByUsername(username: string) {
|
||||||
const getUserByUsernameController = getInjection(
|
const getUserByUsernameController = getInjection(
|
||||||
'IGetUserByUsernameController'
|
'IGetUserByUsernameController'
|
||||||
);
|
);
|
||||||
return await getUserByUsernameController({ username });
|
return await getUserByUsernameController(credential);
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useCreateUserMutation, useInviteUserMutation } from './queries';
|
import { useBanUserMutation, useCreateUserMutation, useDeleteUserMutation, useInviteUserMutation, useUnbanUserMutation, useUpdateUserMutation } from './queries';
|
||||||
import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model';
|
import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { set } from 'date-fns';
|
import { set } from 'date-fns';
|
||||||
|
@ -8,6 +8,226 @@ import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model';
|
import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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 }: {
|
export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: {
|
||||||
onUserAdded: () => void;
|
onUserAdded: () => void;
|
||||||
|
@ -139,6 +359,150 @@ export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<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(userData.id, form.getValues(), {
|
||||||
|
onSuccess: () => {
|
||||||
|
onUserUpdated();
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpdateUser,
|
||||||
|
form,
|
||||||
|
isUpdatePending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useUserManagementHandlers = (refetch: () => void) => {
|
export const useUserManagementHandlers = (refetch: () => void) => {
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [detailUser, setDetailUser] = useState<IUserSchema | null>(null)
|
const [detailUser, setDetailUser] = useState<IUserSchema | null>(null)
|
||||||
|
|
|
@ -13,11 +13,12 @@ import {
|
||||||
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 { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||||
import { 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";
|
||||||
|
|
||||||
const useUsersAction = () => {
|
const useUsersAction = () => {
|
||||||
|
|
||||||
|
@ -28,35 +29,35 @@ const useUsersAction = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Current user query doesn't need parameters
|
// Current user query doesn't need parameters
|
||||||
const getCurrentUserQuery = useQuery({
|
const getCurrentUserQuery = useQuery<IUserSchema>({
|
||||||
queryKey: ["user", "current"],
|
queryKey: ["user", "current"],
|
||||||
queryFn: async () => await getCurrentUser()
|
queryFn: async () => await getCurrentUser()
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserByIdQuery = (id: string) => ({
|
const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery<IUserSchema>({
|
||||||
queryKey: ["user", id],
|
queryKey: ["user", "id", credential.id],
|
||||||
queryFn: async () => await getUserById(id)
|
queryFn: async () => await getUserById(credential)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserByEmailQuery = (email: string) => ({
|
const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery<IUserSchema>({
|
||||||
queryKey: ["user", "email", email],
|
queryKey: ["user", "email", credential.email],
|
||||||
queryFn: async () => await getUserByEmail({ email })
|
queryFn: async () => await getUserByEmail(credential)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserByUsernameQuery = (username: string) => ({
|
const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery<IUserSchema>({
|
||||||
queryKey: ["user", "username", username],
|
queryKey: ["user", "username", credential.username],
|
||||||
queryFn: async () => await getUserByUsername(username)
|
queryFn: async () => await getUserByUsername(credential)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mutations that don't need dynamic parameters
|
// Mutations that don't need dynamic parameters
|
||||||
const banUserMutation = useMutation({
|
const banUserMutation = useMutation({
|
||||||
mutationKey: ["banUser"],
|
mutationKey: ["banUser"],
|
||||||
mutationFn: async ({ credential, params }: { credential: ICredentialsBanUserSchema; params: IBanUserSchema }) => await banUser(credential.id, params.ban_duration)
|
mutationFn: async ({ credential, data }: { credential: ICredentialsBanUserSchema; data: IBanUserSchema }) => await banUser(credential, data)
|
||||||
});
|
});
|
||||||
|
|
||||||
const unbanUserMutation = useMutation({
|
const unbanUserMutation = useMutation({
|
||||||
mutationKey: ["unbanUser"],
|
mutationKey: ["unbanUser"],
|
||||||
mutationFn: async (params: IUnbanUserSchema) => await unbanUser(params.id)
|
mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create functions that return configured hooks
|
// Create functions that return configured hooks
|
||||||
|
@ -117,6 +118,39 @@ export const useGetCurrentUserQuery = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useCreateUserMutation = () => {
|
export const useCreateUserMutation = () => {
|
||||||
const { createUser } = useUsersAction();
|
const { createUser } = useUsersAction();
|
||||||
|
|
||||||
|
@ -136,3 +170,43 @@ export const useInviteUserMutation = () => {
|
||||||
errors: inviteUser.error,
|
errors: inviteUser.error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUpdateUserMutation = () => {
|
||||||
|
const { updateUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateUser: updateUser.mutateAsync,
|
||||||
|
isPending: updateUser.isPending,
|
||||||
|
errors: updateUser.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBanUserMutation = () => {
|
||||||
|
const { banUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
banUser: banUser.mutateAsync,
|
||||||
|
isPending: banUser.isPending,
|
||||||
|
errors: banUser.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUnbanUserMutation = () => {
|
||||||
|
const { unbanUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
unbanUser: unbanUser.mutateAsync,
|
||||||
|
isPending: unbanUser.isPending,
|
||||||
|
errors: unbanUser.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteUserMutation = () => {
|
||||||
|
const { deleteUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteUser: deleteUser.mutateAsync,
|
||||||
|
isPending: deleteUser.isPending,
|
||||||
|
errors: deleteUser.error,
|
||||||
|
}
|
||||||
|
}
|
|
@ -134,13 +134,12 @@ export async function verifyOtp(formData: FormData) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendMagicLink(formData: FormData) {
|
export async function sendMagicLink(email: string) {
|
||||||
const instrumentationService = getInjection("IInstrumentationService")
|
const instrumentationService = getInjection("IInstrumentationService")
|
||||||
return await instrumentationService.instrumentServerAction("sendMagicLink", {
|
return await instrumentationService.instrumentServerAction("sendMagicLink", {
|
||||||
recordResponse: true
|
recordResponse: true
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const email = formData.get("email")?.toString()
|
|
||||||
|
|
||||||
const sendMagicLinkController = getInjection("ISendMagicLinkController")
|
const sendMagicLinkController = getInjection("ISendMagicLinkController")
|
||||||
await sendMagicLinkController({ email })
|
await sendMagicLinkController({ email })
|
||||||
|
@ -161,13 +160,12 @@ export async function sendMagicLink(formData: FormData) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPasswordRecovery(formData: FormData) {
|
export async function sendPasswordRecovery(email: string) {
|
||||||
const instrumentationService = getInjection("IInstrumentationService")
|
const instrumentationService = getInjection("IInstrumentationService")
|
||||||
return await instrumentationService.instrumentServerAction("sendPasswordRecovery", {
|
return await instrumentationService.instrumentServerAction("sendPasswordRecovery", {
|
||||||
recordResponse: true
|
recordResponse: true
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const email = formData.get("email")?.toString()
|
|
||||||
|
|
||||||
const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController")
|
const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController")
|
||||||
await sendPasswordRecoveryController({ email })
|
await sendPasswordRecoveryController({ email })
|
||||||
|
|
|
@ -21,12 +21,12 @@ export function useAuthActions() {
|
||||||
|
|
||||||
const sendMagicLinkMutation = useMutation({
|
const sendMagicLinkMutation = useMutation({
|
||||||
mutationKey: ["sendMagicLink"],
|
mutationKey: ["sendMagicLink"],
|
||||||
mutationFn: async (formData: FormData) => await sendMagicLink(formData)
|
mutationFn: async (email: string) => await sendMagicLink(email)
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendPasswordRecoveryMutation = useMutation({
|
const sendPasswordRecoveryMutation = useMutation({
|
||||||
mutationKey: ["sendPasswordRecovery"],
|
mutationKey: ["sendPasswordRecovery"],
|
||||||
mutationFn: async (formData: FormData) => await sendPasswordRecovery(formData)
|
mutationFn: async (email: string) => await sendPasswordRecovery(email)
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -37,3 +37,53 @@ export function useAuthActions() {
|
||||||
sendPasswordRecovery: sendPasswordRecoveryMutation
|
sendPasswordRecovery: sendPasswordRecoveryMutation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useSignInMutation = () => {
|
||||||
|
const { signIn } = useAuthActions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
signIn: signIn.mutateAsync,
|
||||||
|
isPending: signIn.isPending,
|
||||||
|
error: signIn.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVerifyOtpMutation = () => {
|
||||||
|
const { verifyOtp } = useAuthActions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
verifyOtp: verifyOtp.mutateAsync,
|
||||||
|
isPending: verifyOtp.isPending,
|
||||||
|
error: verifyOtp.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSignOutMutation = () => {
|
||||||
|
const { signOut } = useAuthActions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
signOut: signOut.mutateAsync,
|
||||||
|
isPending: signOut.isPending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSendMagicLinkMutation = () => {
|
||||||
|
const { sendMagicLink } = useAuthActions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendMagicLink: sendMagicLink.mutateAsync,
|
||||||
|
isPending: sendMagicLink.isPending,
|
||||||
|
error: sendMagicLink.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSendPasswordRecoveryMutation = () => {
|
||||||
|
const { sendPasswordRecovery } = useAuthActions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendPasswordRecovery: sendPasswordRecovery.mutateAsync,
|
||||||
|
isPending: sendPasswordRecovery.isPending,
|
||||||
|
error: sendPasswordRecovery.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default function RootLayout({
|
||||||
</nav> */}
|
</nav> */}
|
||||||
<div className="flex flex-col max-w-full p-0">
|
<div className="flex flex-col max-w-full p-0">
|
||||||
{children}
|
{children}
|
||||||
<Toaster theme="system" richColors position="top-right" />
|
<Toaster theme="system" position="top-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto">
|
{/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto">
|
||||||
|
|
|
@ -31,6 +31,7 @@ interface FormFieldProps {
|
||||||
booleanType?: "switch" | "checkbox" | "select"
|
booleanType?: "switch" | "checkbox" | "select"
|
||||||
fromYear?: number
|
fromYear?: number
|
||||||
toYear?: number
|
toYear?: number
|
||||||
|
disabled?: boolean // Ganti readonly menjadi disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormFieldWrapper({
|
export function FormFieldWrapper({
|
||||||
|
@ -47,6 +48,7 @@ export function FormFieldWrapper({
|
||||||
booleanType = "switch",
|
booleanType = "switch",
|
||||||
fromYear = 1900,
|
fromYear = 1900,
|
||||||
toYear = new Date().getFullYear(),
|
toYear = new Date().getFullYear(),
|
||||||
|
disabled = false, // Default disabled adalah false
|
||||||
}: FormFieldProps) {
|
}: FormFieldProps) {
|
||||||
// Default boolean options for select
|
// Default boolean options for select
|
||||||
const booleanOptions = [
|
const booleanOptions = [
|
||||||
|
@ -60,11 +62,12 @@ export function FormFieldWrapper({
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem
|
<FormItem
|
||||||
className={
|
className={cn(
|
||||||
isBoolean && booleanType === "switch"
|
isBoolean && booleanType === "switch"
|
||||||
? "flex items-start justify-between rounded-lg border p-4"
|
? "flex items-start justify-between rounded-lg border p-4"
|
||||||
: "flex justify-between items-start gap-4"
|
: "flex justify-between items-start gap-4",
|
||||||
}
|
disabled && "opacity-50 cursor-not-allowed" // Tambahkan gaya khusus untuk disabled
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className={isBoolean && booleanType === "switch" ? "space-y-1" : "w-1/3"}>
|
<div className={isBoolean && booleanType === "switch" ? "space-y-1" : "w-1/3"}>
|
||||||
<FormLabel className={isBoolean && booleanType === "switch" ? "text-base" : ""}>{label}</FormLabel>
|
<FormLabel className={isBoolean && booleanType === "switch" ? "text-base" : ""}>{label}</FormLabel>
|
||||||
|
@ -75,13 +78,14 @@ export function FormFieldWrapper({
|
||||||
<FormControl>
|
<FormControl>
|
||||||
{isBoolean ? (
|
{isBoolean ? (
|
||||||
booleanType === "switch" ? (
|
booleanType === "switch" ? (
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch checked={field.value} onCheckedChange={field.onChange} disabled={disabled} />
|
||||||
) : booleanType === "checkbox" ? (
|
) : booleanType === "checkbox" ? (
|
||||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
<Checkbox checked={field.value} onCheckedChange={field.onChange} disabled={disabled} />
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => field.onChange(value === "true")}
|
onValueChange={(value) => field.onChange(value === "true")}
|
||||||
defaultValue={String(field.value)}
|
defaultValue={String(field.value)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
|
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
|
||||||
|
@ -96,7 +100,7 @@ export function FormFieldWrapper({
|
||||||
</Select>
|
</Select>
|
||||||
)
|
)
|
||||||
) : type.includes("select") && options ? (
|
) : type.includes("select") && options ? (
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={disabled}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
|
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
@ -113,12 +117,17 @@ export function FormFieldWrapper({
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
className={cn(
|
||||||
|
"w-full pl-3 text-left font-normal",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{field.value ? format(field.value, "MM/dd/yyyy hh:mm:ss a") : <span>Pick a date and time</span>}
|
{field.value ? format(field.value, "MM/dd/yyyy hh:mm:ss a") : <span>Pick a date and time</span>}
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
{!disabled && (
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
selected={field.value instanceof Date ? field.value : undefined}
|
selected={field.value instanceof Date ? field.value : undefined}
|
||||||
|
@ -128,8 +137,8 @@ export function FormFieldWrapper({
|
||||||
toYear={toYear}
|
toYear={toYear}
|
||||||
showTimePicker
|
showTimePicker
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
) : rows > 1 ? (
|
) : rows > 1 ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
@ -138,6 +147,7 @@ export function FormFieldWrapper({
|
||||||
rows={rows}
|
rows={rows}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
@ -145,6 +155,7 @@ export function FormFieldWrapper({
|
||||||
type={type.includes("URL") ? "url" : type === "string" ? "text" : type}
|
type={type.includes("URL") ? "url" : type === "string" ? "text" : type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -155,4 +166,3 @@ export function FormFieldWrapper({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Locale } from "date-fns";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available date format patterns for the formatDate function.
|
||||||
|
* See date-fns format documentation for more details on each pattern.
|
||||||
|
*/
|
||||||
|
export type DateFormatPattern =
|
||||||
|
// Year formats
|
||||||
|
| "yyyy" // 2025
|
||||||
|
| "yy" // 25
|
||||||
|
| "y" // 2025
|
||||||
|
|
||||||
|
// Month formats
|
||||||
|
| "MMMM" // March
|
||||||
|
| "MMM" // Mar
|
||||||
|
| "MM" // 03
|
||||||
|
| "M" // 3
|
||||||
|
|
||||||
|
// Day formats
|
||||||
|
| "dd" // 01-31
|
||||||
|
| "d" // 1-31
|
||||||
|
| "Do" // 1st, 2nd, etc.
|
||||||
|
| "EEEE" // Monday, Tuesday, etc.
|
||||||
|
| "EEE" // Mon, Tue, etc.
|
||||||
|
| "EE" // Mo, Tu, etc.
|
||||||
|
| "E" // M, T, etc.
|
||||||
|
|
||||||
|
// Hour formats
|
||||||
|
| "HH" // 00-23
|
||||||
|
| "H" // 0-23
|
||||||
|
| "hh" // 01-12
|
||||||
|
| "h" // 1-12
|
||||||
|
|
||||||
|
// Minute and second formats
|
||||||
|
| "mm" // 00-59
|
||||||
|
| "m" // 0-59
|
||||||
|
| "ss" // 00-59
|
||||||
|
| "s" // 0-59
|
||||||
|
|
||||||
|
// AM/PM
|
||||||
|
| "a" // am/pm
|
||||||
|
| "aa" // AM/PM
|
||||||
|
|
||||||
|
// Combined common formats
|
||||||
|
| "PPpp" // Mar 23, 2025, 12:00 AM
|
||||||
|
| "Pp" // 03/23/2025, 12:00 AM
|
||||||
|
| "PP" // Mar 23, 2025
|
||||||
|
| "p" // 12:00 AM
|
||||||
|
|
||||||
|
// Custom combined patterns (allow any string)
|
||||||
|
| string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom formatting options for dates.
|
||||||
|
*/
|
||||||
|
export interface DateFormatOptions {
|
||||||
|
/**
|
||||||
|
* The format pattern to use when formatting the date.
|
||||||
|
* @default "PPpp"
|
||||||
|
*/
|
||||||
|
format?: DateFormatPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to display when the date is null or undefined.
|
||||||
|
* @default "-"
|
||||||
|
*/
|
||||||
|
fallback?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The locale to use for formatting.
|
||||||
|
* @default undefined (uses system locale)
|
||||||
|
*/
|
||||||
|
locale?: Locale;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ServerActionErrorParams {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
field?: string;
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { format } from "date-fns";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { DateFormatOptions, DateFormatPattern } from "../_lib/types/date-format.interface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
@ -71,3 +73,48 @@ export function generateUsername(email: string): string {
|
||||||
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
|
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
|
||||||
return `${localPart}.${randomSuffix}`;
|
return `${localPart}.${randomSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date string to a human-readable format with type safety.
|
||||||
|
* @param date - The date string to format.
|
||||||
|
* @param options - Formatting options or a format string.
|
||||||
|
* @returns The formatted date string.
|
||||||
|
* @example
|
||||||
|
* // Using default format
|
||||||
|
* formatDate("2025-03-23")
|
||||||
|
*
|
||||||
|
* // Using a custom format string
|
||||||
|
* formatDate("2025-03-23", "yyyy-MM-dd")
|
||||||
|
*
|
||||||
|
* // Using formatting options
|
||||||
|
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
|
||||||
|
*/
|
||||||
|
export const formatDate = (
|
||||||
|
date: string | Date | undefined | null,
|
||||||
|
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
|
||||||
|
): string => {
|
||||||
|
if (!date) {
|
||||||
|
return typeof options === "string"
|
||||||
|
? "-"
|
||||||
|
: (options.fallback || "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
|
|
||||||
|
// Handle invalid dates
|
||||||
|
if (isNaN(dateObj.getTime())) {
|
||||||
|
return typeof options === "string"
|
||||||
|
? "-"
|
||||||
|
: (options.fallback || "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options === "string") {
|
||||||
|
return format(dateObj, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { format: formatPattern = "PPpp", locale } = options;
|
||||||
|
|
||||||
|
return locale
|
||||||
|
? format(dateObj, formatPattern, { locale })
|
||||||
|
: format(dateObj, formatPattern);
|
||||||
|
};
|
|
@ -51,14 +51,14 @@ export function createUsersModule() {
|
||||||
.bind(DI_SYMBOLS.IBanUserUseCase)
|
.bind(DI_SYMBOLS.IBanUserUseCase)
|
||||||
.toHigherOrderFunction(banUserUseCase, [
|
.toHigherOrderFunction(banUserUseCase, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IUsersRepository
|
DI_SYMBOLS.IUsersRepository,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
.bind(DI_SYMBOLS.IUnbanUserUseCase)
|
.bind(DI_SYMBOLS.IUnbanUserUseCase)
|
||||||
.toHigherOrderFunction(unbanUserUseCase, [
|
.toHigherOrderFunction(unbanUserUseCase, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IUsersRepository
|
DI_SYMBOLS.IUsersRepository,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
|
@ -129,14 +129,16 @@ export function createUsersModule() {
|
||||||
.bind(DI_SYMBOLS.IBanUserController)
|
.bind(DI_SYMBOLS.IBanUserController)
|
||||||
.toHigherOrderFunction(banUserController, [
|
.toHigherOrderFunction(banUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IBanUserUseCase
|
DI_SYMBOLS.IBanUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
.bind(DI_SYMBOLS.IUnbanUserController)
|
.bind(DI_SYMBOLS.IUnbanUserController)
|
||||||
.toHigherOrderFunction(unbanUserController, [
|
.toHigherOrderFunction(unbanUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IUnbanUserUseCase
|
DI_SYMBOLS.IUnbanUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
|
@ -194,14 +196,16 @@ export function createUsersModule() {
|
||||||
.bind(DI_SYMBOLS.IUpdateUserController)
|
.bind(DI_SYMBOLS.IUpdateUserController)
|
||||||
.toHigherOrderFunction(updateUserController, [
|
.toHigherOrderFunction(updateUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IUpdateUserUseCase
|
DI_SYMBOLS.IUpdateUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
.bind(DI_SYMBOLS.IDeleteUserController)
|
.bind(DI_SYMBOLS.IDeleteUserController)
|
||||||
.toHigherOrderFunction(deleteUserController, [
|
.toHigherOrderFunction(deleteUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IDeleteUserUseCase
|
DI_SYMBOLS.IDeleteUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,13 @@ export const sendMagicLinkUseCase = (
|
||||||
) => async (input: { email: string }): Promise<void> => {
|
) => async (input: { email: string }): Promise<void> => {
|
||||||
return await instrumentationService.startSpan({ name: "sendMagicLink Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "sendMagicLink Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const user = await usersRepository.getUserByEmail(input.email)
|
const user = await usersRepository.getUserByEmail({ email: input.email })
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
await authenticationService.sendMagicLink(input.email)
|
await authenticationService.sendMagicLink({ email: input.email })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,13 +13,13 @@ export const sendPasswordRecoveryUseCase = (
|
||||||
) => async (input: { email: string }): Promise<void> => {
|
) => async (input: { email: string }): Promise<void> => {
|
||||||
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const user = await usersRepository.getUserByEmail(input.email)
|
const user = await usersRepository.getUserByEmail({ email: input.email })
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
await authenticationService.sendPasswordRecovery(input.email)
|
await authenticationService.sendPasswordRecovery({ email: input.email })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ServerActionErrorParams } from "@/app/_lib/types/error-server-action.interface";
|
||||||
|
|
||||||
export class DatabaseOperationError extends Error {
|
export class DatabaseOperationError extends Error {
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
super(message, options);
|
super(message, options);
|
||||||
|
@ -16,12 +18,15 @@ export class InputParseError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ServerActionError extends Error {
|
export class ServerActionError extends Error {
|
||||||
code: string;
|
code: string;
|
||||||
|
field?: string;
|
||||||
|
|
||||||
constructor(message: string, code: string) {
|
constructor({ message, code, field }: ServerActionErrorParams) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ServerActionError";
|
this.name = "ServerActionError";
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
this.field = field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,31 +30,39 @@ export const UpdateUserSchema = z.object({
|
||||||
|
|
||||||
export type IUpdateUserSchema = z.infer<typeof UpdateUserSchema>;
|
export type IUpdateUserSchema = z.infer<typeof UpdateUserSchema>;
|
||||||
|
|
||||||
export const defaulIUpdateUserSchemaValues: IUpdateUserSchema = {
|
export const defaultValueUpdateUserSchema: IUpdateUserSchema = {
|
||||||
email: "",
|
email: undefined,
|
||||||
email_confirmed_at: false,
|
email_confirmed_at: undefined,
|
||||||
encrypted_password: "",
|
encrypted_password: undefined,
|
||||||
role: "user",
|
role: "user",
|
||||||
phone: "",
|
phone: undefined,
|
||||||
phone_confirmed_at: false,
|
phone_confirmed_at: undefined,
|
||||||
invited_at: "",
|
invited_at: undefined,
|
||||||
confirmed_at: "",
|
confirmed_at: undefined,
|
||||||
last_sign_in_at: "",
|
last_sign_in_at: undefined,
|
||||||
created_at: "",
|
created_at: undefined,
|
||||||
updated_at: "",
|
updated_at: undefined,
|
||||||
is_anonymous: false,
|
is_anonymous: false,
|
||||||
user_metadata: {},
|
user_metadata: {},
|
||||||
app_metadata: {},
|
app_metadata: {},
|
||||||
profile: {
|
profile: {
|
||||||
avatar: "",
|
avatar: undefined,
|
||||||
username: "",
|
username: undefined,
|
||||||
first_name: "",
|
first_name: undefined,
|
||||||
last_name: "",
|
last_name: undefined,
|
||||||
bio: "",
|
bio: undefined,
|
||||||
address: "",
|
address: {
|
||||||
birth_date: new Date(),
|
street: "",
|
||||||
}
|
city: "",
|
||||||
|
state: "",
|
||||||
|
country: "",
|
||||||
|
postal_code: "",
|
||||||
|
},
|
||||||
|
birth_date: undefined,
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const CredentialUpdateUserSchema = z.object({
|
export const CredentialUpdateUserSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const deleteUserController =
|
||||||
const session = await getCurrentUserUseCase()
|
const session = await getCurrentUserUseCase()
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new UnauthenticatedError("Must be logged in to create a user")
|
throw new UnauthenticatedError("Must be logged in to delete a user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return await deleteUserUseCase(credential);
|
return await deleteUserUseCase(credential);
|
||||||
|
|
Loading…
Reference in New Issue