Remove reactQuery

This commit is contained in:
vergiLgood1 2025-03-01 06:49:42 +07:00
parent 0c16fc2be5
commit f00544cb28
8 changed files with 424 additions and 300 deletions

View File

@ -27,7 +27,7 @@ export function AddUserDialog({
onOpenChange, onOpenChange,
onUserAdded, onUserAdded,
}: AddUserDialogProps) { }: AddUserDialogProps) {
const [loading, setLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: "",
password: "", password: "",
@ -41,7 +41,7 @@ export function AddUserDialog({
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setIsLoading(true);
try { try {
await createUser({ await createUser({
@ -61,21 +61,21 @@ export function AddUserDialog({
} catch (error) { } catch (error) {
toast.error("Failed to create user."); toast.error("Failed to create user.");
} finally { } finally {
setLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md border-0 text-white"> <DialogContent className="sm:max-w-md border-0 ">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4"> <DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
<DialogTitle className="text-xl font-semibold text-white"> <DialogTitle className="text-xl font-semibold ">
Create a new user Create a new user
</DialogTitle> </DialogTitle>
{/* <Button {/* <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-zinc-400 hover:text-white hover:bg-zinc-800" className="h-8 w-8 text-zinc-400 hover: hover:bg-zinc-800"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@ -97,7 +97,7 @@ export function AddUserDialog({
placeholder="user@example.com" placeholder="user@example.com"
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
className="pl-10 bg-zinc-900 border-zinc-800 text-white placeholder:text-zinc-500 " className="pl-10 placeholder:text-zinc-500 "
/> />
</div> </div>
</div> </div>
@ -115,7 +115,7 @@ export function AddUserDialog({
placeholder="••••••••" placeholder="••••••••"
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
className="pl-10 bg-zinc-900 border-zinc-800 text-white placeholder:text-zinc-500 " className="pl-10 placeholder:text-zinc-500 "
/> />
</div> </div>
</div> </div>
@ -134,7 +134,7 @@ export function AddUserDialog({
} }
className="border-zinc-700" className="border-zinc-700"
/> />
<label htmlFor="email-confirm" className="text-sm text-white"> <label htmlFor="email-confirm" className="text-sm ">
Auto Confirm User? Auto Confirm User?
</label> </label>
</div> </div>
@ -144,12 +144,8 @@ export function AddUserDialog({
</p> </p>
</div> </div>
<Button <Button type="submit" disabled={isLoading} className="w-full ">
type="submit" {isLoading ? (
disabled={loading}
className="w-full text-white"
>
{loading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating... Creating...

View File

@ -1,6 +1,6 @@
"use client" "use client";
import { useState } from "react" import { useState } from "react";
import { import {
type ColumnDef, type ColumnDef,
flexRender, flexRender,
@ -11,21 +11,44 @@ import {
getFilteredRowModel, getFilteredRowModel,
type ColumnFiltersState, type ColumnFiltersState,
getPaginationRowModel, getPaginationRowModel,
} from "@tanstack/react-table" } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { Button } from "@/components/ui/button" Table,
import { Input } from "@/components/ui/input" TableBody,
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" TableCell,
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Filter } from "lucide-react" TableHead,
import { Skeleton } from "@/components/ui/skeleton" TableHeader,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Filter,
} from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[];
data: TData[] data: TData[];
loading?: boolean loading?: boolean;
onRowClick?: (row: TData) => void onRowClick?: (row: TData) => void;
pageSize?: number pageSize?: number;
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
@ -35,13 +58,13 @@ export function DataTable<TData, TValue>({
onRowClick, onRowClick,
pageSize = 5, pageSize = 5,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]) const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState({}) const [columnVisibility, setColumnVisibility] = useState({});
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: pageSize, pageSize: pageSize,
}) });
const table = useReactTable({ const table = useReactTable({
data, data,
@ -60,7 +83,7 @@ export function DataTable<TData, TValue>({
columnVisibility, columnVisibility,
pagination, pagination,
}, },
}) });
if (loading) { if (loading) {
return ( return (
@ -71,7 +94,12 @@ export function DataTable<TData, TValue>({
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<TableHead key={header.id}> <TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@ -90,7 +118,7 @@ export function DataTable<TData, TValue>({
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
) );
} }
return ( return (
@ -105,11 +133,16 @@ export function DataTable<TData, TValue>({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={ className={
header.column.getCanSort() ? "cursor-pointer select-none flex items-center gap-1" : "" header.column.getCanSort()
? "cursor-pointer select-none flex items-center gap-1"
: ""
} }
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
> >
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{ {{
asc: " 🔼", asc: " 🔼",
desc: " 🔽", desc: " 🔽",
@ -131,8 +164,13 @@ export function DataTable<TData, TValue>({
<div className="p-2"> <div className="p-2">
<Input <Input
placeholder={`Filter ${header.column.id}...`} placeholder={`Filter ${header.column.id}...`}
value={(header.column.getFilterValue() as string) ?? ""} value={
onChange={(e) => header.column.setFilterValue(e.target.value)} (header.column.getFilterValue() as string) ??
""
}
onChange={(e) =>
header.column.setFilterValue(e.target.value)
}
className="h-8" className="h-8"
/> />
</div> </div>
@ -156,7 +194,9 @@ export function DataTable<TData, TValue>({
onClick={() => onRowClick && onRowClick(row.original)} onClick={() => onRowClick && onRowClick(row.original)}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell> <TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))} ))}
</TableRow> </TableRow>
)) ))
@ -174,10 +214,15 @@ export function DataTable<TData, TValue>({
<div className="flex items-center justify-between px-4 py-2 border-t"> <div className="flex items-center justify-between px-4 py-2 border-t">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<div> <div>
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{" "} Showing{" "}
{table.getState().pagination.pageIndex *
table.getState().pagination.pageSize +
1}{" "}
to{" "}
{Math.min( {Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, (table.getState().pagination.pageIndex + 1) *
table.getFilteredRowModel().rows.length, table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length
)}{" "} )}{" "}
of {table.getFilteredRowModel().rows.length} entries of {table.getFilteredRowModel().rows.length} entries
</div> </div>
@ -188,11 +233,13 @@ export function DataTable<TData, TValue>({
<Select <Select
value={`${table.getState().pagination.pageSize}`} value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => { onValueChange={(value) => {
table.setPageSize(Number(value)) table.setPageSize(Number(value));
}} }}
> >
<SelectTrigger className="h-8 w-[70px]"> <SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} /> <SelectValue
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">
{[5, 10, 20, 30, 40, 50].map((pageSize) => ( {[5, 10, 20, 30, 40, 50].map((pageSize) => (
@ -223,7 +270,8 @@ export function DataTable<TData, TValue>({
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</Button> </Button>
<div className="flex items-center gap-1 text-sm font-medium"> <div className="flex items-center gap-1 text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div> </div>
<Button <Button
variant="outline" variant="outline"
@ -247,6 +295,5 @@ export function DataTable<TData, TValue>({
</div> </div>
</div> </div>
</div> </div>
) );
} }

View File

@ -35,34 +35,7 @@ export function InviteUserDialog({
metadata: "{}", metadata: "{}",
}); });
const inviteUserMutation = useMutation({ const [isLoading, setIsLoading] = useState(false);
mutationFn: async () => {
let metadata = {};
try {
metadata = JSON.parse(formData.metadata);
} catch (error) {
toast.error("Invalid JSON. Please check your metadata format.");
throw new Error("Invalid JSON");
}
return inviteUser({
email: formData.email,
user_metadata: metadata,
});
},
onSuccess: () => {
toast.success("Invitation sent");
onUserInvited();
onOpenChange(false);
setFormData({
email: "",
metadata: "{}",
});
},
onError: () => {
toast.error("Failed to send invitation");
},
});
const handleInputChange = ( const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
@ -71,9 +44,36 @@ export function InviteUserDialog({
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
}; };
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
inviteUserMutation.mutate(); 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,
user_metadata: metadata,
});
toast.success("Invitation sent");
onUserInvited();
onOpenChange(false);
setFormData({
email: "",
metadata: "{}",
});
} catch (error) {
toast.error("Failed to send invitation");
} finally {
setIsLoading(false);
}
}; };
return ( return (
@ -107,8 +107,8 @@ export function InviteUserDialog({
> >
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={inviteUserMutation.isPending}> <Button type="submit" disabled={isLoading}>
{inviteUserMutation.isPending ? "Sending..." : "Send Invitation"} {isLoading ? "Sending..." : "Send Invitation"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@ -1,12 +1,18 @@
"use client" "use client";
import { useState } from "react" import { useState } from "react";
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner" import { toast } from "sonner";
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet" import {
import { Button } from "@/components/ui/button" Sheet,
import { Badge } from "@/components/ui/badge" SheetContent,
import { Separator } from "@/components/ui/separator" SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -17,118 +23,118 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog";
import { Mail, Trash2, Ban, SendHorizonal, CheckCircle, XCircle, Copy, Loader2 } from "lucide-react" import {
import { banUser, deleteUser, sendMagicLink, sendPasswordRecovery, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action" Mail,
Trash2,
// // Mock functions (replace with your actual API calls) Ban,
// const updateUser = async (id: string, data: any) => { SendHorizonal,
// console.log(`Updating user ${id} with data:`, data) CheckCircle,
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call XCircle,
// return { id, ...data } Copy,
// } Loader2,
} from "lucide-react";
// const deleteUser = async (id: string) => { import {
// console.log(`Deleting user ${id}`) banUser,
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call deleteUser,
// return { success: true } sendMagicLink,
// } sendPasswordRecovery,
unbanUser,
// const sendPasswordRecovery = async (email: string) => { } from "@/app/protected/(admin)/dashboard/user-management/action";
// console.log(`Sending password recovery email to ${email}`) import { format } from "date-fns";
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { success: true }
// }
// const sendMagicLink = async (email: string) => {
// console.log(`Sending magic link to ${email}`)
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { success: true }
// }
// const banUser = async (id: string) => {
// console.log(`Banning user ${id}`)
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { success: true }
// }
// const unbanUser = async (id: string) => {
// console.log(`Unbanning user ${id}`)
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { success: true }
// }
interface UserDetailsSheetProps { interface UserDetailsSheetProps {
open: boolean open: boolean;
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void;
user: any user: any;
onUserUpdate: () => void onUserUpdate: () => void;
} }
export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: UserDetailsSheetProps) { export function UserDetailsSheet({
const [isDeleting, setIsDeleting] = useState(false) open,
onOpenChange,
user,
onUserUpdate,
}: UserDetailsSheetProps) {
const [isDeleting, setIsDeleting] = useState(false);
const [isLoading, setIsLoading] = useState({
deleteUser: false,
sendPasswordRecovery: false,
sendMagicLink: false,
toggleBan: false,
});
const deleteUserMutation = useMutation({ const handleDeleteUser = async () => {
mutationFn: () => deleteUser(user.id), setIsLoading((prev) => ({ ...prev, deleteUser: true }));
onSuccess: () => { setIsDeleting(true);
toast.success("User deleted successfully") try {
onUserUpdate() await deleteUser(user.id);
onOpenChange(false) toast.success("User deleted successfully");
}, onUserUpdate();
onError: () => { onOpenChange(false);
toast.error("Failed to delete user") } catch {
}, toast.error("Failed to delete user");
onSettled: () => { } finally {
setIsDeleting(false) setIsLoading((prev) => ({ ...prev, deleteUser: false }));
}, setIsDeleting(false);
})
const sendPasswordRecoveryMutation = useMutation({
mutationFn: () => {
if (!user.email) {
throw new Error("User does not have an email address")
} }
return sendPasswordRecovery(user.email) };
},
onSuccess: () => {
toast.success("Password recovery email sent")
},
onError: () => {
toast.error("Failed to send password recovery email")
},
})
const sendMagicLinkMutation = useMutation({ const handleSendPasswordRecovery = async () => {
mutationFn: () => { setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
try {
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) await sendPasswordRecovery(user.email);
}, toast.success("Password recovery email sent");
onSuccess: () => { } catch {
toast.success("Magic link sent successfully") toast.error("Failed to send password recovery email");
}, } finally {
onError: () => { setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
toast.error("Failed to send magic link") }
}, };
})
const toggleBanMutation = useMutation({ const handleSendMagicLink = async () => {
mutationFn: () => { setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
try {
if (!user.email) {
throw new Error("User does not have an email address");
}
await sendMagicLink(user.email);
toast.success("Magic link sent successfully");
} catch {
toast.error("Failed to send magic link");
} finally {
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
}
};
const handleToggleBan = async () => {
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
try {
if (user.banned_until) { if (user.banned_until) {
return unbanUser(user.id) await unbanUser(user.id);
} else { } else {
return banUser(user.id) await banUser(user.id);
} }
}, toast.success("User ban status updated");
onSuccess: () => { onUserUpdate();
toast.success("User ban status updated") } catch {
onUserUpdate() toast.error("Failed to update user ban status");
}, } finally {
onError: () => { setIsLoading((prev) => ({ ...prev, toggleBan: false }));
toast.error("Failed to update user ban status") }
}, };
})
const handleCopyItem = (item: string) => {
navigator.clipboard.writeText(item);
toast.success("Copied to clipboard");
};
const formatDate = (date: string | undefined | null) => {
return date ? format(new Date(date), "PPpp") : "-";
};
return ( return (
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
@ -140,16 +146,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-4 w-4" className="h-4 w-4"
onClick={() => { onClick={() => handleCopyItem(user.email)}
navigator.clipboard.writeText(user.email)
// Optionally add a toast notification here
}}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
{user.banned_until && <Badge variant="destructive">Banned</Badge>} {user.banned_until && <Badge variant="destructive">Banned</Badge>}
{!user.email_confirmed_at && <Badge variant="outline">Unconfirmed</Badge>} {!user.email_confirmed_at && (
{!user.banned_until && user.email_confirmed_at && <Badge variant="default">Active</Badge>} <Badge variant="outline">Unconfirmed</Badge>
)}
{!user.banned_until && user.email_confirmed_at && (
<Badge variant="default">Active</Badge>
)}
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
@ -167,10 +174,7 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-4 w-4 ml-2" className="h-4 w-4 ml-2"
onClick={() => { onClick={() => handleCopyItem(user.id)}
navigator.clipboard.writeText(user.id)
// Optionally add a toast notification here
}}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
@ -184,27 +188,33 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<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>{new Date(user.updated_at || user.created_at).toLocaleString()}</span> <span>
{new Date(
user.updated_at || user.created_at
).toLocaleString()}
</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">Invited at</span> <span className="text-muted-foreground">Invited at</span>
<span>{user.invited_at ? new Date(user.invited_at).toLocaleString() : "-"}</span> <span>{formatDate(user.invited_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">Confirmation sent at</span> <span className="text-muted-foreground">
<span>{user.confirmation_sent_at ? new Date(user.confirmation_sent_at).toLocaleString() : "-"}</span> Confirmation sent at
</span>
<span>{formatDate(user.email_confirmation_sent_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">Confirmed at</span> <span className="text-muted-foreground">Confirmed at</span>
<span>{user.email_confirmed_at ? new Date(user.email_confirmed_at).toLocaleString() : "-"}</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">
<span className="text-muted-foreground">Last signed in</span> <span className="text-muted-foreground">Last signed in</span>
<span>{user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : "Never"}</span> <span>{formatDate(user.last_sign_in_at)}</span>
</div> </div>
<div className="flex justify-between items-center py-1"> <div className="flex justify-between items-center py-1">
@ -219,7 +229,9 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* Provider Information Section */} {/* Provider Information Section */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Provider Information</h3> <h3 className="text-lg font-semibold">Provider Information</h3>
<p className="text-sm text-muted-foreground">The user has the following providers</p> <p className="text-sm text-muted-foreground">
The user has the following providers
</p>
<div className="border rounded-md p-4 space-y-3"> <div className="border rounded-md p-4 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -227,10 +239,15 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<Mail className="h-5 w-5" /> <Mail className="h-5 w-5" />
<div> <div>
<div className="font-medium">Email</div> <div className="font-medium">Email</div>
<div className="text-xs text-muted-foreground">Signed in with a email account via OAuth</div> <div className="text-xs text-muted-foreground">
Signed in with a email account via OAuth
</div> </div>
</div> </div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20"> </div>
<Badge
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
<CheckCircle className="h-3 w-3 mr-1" /> Enabled <CheckCircle className="h-3 w-3 mr-1" /> Enabled
</Badge> </Badge>
</div> </div>
@ -240,15 +257,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h4 className="font-medium">Reset password</h4> <h4 className="font-medium">Reset password</h4>
<p className="text-xs text-muted-foreground">Send a password recovery email to the user</p> <p className="text-xs text-muted-foreground">
Send a password recovery email to the user
</p>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => sendPasswordRecoveryMutation.mutate()} onClick={handleSendPasswordRecovery}
disabled={sendPasswordRecoveryMutation.isPending || !user.email} disabled={isLoading.sendPasswordRecovery || !user.email}
> >
{sendPasswordRecoveryMutation.isPending ? ( {isLoading.sendPasswordRecovery ? (
<> <>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending... Sending...
@ -267,15 +286,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h4 className="font-medium">Send magic link</h4> <h4 className="font-medium">Send magic link</h4>
<p className="text-xs text-muted-foreground">Passwordless login via email for the user</p> <p className="text-xs text-muted-foreground">
Passwordless login via email for the user
</p>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => sendMagicLinkMutation.mutate()} onClick={handleSendMagicLink}
disabled={sendMagicLinkMutation.isPending || !user.email} disabled={isLoading.sendMagicLink || !user.email}
> >
{sendMagicLinkMutation.isPending ? ( {isLoading.sendMagicLink ? (
<> <>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending... Sending...
@ -295,22 +316,28 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* Danger Zone Section */} {/* Danger Zone Section */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold text-destructive">Danger zone</h3> <h3 className="text-lg font-semibold text-destructive">
<p className="text-sm text-muted-foreground">Be wary of the following features as they cannot be undone.</p> Danger zone
</h3>
<p className="text-sm text-muted-foreground">
Be wary of the following features as they cannot be undone.
</p>
<div className="space-y-4"> <div className="space-y-4">
<div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center"> <div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center">
<div> <div>
<h4 className="font-medium">Ban user</h4> <h4 className="font-medium">Ban user</h4>
<p className="text-xs text-muted-foreground">Revoke access to the project for a set duration</p> <p className="text-xs text-muted-foreground">
Revoke access to the project for a set duration
</p>
</div> </div>
<Button <Button
variant={user.banned_until ? "outline" : "outline"} variant={user.banned_until ? "outline" : "outline"}
size="sm" size="sm"
onClick={() => toggleBanMutation.mutate()} onClick={handleToggleBan}
disabled={toggleBanMutation.isPending} disabled={isLoading.toggleBan}
> >
{toggleBanMutation.isPending ? ( {isLoading.toggleBan ? (
<> <>
<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..."}
@ -327,31 +354,47 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center"> <div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center">
<div> <div>
<h4 className="font-medium">Delete user</h4> <h4 className="font-medium">Delete user</h4>
<p className="text-xs text-muted-foreground">User will no longer have access to the project</p> <p className="text-xs text-muted-foreground">
User will no longer have access to the project
</p>
</div> </div>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="destructive" size="sm"> <Button
variant="destructive"
size="sm"
disabled={isDeleting}
>
{isDeleting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
Delete user Delete user
</>
)}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will permanently delete the user account and remove their This action cannot be undone. This will permanently
data from our servers. delete the user account and remove their data from our
servers.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => { onClick={handleDeleteUser}
setIsDeleting(true)
deleteUserMutation.mutate()
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isDeleting}
> >
{isDeleting ? ( {isDeleting ? (
<> <>
@ -377,6 +420,5 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
</SheetFooter> */} </SheetFooter> */}
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) );
} }

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useMemo } from "react"; import { useState, useMemo, useEffect } from "react";
import { import {
PlusCircle, PlusCircle,
Search, Search,
@ -38,6 +38,8 @@ import { DataTable } from "./data-table";
import { InviteUserDialog } from "./invite-user"; import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog"; import { AddUserDialog } from "./add-user-dialog";
import { UserDetailsSheet } from "./sheet"; import { UserDetailsSheet } from "./sheet";
import { Avatar } from "@radix-ui/react-avatar";
import Image from "next/image";
export default function UserManagement() { export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -62,21 +64,35 @@ export default function UserManagement() {
}); });
// Use React Query to fetch users // Use React Query to fetch users
const { const [users, setUsers] = useState<User[]>([]);
data: users = [], const [isLoading, setIsLoading] = useState(true);
isLoading,
refetch, useEffect(() => {
} = useQuery({ const fetchData = async () => {
queryKey: ["users"],
queryFn: async () => {
try { try {
return await fetchUsers(); const fetchedUsers = await fetchUsers();
setUsers(fetchedUsers);
} catch (error) { } catch (error) {
toast.error("Failed to fetch users"); toast.error("Failed to fetch users");
return []; } 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 handleUserClick = (user: User) => { const handleUserClick = (user: User) => {
setSelectedUser(user); setSelectedUser(user);
@ -203,7 +219,6 @@ export default function UserManagement() {
id: "email", id: "email",
header: ({ column }: any) => ( header: ({ column }: any) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>Email</span> <span>Email</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -234,9 +249,19 @@ export default function UserManagement() {
), ),
cell: ({ row }: { row: { original: User } }) => ( cell: ({ row }: { row: { original: User } }) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium"> <Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
{row.original.email?.[0]?.toUpperCase() || "?"} {row.original.profile?.avatar ? (
</div> <Image
src={row.original.profile.avatar}
alt="Avatar"
className="w-full h-full rounded-full"
width={32}
height={32}
/>
) : (
row.original.email?.[0]?.toUpperCase() || "?"
)}
</Avatar>
<div> <div>
<div className="font-medium"> <div className="font-medium">
{row.original.email || "No email"} {row.original.email || "No email"}
@ -252,7 +277,6 @@ export default function UserManagement() {
id: "phone", id: "phone",
header: ({ column }: any) => ( header: ({ column }: any) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>Phone</span> <span>Phone</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -287,7 +311,6 @@ export default function UserManagement() {
id: "lastSignIn", id: "lastSignIn",
header: ({ column }: any) => ( header: ({ column }: any) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>Last Sign In</span> <span>Last Sign In</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -360,7 +383,6 @@ export default function UserManagement() {
id: "createdAt", id: "createdAt",
header: ({ column }: any) => ( header: ({ column }: any) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>Created At</span> <span>Created At</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -420,7 +442,6 @@ export default function UserManagement() {
id: "status", id: "status",
header: ({ column }: any) => ( header: ({ column }: any) => (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>Status</span> <span>Status</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.pexels.com",
},
],
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -132,10 +132,11 @@ model geographics {
model profiles { model profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid user_id String @unique @db.Uuid
bio String? avatar String? @db.VarChar(255)
address String? @db.VarChar(255) first_name String? @db.VarChar(255)
city String? @db.VarChar(100) last_name String? @db.VarChar(255)
country String? @db.VarChar(100) bio String? @db.VarChar
address Json? @db.Json
birth_date DateTime? birth_date DateTime?
users users @relation(fields: [user_id], references: [id]) users users @relation(fields: [user_id], references: [id])

View File

@ -1,43 +1,53 @@
export interface User { export interface User {
id: string id: string;
email?: string email?: string;
phone?: string phone?: string;
created_at: string created_at: string;
updated_at: string updated_at: string;
last_sign_in_at?: string last_sign_in_at?: string;
email_confirmed_at?: string email_confirmed_at?: string;
phone_confirmed_at?: string phone_confirmed_at?: string;
invited_at?: string invited_at?: string;
confirmation_sent_at?: string confirmation_sent_at?: string;
banned_until?: string banned_until?: string;
factors?: { factors?: {
id: string id: string;
factor_type: string factor_type: string;
created_at: string created_at: string;
updated_at: string updated_at: string;
}[] }[];
raw_user_meta_data?: Record<string, any> raw_user_meta_data?: Record<string, any>;
raw_app_meta_data?: Record<string, any> raw_app_meta_data?: Record<string, any>;
} profile?: Profile;
}
export interface CreateUserParams { export interface CreateUserParams {
email: string email: string;
password: string password: string;
phone?: string phone?: string;
user_metadata?: Record<string, any> user_metadata?: Record<string, any>;
email_confirm?: boolean email_confirm?: boolean;
} }
export interface UpdateUserParams { export interface UpdateUserParams {
email?: string email?: string;
phone?: string phone?: string;
password?: string password?: string;
user_metadata?: Record<string, any> user_metadata?: Record<string, any>;
} }
export interface InviteUserParams {
email: string
user_metadata?: Record<string, any>
}
export interface InviteUserParams {
email: string;
user_metadata?: Record<string, any>;
}
export interface Profile {
id: string;
user_id: string;
avatar?: string;
first_name?: string;
last_name?: string;
bio: string;
address?: string;
birthdate?: string;
}