Menggunakan rreact query untuk menangani CRUD
This commit is contained in:
parent
84490a1c70
commit
5ee59cbf20
|
@ -2,9 +2,9 @@
|
|||
|
||||
import * as React from "react";
|
||||
|
||||
import { NavMain } from "@/app/_components/admin/navigations/nav-main";
|
||||
import { NavReports } from "@/app/_components/admin/navigations/nav-report";
|
||||
import { NavUser } from "@/app/_components/admin/navigations/nav-user";
|
||||
import { NavMain } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-main";
|
||||
import { NavReports } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-report";
|
||||
import { NavUser } from "@/app/(protected)/(admin)/_components/admin/navigations/nav-user";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
} from "@/app/_components/ui/sidebar";
|
||||
import { NavPreMain } from "./navigations/nav-pre-main";
|
||||
import { navData } from "@/prisma/data/nav";
|
||||
import { TeamSwitcher } from "../team-switcher";
|
||||
import { TeamSwitcher } from "../../../../_components/team-switcher";
|
||||
|
||||
import { Profile, User } from "@/src/models/users/users.model";
|
||||
import { getCurrentUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
|
@ -4,7 +4,7 @@ import { Card, CardContent } from "@/app/_components/ui/card";
|
|||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { Upload } from "lucide-react";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Badge } from "../../../../../_components/ui/badge";
|
||||
import {
|
||||
IconBrandGoogleAnalytics,
|
||||
IconCsv,
|
|
@ -5,8 +5,8 @@ import { ChevronDown } from "lucide-react";
|
|||
import { Switch } from "@/app/_components/ui/switch";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import { ThemeSwitcher } from "../../theme-switcher";
|
||||
import DropdownSwitcher from "../../custom-dropdown-switcher";
|
||||
import { ThemeSwitcher } from "../../../../../_components/theme-switcher";
|
||||
import DropdownSwitcher from "../../../../../_components/custom-dropdown-switcher";
|
||||
import {
|
||||
type CookiePreferences,
|
||||
defaultCookiePreferences,
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
|
@ -15,6 +13,7 @@ import { Checkbox } from "@/app/_components/ui/checkbox";
|
|||
import { createUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||
import { toast } from "sonner";
|
||||
import { Mail, Lock, Loader2, X } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
interface AddUserDialogProps {
|
||||
open: boolean;
|
||||
|
@ -27,7 +26,6 @@ export function AddUserDialog({
|
|||
onOpenChange,
|
||||
onUserAdded,
|
||||
}: AddUserDialogProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
|
@ -39,17 +37,10 @@ export function AddUserDialog({
|
|||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await createUser({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
email_confirm: formData.emailConfirm,
|
||||
});
|
||||
|
||||
const { mutate: createUserMutation, isPending } = useMutation({
|
||||
mutationKey: ["createUser"],
|
||||
mutationFn: createUser,
|
||||
onSuccess: () => {
|
||||
toast.success("User created successfully.");
|
||||
onUserAdded();
|
||||
onOpenChange(false);
|
||||
|
@ -58,10 +49,24 @@ export function AddUserDialog({
|
|||
password: "",
|
||||
emailConfirm: true,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to create user.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await createUserMutation({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
email_confirm: formData.emailConfirm,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Failed to create user.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -144,8 +149,8 @@ export function AddUserDialog({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={isLoading} className="w-full ">
|
||||
{isLoading ? (
|
||||
<Button type="submit" disabled={isPending} className="w-full ">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@/app/_components/ui/badge";
|
||||
import { Checkbox } from "@/app/_components/ui/checkbox";
|
|
@ -48,6 +48,7 @@ interface DataTableProps<TData, TValue> {
|
|||
data: TData[];
|
||||
loading?: boolean;
|
||||
onRowClick?: (row: TData) => void;
|
||||
onActionClick?: (row: TData, action: string) => void;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,7 @@ export function DataTable<TData, TValue>({
|
|||
data,
|
||||
loading = false,
|
||||
onRowClick,
|
||||
onActionClick,
|
||||
pageSize = 5,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
@ -198,11 +200,38 @@ export function DataTable<TData, TValue>({
|
|||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
{/* {onActionClick && (
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onActionClick(row.original, "update");
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onActionClick(row.original, "delete");
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
)} */}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
|
@ -35,8 +33,6 @@ export function InviteUserDialog({
|
|||
metadata: "{}",
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
|
@ -44,23 +40,10 @@ export function InviteUserDialog({
|
|||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
let metadata = {};
|
||||
try {
|
||||
metadata = JSON.parse(formData.metadata);
|
||||
} catch (error) {
|
||||
toast.error("Invalid JSON. Please check your metadata format.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await inviteUser({
|
||||
email: formData.email,
|
||||
});
|
||||
const { mutate: inviteUserMutation, isPending } = useMutation({
|
||||
mutationKey: ["inviteUser"],
|
||||
mutationFn: inviteUser,
|
||||
onSuccess: () => {
|
||||
toast.success("Invitation sent");
|
||||
onUserInvited();
|
||||
onOpenChange(false);
|
||||
|
@ -68,10 +51,24 @@ export function InviteUserDialog({
|
|||
email: "",
|
||||
metadata: "{}",
|
||||
});
|
||||
} catch (error) {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to send invitation");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
let metadata = {};
|
||||
try {
|
||||
metadata = JSON.parse(formData.metadata);
|
||||
inviteUserMutation({
|
||||
email: formData.email,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Invalid JSON. Please check your metadata format.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -106,8 +103,8 @@ export function InviteUserDialog({
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Sending..." : "Send Invitation"}
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? "Sending..." : "Send Invitation"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
@ -43,19 +41,19 @@ import {
|
|||
} from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||
import { format } from "date-fns";
|
||||
|
||||
interface UserDetailsSheetProps {
|
||||
interface UserDetailSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
user: any;
|
||||
onUserUpdate: () => void;
|
||||
}
|
||||
|
||||
export function UserDetailsSheet({
|
||||
export function UserDetailSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
user,
|
||||
onUserUpdate,
|
||||
}: UserDetailsSheetProps) {
|
||||
}: UserDetailSheetProps) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState({
|
||||
deleteUser: false,
|
||||
|
@ -64,67 +62,105 @@ export function UserDetailsSheet({
|
|||
toggleBan: false,
|
||||
});
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: () => deleteUser(user.id),
|
||||
onMutate: () => {
|
||||
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
|
||||
setIsDeleting(true);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("User deleted successfully");
|
||||
onUserUpdate();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to delete user");
|
||||
} finally {
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const handleSendPasswordRecovery = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
||||
try {
|
||||
const sendPasswordRecoveryMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!user.email) {
|
||||
throw new Error("User does not have an email address");
|
||||
}
|
||||
await sendPasswordRecovery(user.email);
|
||||
return sendPasswordRecovery(user.email);
|
||||
},
|
||||
onMutate: () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Password recovery email sent");
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to send password recovery email");
|
||||
} finally {
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const handleSendMagicLink = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
||||
try {
|
||||
const sendMagicLinkMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!user.email) {
|
||||
throw new Error("User does not have an email address");
|
||||
}
|
||||
await sendMagicLink(user.email);
|
||||
return sendMagicLink(user.email);
|
||||
},
|
||||
onMutate: () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Magic link sent successfully");
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to send magic link");
|
||||
} finally {
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const handleToggleBan = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
||||
try {
|
||||
const toggleBanMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (user.banned_until) {
|
||||
await unbanUser(user.id);
|
||||
return unbanUser(user.id);
|
||||
} else {
|
||||
await banUser(user.id);
|
||||
return banUser(user.id);
|
||||
}
|
||||
},
|
||||
onMutate: () => {
|
||||
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("User ban status updated");
|
||||
onUserUpdate();
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to update user ban status");
|
||||
} finally {
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteUser = () => {
|
||||
deleteUserMutation.mutate();
|
||||
};
|
||||
|
||||
const handleSendPasswordRecovery = () => {
|
||||
sendPasswordRecoveryMutation.mutate();
|
||||
};
|
||||
|
||||
const handleSendMagicLink = () => {
|
||||
sendMagicLinkMutation.mutate();
|
||||
};
|
||||
|
||||
const handleToggleBan = () => {
|
||||
toggleBanMutation.mutate();
|
||||
};
|
||||
|
||||
const handleCopyItem = (item: string) => {
|
|
@ -0,0 +1,449 @@
|
|||
import { FormDescription } from "@/app/_components/ui/form";
|
||||
|
||||
import { useState } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/app/_components/ui/sheet";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/app/_components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/app/_components/ui/select";
|
||||
import { Input } from "@/app/_components/ui/input";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Textarea } from "@/app/_components/ui/textarea";
|
||||
import { Calendar } from "@/app/_components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/app/_components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CalendarIcon, Loader2 } from "lucide-react";
|
||||
import { Switch } from "@/app/_components/ui/switch";
|
||||
import { User, UserSchema } from "@/src/models/users/users.model";
|
||||
|
||||
type UserProfileFormValues = z.infer<typeof UserSchema>;
|
||||
|
||||
interface UserProfileSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
userData?: User; // Replace with your user data type
|
||||
onSave: (data: UserProfileFormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export function UserProfileSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
userData,
|
||||
onSave,
|
||||
}: UserProfileSheetProps) {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Initialize form with user data
|
||||
const form = useForm<UserProfileFormValues>({
|
||||
resolver: zodResolver(UserSchema),
|
||||
defaultValues: {
|
||||
email: userData?.email || "",
|
||||
phone: userData?.phone || "",
|
||||
role: userData?.role || "user",
|
||||
is_anonymous: userData?.is_anonymous || false,
|
||||
profile: {
|
||||
username: userData?.profile?.username || "",
|
||||
first_name: userData?.profile?.first_name || "",
|
||||
last_name: userData?.profile?.last_name || "",
|
||||
bio: userData?.profile?.bio || "",
|
||||
birth_date: userData?.profile?.birth_date
|
||||
? new Date(userData.profile.birth_date)
|
||||
: null,
|
||||
avatar: userData?.profile?.avatar || "",
|
||||
address: userData?.profile?.address || {
|
||||
street: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "",
|
||||
postal_code: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: UserProfileFormValues) {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await onSave(data);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving user profile:", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
|
||||
<SheetHeader className="mb-6">
|
||||
<SheetTitle>Update User Profile</SheetTitle>
|
||||
<SheetDescription>
|
||||
Make changes to the user profile here. Click save when you're
|
||||
done.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* User Information Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">User Information</h3>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Phone</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="tel"
|
||||
placeholder="+1234567890"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="is_anonymous"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Anonymous User
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Make this user anonymous in the system
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profile Information Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Profile Information</h3>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="username"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.first_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="John"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.last_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Doe"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="Tell us about yourself"
|
||||
className="resize-none"
|
||||
rows={4}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.birth_date"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={
|
||||
field.value instanceof Date
|
||||
? field.value
|
||||
: undefined
|
||||
}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Avatar URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="url"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Address Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Address</h3>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.address.street"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Street Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="123 Main St" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.address.city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>City</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="City" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.address.state"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>State</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="State" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.address.country"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Country</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Country" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profile.address.postal_code"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postal Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Postal Code" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-4 sticky bottom-0 bg-background py-4 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isSaving ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useState, useMemo, useEffect, JSX } from "react";
|
||||
import {
|
||||
PlusCircle,
|
||||
Search,
|
||||
|
@ -18,6 +18,9 @@ import {
|
|||
Calendar,
|
||||
ShieldAlert,
|
||||
ListFilter,
|
||||
XCircle,
|
||||
Trash2,
|
||||
UserPen,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Input } from "@/app/_components/ui/input";
|
||||
|
@ -30,33 +33,39 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/app/_components/ui/dropdown-menu";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
|
||||
import { User } from "@/src/models/users/users.model";
|
||||
import { toast } from "sonner";
|
||||
import { DataTable } from "./data-table";
|
||||
import { InviteUserDialog } from "./invite-user";
|
||||
import { AddUserDialog } from "./add-user-dialog";
|
||||
import { UserDetailsSheet } from "./sheet";
|
||||
import { UserDetailSheet } from "./sheet";
|
||||
import { Avatar } from "@radix-ui/react-avatar";
|
||||
import Image from "next/image";
|
||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||
import { ColumnDef, HeaderContext } from "@tanstack/react-table";
|
||||
import { UserProfileSheet } from "./update-user";
|
||||
|
||||
type UserFilterOptions = {
|
||||
email: string;
|
||||
phone: string;
|
||||
lastSignIn: string;
|
||||
createdAt: string;
|
||||
status: string[];
|
||||
};
|
||||
|
||||
type UserTableColumn = ColumnDef<User, User>;
|
||||
|
||||
export default function UserManagement() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedUser, setSelectedUser] = useState<User | 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<{
|
||||
email: string;
|
||||
phone: string;
|
||||
lastSignIn: string;
|
||||
createdAt: string;
|
||||
status: string[];
|
||||
}>({
|
||||
const [filters, setFilters] = useState<UserFilterOptions>({
|
||||
email: "",
|
||||
phone: "",
|
||||
lastSignIn: "",
|
||||
|
@ -65,45 +74,27 @@ export default function UserManagement() {
|
|||
});
|
||||
|
||||
// Use React Query to fetch users
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const { isLoading, setIsLoading } = useNavigations();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const fetchedUsers = await fetchUsers();
|
||||
setUsers(fetchedUsers);
|
||||
} catch (error) {
|
||||
toast.error("Failed to fetch users");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const refetch = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const fetchedUsers = await fetchUsers();
|
||||
setUsers(fetchedUsers);
|
||||
} catch (error) {
|
||||
toast.error("Failed to fetch users");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
data: users = [],
|
||||
isLoading,
|
||||
refetch,
|
||||
isPlaceholderData,
|
||||
} = useQuery<User[]>({
|
||||
queryKey: ["users"],
|
||||
queryFn: fetchUsers,
|
||||
placeholderData: keepPreviousData,
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
const handleUserClick = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setIsSheetOpen(true);
|
||||
};
|
||||
|
||||
const handleUserUpdate = () => {
|
||||
refetch();
|
||||
const handleUserUpdate = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setIsSheetOpen(false);
|
||||
setIsUpdateOpen(true);
|
||||
};
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
|
@ -224,10 +215,10 @@ export default function UserManagement() {
|
|||
(Array.isArray(value) && value.length > 0)
|
||||
).length;
|
||||
|
||||
const columns = [
|
||||
const columns: UserTableColumn[] = [
|
||||
{
|
||||
id: "email",
|
||||
header: ({ column }: any) => (
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Email</span>
|
||||
<DropdownMenu>
|
||||
|
@ -257,7 +248,7 @@ export default function UserManagement() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: User } }) => (
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
||||
{row.original.profile?.avatar ? (
|
||||
|
@ -285,7 +276,7 @@ export default function UserManagement() {
|
|||
},
|
||||
{
|
||||
id: "phone",
|
||||
header: ({ column }: any) => (
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Phone</span>
|
||||
<DropdownMenu>
|
||||
|
@ -315,11 +306,11 @@ export default function UserManagement() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: User } }) => row.original.phone || "-",
|
||||
cell: ({ row }) => row.original.phone || "-",
|
||||
},
|
||||
{
|
||||
id: "lastSignIn",
|
||||
header: ({ column }: any) => (
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Last Sign In</span>
|
||||
<DropdownMenu>
|
||||
|
@ -383,7 +374,7 @@ export default function UserManagement() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: User } }) => {
|
||||
cell: ({ row }) => {
|
||||
return row.original.last_sign_in_at
|
||||
? new Date(row.original.last_sign_in_at).toLocaleString()
|
||||
: "Never";
|
||||
|
@ -391,7 +382,7 @@ export default function UserManagement() {
|
|||
},
|
||||
{
|
||||
id: "createdAt",
|
||||
header: ({ column }: any) => (
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Created At</span>
|
||||
<DropdownMenu>
|
||||
|
@ -444,7 +435,7 @@ export default function UserManagement() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: User } }) => {
|
||||
cell: ({ row }) => {
|
||||
return row.original.created_at
|
||||
? new Date(row.original.created_at).toLocaleString()
|
||||
: "N/A";
|
||||
|
@ -452,7 +443,7 @@ export default function UserManagement() {
|
|||
},
|
||||
{
|
||||
id: "status",
|
||||
header: ({ column }: any) => (
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Status</span>
|
||||
<DropdownMenu>
|
||||
|
@ -514,7 +505,7 @@ export default function UserManagement() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: { original: User } }) => {
|
||||
cell: ({ row }) => {
|
||||
if (row.original.banned_until) {
|
||||
return <Badge variant="destructive">Banned</Badge>;
|
||||
}
|
||||
|
@ -527,17 +518,36 @@ export default function UserManagement() {
|
|||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
cell: ({ row }: { row: { original: User } }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUserClick(row.original);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
||||
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
||||
Update
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle delete */
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle ban */
|
||||
}}
|
||||
>
|
||||
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||
{row.original.banned_until != null ? "Unban" : "Ban"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -600,34 +610,38 @@ export default function UserManagement() {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={filteredUsers}
|
||||
loading={isLoading}
|
||||
onRowClick={(user) => handleUserClick(user)}
|
||||
/>
|
||||
|
||||
{selectedUser && (
|
||||
<UserDetailsSheet
|
||||
<UserDetailSheet
|
||||
user={selectedUser}
|
||||
open={isSheetOpen}
|
||||
onOpenChange={setIsSheetOpen}
|
||||
onUserUpdate={handleUserUpdate}
|
||||
onUserUpdate={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddUserDialog
|
||||
open={isAddUserOpen}
|
||||
onOpenChange={setIsAddUserOpen}
|
||||
onUserAdded={() => refetch()}
|
||||
/>
|
||||
|
||||
<InviteUserDialog
|
||||
open={isInviteUserOpen}
|
||||
onOpenChange={setIsInviteUserOpen}
|
||||
onUserInvited={() => refetch()}
|
||||
/>
|
||||
{selectedUser && (
|
||||
<UserProfileSheet
|
||||
open={isUpdateOpen}
|
||||
onOpenChange={setIsUpdateOpen}
|
||||
userData={selectedUser}
|
||||
onSave={async () => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -28,23 +28,10 @@ function calculateUserStats(users: User[]) {
|
|||
}
|
||||
|
||||
export function UserStats() {
|
||||
const { isLoading, setIsLoading } = useNavigations();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const fetchedUsers = await fetchUsers();
|
||||
setUsers(fetchedUsers);
|
||||
} catch (error) {
|
||||
toast.error("Failed to fetch users");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUserData();
|
||||
}, [setIsLoading]);
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: fetchUsers,
|
||||
});
|
||||
|
||||
const stats = calculateUserStats(users);
|
||||
|
|
@ -36,6 +36,8 @@ export async function fetchUsers(): Promise<User[]> {
|
|||
throw new Error("Users not found");
|
||||
}
|
||||
|
||||
console.log("fetchedUsers");
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import UserManagement from "@/app/_components/admin/users/user-management";
|
||||
import { UserStats } from "@/app/_components/admin/users/user-stats";
|
||||
import UserManagement from "@/app/(protected)/(admin)/dashboard/user-management/_components/user-management";
|
||||
import { UserStats } from "@/app/(protected)/(admin)/dashboard/user-management/_components/user-stats";
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
|
|
|
@ -26,7 +26,7 @@ import { ThemeSwitcher } from "@/app/_components/theme-switcher";
|
|||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { InboxDrawer } from "@/app/_components/inbox-drawer";
|
||||
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
|
||||
import { AppSidebar } from "@/app/_components/admin/app-sidebar";
|
||||
import { AppSidebar } from "@/app/(protected)/(admin)/_components/admin/app-sidebar";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// "use client"
|
||||
|
||||
// import { useState } from "react"
|
||||
// import { DataTable } from "./data-table"
|
||||
// import { columns, User } from "./column"
|
||||
// import { useQuery } from "react-query"
|
||||
// import { getUsers } from "../../user-management/action"
|
||||
// import { UserDetailSheet } from "./sheet"
|
||||
|
||||
|
||||
// export function UsersTable() {
|
||||
// const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
||||
// const [sheetOpen, setSheetOpen] = useState(false)
|
||||
|
||||
// const { data: users = [], isLoading } = useQuery({
|
||||
// queryKey: ["users"],
|
||||
// queryFn: getUsers,
|
||||
// })
|
||||
|
||||
// const handleRowClick = (user: User) => {
|
||||
// setSelectedUser(user)
|
||||
// setSheetOpen(true)
|
||||
// }
|
||||
|
||||
// if (isLoading) {
|
||||
// return <div>Loading...</div>
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div className="w-full">
|
||||
// <DataTable columns={columns} data={users} onRowClick={handleRowClick} />
|
||||
// {selectedUser && <UserDetailSheet user={selectedUser} open={sheetOpen} onOpenChange={setSheetOpen} />}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
|
@ -33,15 +33,16 @@ export default function RootLayout({
|
|||
return (
|
||||
<html lang="en" className={geistSans.className} suppressHydrationWarning>
|
||||
<body className="bg-background text-foreground">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<main className="min-h-screen flex flex-col items-center">
|
||||
<div className="flex-1 w-full gap-20 items-center">
|
||||
{/* <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
|
||||
<ReactQueryProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<main className="min-h-screen flex flex-col items-center">
|
||||
<div className="flex-1 w-full gap-20 items-center">
|
||||
{/* <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
|
||||
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
|
||||
<div className="flex gap-5 items-center font-semibold">
|
||||
<Link href={"/"}>
|
||||
|
@ -57,12 +58,12 @@ export default function RootLayout({
|
|||
</div>
|
||||
</div>
|
||||
</nav> */}
|
||||
<div className="flex flex-col max-w-full p-0">
|
||||
{children}
|
||||
<Toaster theme="system" richColors position="top-right" />
|
||||
</div>
|
||||
<div className="flex flex-col max-w-full p-0">
|
||||
{children}
|
||||
<Toaster theme="system" richColors position="top-right" />
|
||||
</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">
|
||||
<p>
|
||||
Powered by{" "
|
||||
<a
|
||||
|
@ -75,9 +76,10 @@ export default function RootLayout({
|
|||
</a>
|
||||
</p>
|
||||
</footer> */}
|
||||
</div>
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,6 +52,8 @@
|
|||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.67.2",
|
||||
"@tanstack/react-query-devtools": "^5.67.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
|
|
|
@ -2,12 +2,31 @@
|
|||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Pengaturan caching global
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
retryDelay: (attemptIndex) =>
|
||||
Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
{process.env.NODE_ENV === "development" && <ReactQueryDevtools />}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue