update users stat, user sheet, dan action user

This commit is contained in:
vergiLgood1 2025-02-28 22:06:43 +07:00
parent 681517e28e
commit dd24481574
11 changed files with 768 additions and 542 deletions

View File

@ -60,7 +60,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 position="top-right" /> <Toaster theme="system" richColors 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">

View File

@ -112,7 +112,7 @@ export async function banUser(userId: string): Promise<User> {
banUntil.setFullYear(banUntil.getFullYear() + 100) banUntil.setFullYear(banUntil.getFullYear() + 100)
const { data, error } = await supabase.auth.admin.updateUserById(userId, { const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "100y", ban_duration: "100h",
}) })
if (error) { if (error) {

View File

@ -1,156 +1,165 @@
"use client" "use client";
import { useState } from "react" import type React from "react";
import { useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input";
import { Input } from "@/components/ui/input" import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea" import { createUser } from "@/app/protected/(admin)/dashboard/user-management/action";
import { Switch } from "@/components/ui/switch" import { toast } from "sonner";
import { createUser } from "@/app/protected/(admin)/dashboard/user-management/action" import { Mail, Lock, Loader2, X } from "lucide-react";
import { toast } from "sonner"
interface AddUserDialogProps { interface AddUserDialogProps {
open: boolean open: boolean;
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void;
onUserAdded: () => void onUserAdded: () => void;
} }
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) { export function AddUserDialog({
const [loading, setLoading] = useState(false) open,
onOpenChange,
onUserAdded,
}: AddUserDialogProps) {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: "",
password: "", password: "",
phone: "",
metadata: "{}",
emailConfirm: true, emailConfirm: true,
}) });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value })) setFormData((prev) => ({ ...prev, [name]: value }));
} };
const handleSwitchChange = (checked: boolean) => {
setFormData((prev) => ({ ...prev, emailConfirm: checked }))
}
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
setLoading(true) setLoading(true);
try { try {
let metadata = {}
try {
metadata = JSON.parse(formData.metadata)
} catch (error) {
toast.error("Invalid JSON. Please check your metadata format.")
setLoading(false)
return
}
await createUser({ await createUser({
email: formData.email, email: formData.email,
password: formData.password, password: formData.password,
phone: formData.phone,
user_metadata: metadata,
email_confirm: formData.emailConfirm, email_confirm: formData.emailConfirm,
}) });
toast.success("User created successfully.") toast.success("User created successfully.");
onUserAdded() onUserAdded();
onOpenChange(false) onOpenChange(false);
setFormData({ setFormData({
email: "", email: "",
password: "", password: "",
phone: "",
metadata: "{}",
emailConfirm: true, emailConfirm: true,
}) });
} catch (error) { } catch (error) {
toast.error("Failed to create user.") toast.error("Failed to create user.");
} finally { } finally {
setLoading(false) setLoading(false);
}
} }
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md border-0 text-white">
<DialogHeader> <DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
<DialogTitle>Add User</DialogTitle> <DialogTitle className="text-xl font-semibold text-white">
<DialogDescription> Create a new user
Create a new user account with email and password. </DialogTitle>
</DialogDescription> {/* <Button
variant="ghost"
size="icon"
className="h-8 w-8 text-zinc-400 hover:text-white hover:bg-zinc-800"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button> */}
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">Email *</Label> <label htmlFor="email" className="text-sm text-zinc-400">
Email address
</label>
<div className="relative">
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
<Input <Input
id="email" id="email"
name="email" name="email"
type="email" type="email"
required required
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 "
/> />
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Password *</Label> <label htmlFor="password" className="text-sm text-zinc-400">
User Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
<Input <Input
id="password" id="password"
name="password" name="password"
type="password" type="password"
required required
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 "
/> />
</div> </div>
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="metadata">Metadata (JSON)</Label>
<Textarea
id="metadata"
name="metadata"
value={formData.metadata}
onChange={handleInputChange}
className="font-mono text-sm"
/>
</div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Checkbox
id="email-confirm" id="email-confirm"
checked={formData.emailConfirm} checked={formData.emailConfirm}
onCheckedChange={handleSwitchChange} onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
emailConfirm: checked as boolean,
}))
}
className="border-zinc-700"
/> />
<Label htmlFor="email-confirm">Auto-confirm email</Label> <label htmlFor="email-confirm" className="text-sm text-white">
Auto Confirm User?
</label>
</div> </div>
<DialogFooter> <p className="text-sm text-zinc-500 pl-6">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}> A confirmation email will not be sent when creating a user via
Cancel this form.
</p>
</div>
<Button
type="submit"
disabled={loading}
className="w-full text-white"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create user"
)}
</Button> </Button>
<Button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create User"}
</Button>
</DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }

View File

@ -10,19 +10,22 @@ import {
type SortingState, type SortingState,
getFilteredRowModel, getFilteredRowModel,
type ColumnFiltersState, type ColumnFiltersState,
getPaginationRowModel,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Filter } from "lucide-react" import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Filter } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton" 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
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
@ -30,10 +33,15 @@ export function DataTable<TData, TValue>({
data, data,
loading = false, loading = false,
onRowClick, onRowClick,
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({
pageIndex: 0,
pageSize: pageSize,
})
const table = useReactTable({ const table = useReactTable({
data, data,
@ -44,10 +52,13 @@ export function DataTable<TData, TValue>({
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: { state: {
sorting, sorting,
columnFilters, columnFilters,
columnVisibility, columnVisibility,
pagination,
}, },
}) })
@ -67,7 +78,7 @@ export function DataTable<TData, TValue>({
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{Array.from({ length: 5 }).map((_, index) => ( {Array.from({ length: pagination.pageSize }).map((_, index) => (
<TableRow key={index}> <TableRow key={index}>
{columns.map((_, colIndex) => ( {columns.map((_, colIndex) => (
<TableCell key={colIndex}> <TableCell key={colIndex}>
@ -158,6 +169,83 @@ export function DataTable<TData, TValue>({
)} )}
</TableBody> </TableBody>
</Table> </Table>
{/* Pagination Controls */}
<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>
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{" "}
{Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length,
)}{" "}
of {table.getFilteredRowModel().rows.length} entries
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-1 text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</div>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -1,8 +1,8 @@
"use client" "use client";
import type React from "react" import type React from "react";
import { useState } from "react" import { useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -10,72 +10,80 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea";
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query";
import { inviteUser } from "@/app/protected/(admin)/dashboard/user-management/action" import { inviteUser } from "@/app/protected/(admin)/dashboard/user-management/action";
import { toast } from "sonner" import { toast } from "sonner";
interface InviteUserDialogProps { interface InviteUserDialogProps {
open: boolean open: boolean;
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void;
onUserInvited: () => void onUserInvited: () => void;
} }
export function InviteUserDialog({ open, onOpenChange, onUserInvited }: InviteUserDialogProps) { export function InviteUserDialog({
open,
onOpenChange,
onUserInvited,
}: InviteUserDialogProps) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: "",
metadata: "{}", metadata: "{}",
}) });
const inviteUserMutation = useMutation({ const inviteUserMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
let metadata = {} let metadata = {};
try { try {
metadata = JSON.parse(formData.metadata) metadata = JSON.parse(formData.metadata);
} catch (error) { } catch (error) {
toast.error("Invalid JSON. Please check your metadata format.") toast.error("Invalid JSON. Please check your metadata format.");
throw new Error("Invalid JSON") throw new Error("Invalid JSON");
} }
return inviteUser({ return inviteUser({
email: formData.email, email: formData.email,
user_metadata: metadata, user_metadata: metadata,
}) });
}, },
onSuccess: () => { onSuccess: () => {
toast.success("Invitation sent") toast.success("Invitation sent");
onUserInvited() onUserInvited();
onOpenChange(false) onOpenChange(false);
setFormData({ setFormData({
email: "", email: "",
metadata: "{}", metadata: "{}",
}) });
}, },
onError: () => { onError: () => {
toast.error("Failed to send invitation") toast.error("Failed to send invitation");
}, },
}) });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleInputChange = (
const { name, value } = e.target e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
setFormData((prev) => ({ ...prev, [name]: value })) ) => {
} const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
inviteUserMutation.mutate() inviteUserMutation.mutate();
} };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Invite User</DialogTitle> <DialogTitle>Invite User</DialogTitle>
<DialogDescription>Send an invitation email to a new user.</DialogDescription> <DialogDescription>
Send an invitation email to a new user.
</DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@ -87,20 +95,16 @@ export function InviteUserDialog({ open, onOpenChange, onUserInvited }: InviteUs
required required
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="example@gmail.com"
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="invite-metadata">Metadata (JSON)</Label>
<Textarea
id="invite-metadata"
name="metadata"
value={formData.metadata}
onChange={handleInputChange}
className="font-mono text-sm"
/>
</div>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}> <Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={inviteUserMutation.isPending}> <Button type="submit" disabled={inviteUserMutation.isPending}>
@ -110,6 +114,5 @@ export function InviteUserDialog({ open, onOpenChange, onUserInvited }: InviteUs
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }

View File

@ -1,20 +1,12 @@
"use client" "use client"
import type React from "react"
import { useState } from "react" import { useState } from "react"
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner"
import { Sheet, SheetContent, 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,
@ -26,59 +18,69 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { updateUser, deleteUser, sendPasswordRecovery, sendMagicLink, banUser, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action" import { Mail, Trash2, Ban, SendHorizonal, CheckCircle, XCircle, Copy, Loader2 } from "lucide-react"
import { User } from "@/src/models/users/users.model" import { banUser, deleteUser, sendMagicLink, sendPasswordRecovery, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action"
import { toast } from "sonner"
interface UserSheetProps { // // Mock functions (replace with your actual API calls)
user: User // const updateUser = async (id: string, data: any) => {
// console.log(`Updating user ${id} with data:`, data)
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { id, ...data }
// }
// const deleteUser = async (id: string) => {
// console.log(`Deleting user ${id}`)
// await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate API call
// return { success: true }
// }
// const sendPasswordRecovery = async (email: string) => {
// console.log(`Sending password recovery email to ${email}`)
// 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 {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
user: any
onUserUpdate: () => void onUserUpdate: () => void
} }
export function UserSheet({ user, open, onOpenChange, onUserUpdate }: UserSheetProps) { export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: UserDetailsSheetProps) {
const [isDeleting, setIsDeleting] = useState(false)
const [formData, setFormData] = useState({
email: user.email || "",
phone: user.phone || "",
metadata: JSON.stringify(user.raw_user_meta_data || {}, null, 2),
})
const updateUserMutation = useMutation({
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 updateUser(user.id, {
email: formData.email,
phone: formData.phone,
user_metadata: metadata,
})
},
onSuccess: () => {
toast.success("User updated successfully")
onUserUpdate()
},
onError: () => {
toast.error("Failed to update user")
},
})
const deleteUserMutation = useMutation({ const deleteUserMutation = useMutation({
mutationFn: () => deleteUser(user.id), mutationFn: () => deleteUser(user.id),
onSuccess: () => { onSuccess: () => {
toast.success("User deleted successfully") toast.success("User deleted successfully")
onUserUpdate() onUserUpdate()
onOpenChange(false)
}, },
onError: () => { onError: () => {
toast.error("Failed to delete user") toast.error("Failed to delete user")
}, },
onSettled: () => {
setIsDeleting(false)
},
}) })
const sendPasswordRecoveryMutation = useMutation({ const sendPasswordRecoveryMutation = useMutation({
@ -128,189 +130,247 @@ export function UserSheet({ user, open, onOpenChange, onUserUpdate }: UserSheetP
}, },
}) })
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
return ( return (
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md md:max-w-lg overflow-y-auto"> <SheetContent className="sm:max-w-md md:max-w-lg overflow-y-auto">
<SheetHeader className="space-y-1"> <SheetHeader className="space-y-1">
<SheetTitle className="text-xl flex items-center gap-2"> <SheetTitle className="text-xl flex items-center gap-2">
User Details {user.email}
<Button
variant="ghost"
size="icon"
className="h-4 w-4"
onClick={() => {
navigator.clipboard.writeText(user.email)
// Optionally add a toast notification here
}}
>
<Copy className="h-4 w-4" />
</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 && <Badge variant="outline">Unconfirmed</Badge>}
{!user.banned_until && user.email_confirmed_at && <Badge variant="default">Active</Badge>} {!user.banned_until && user.email_confirmed_at && <Badge variant="default">Active</Badge>}
</SheetTitle> </SheetTitle>
<SheetDescription>ID: {user.id}</SheetDescription>
</SheetHeader> </SheetHeader>
<Tabs defaultValue="details" className="mt-6"> <div className="mt-6 space-y-8">
<TabsList className="grid grid-cols-3 mb-4"> {/* User Information Section */}
<TabsTrigger value="details">Details</TabsTrigger> <div className="space-y-4">
<TabsTrigger value="security">Security</TabsTrigger> <h3 className="text-lg font-semibold">User Information</h3>
<TabsTrigger value="actions">Actions</TabsTrigger>
</TabsList>
<TabsContent value="details" className="space-y-4"> <div className="space-y-2 text-sm">
<div className="space-y-2"> <div className="flex justify-between items-center py-1">
<Label htmlFor="email">Email</Label> <span className="text-muted-foreground">User UID</span>
<Input id="email" name="email" value={formData.email} onChange={handleInputChange} /> <div className="flex items-center">
</div> <span className="font-mono">{user.id}</span>
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input id="phone" name="phone" value={formData.phone} onChange={handleInputChange} />
</div>
<div className="space-y-2">
<Label htmlFor="metadata">Metadata (JSON)</Label>
<Textarea
id="metadata"
name="metadata"
value={formData.metadata}
onChange={handleInputChange}
className="font-mono text-sm h-40"
/>
</div>
<div className="space-y-2">
<Label>Created At</Label>
<div className="text-sm text-muted-foreground">{new Date(user.created_at).toLocaleString()}</div>
</div>
<div className="space-y-2">
<Label>Last Sign In</Label>
<div className="text-sm text-muted-foreground">
{user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : "Never"}
</div>
</div>
<Button <Button
onClick={() => updateUserMutation.mutate()} variant="ghost"
disabled={updateUserMutation.isPending} size="icon"
className="w-full" className="h-4 w-4 ml-2"
onClick={() => {
navigator.clipboard.writeText(user.id)
// Optionally add a toast notification here
}}
> >
{updateUserMutation.isPending ? "Saving..." : "Save Changes"} <Copy className="h-4 w-4" />
</Button> </Button>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="email-confirmed">Email Confirmed</Label>
<Switch id="email-confirmed" checked={!!user.email_confirmed_at} disabled />
</div> </div>
{user.email_confirmed_at && (
<div className="text-xs text-muted-foreground">
Confirmed at: {new Date(user.email_confirmed_at).toLocaleString()}
</div>
)}
</div> </div>
<div className="space-y-2"> <div className="flex justify-between items-center py-1">
<div className="flex items-center justify-between"> <span className="text-muted-foreground">Created at</span>
<Label htmlFor="phone-confirmed">Phone Confirmed</Label> <span>{new Date(user.created_at).toLocaleString()}</span>
<Switch id="phone-confirmed" checked={!!user.phone_confirmed_at} disabled />
</div>
{user.phone_confirmed_at && (
<div className="text-xs text-muted-foreground">
Confirmed at: {new Date(user.phone_confirmed_at).toLocaleString()}
</div>
)}
</div> </div>
<Separator /> <div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Updated at</span>
<div className="space-y-2"> <span>{new Date(user.updated_at || user.created_at).toLocaleString()}</span>
<Label>Authentication Factors</Label> </div>
<div className="text-sm text-muted-foreground">
{user.factors?.length <div className="flex justify-between items-center py-1">
? user.factors.map((factor, i) => ( <span className="text-muted-foreground">Invited at</span>
<div key={i} className="flex items-center gap-2"> <span>{user.invited_at ? new Date(user.invited_at).toLocaleString() : "-"}</span>
<Badge variant="outline">{factor.factor_type}</Badge> </div>
<span>{new Date(factor.created_at).toLocaleString()}</span>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Confirmation sent at</span>
<span>{user.confirmation_sent_at ? new Date(user.confirmation_sent_at).toLocaleString() : "-"}</span>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Confirmed at</span>
<span>{user.email_confirmed_at ? new Date(user.email_confirmed_at).toLocaleString() : "-"}</span>
</div>
<div className="flex justify-between items-center py-1">
<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>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">SSO</span>
<XCircle className="h-4 w-4 text-muted-foreground" />
</div> </div>
))
: "No authentication factors"}
</div> </div>
</div> </div>
<Separator /> <Separator />
<div className="space-y-2"> {/* Provider Information Section */}
<Label>Password Reset</Label> <div className="space-y-4">
<h3 className="text-lg font-semibold">Provider Information</h3>
<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="flex items-center justify-between">
<div className="flex items-center gap-2">
<Mail className="h-5 w-5" />
<div>
<div className="font-medium">Email</div>
<div className="text-xs text-muted-foreground">Signed in with a email account via OAuth</div>
</div>
</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
</Badge>
</div>
</div>
<div className="border rounded-md p-4 space-y-4">
<div className="flex justify-between items-center">
<div>
<h4 className="font-medium">Reset password</h4>
<p className="text-xs text-muted-foreground">Send a password recovery email to the user</p>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm"
onClick={() => sendPasswordRecoveryMutation.mutate()} onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={sendPasswordRecoveryMutation.isPending || !user.email} disabled={sendPasswordRecoveryMutation.isPending || !user.email}
className="w-full"
> >
Send Password Recovery Email {sendPasswordRecoveryMutation.isPending ? (
</Button> <>
</div> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
<div className="space-y-2"> </>
<Label>Magic Link</Label> ) : (
<Button <>
variant="outline" <Mail className="h-4 w-4 mr-2" />
onClick={() => sendMagicLinkMutation.mutate()} Send password recovery
disabled={sendMagicLinkMutation.isPending || !user.email} </>
className="w-full"
>
Send Magic Link
</Button>
</div>
</TabsContent>
<TabsContent value="actions" className="space-y-4">
<div className="space-y-2">
<Label>Ban User</Label>
<Button
variant={user.banned_until ? "default" : "destructive"}
onClick={() => toggleBanMutation.mutate()}
disabled={toggleBanMutation.isPending}
className="w-full"
>
{user.banned_until ? "Unban User" : "Ban User"}
</Button>
{user.banned_until && (
<div className="text-xs text-muted-foreground">
Banned until: {new Date(user.banned_until).toLocaleString()}
</div>
)} )}
</Button>
</div> </div>
<Separator /> <Separator />
<div className="space-y-2"> <div className="flex justify-between items-center">
<Label>Delete User</Label> <div>
<h4 className="font-medium">Send magic link</h4>
<p className="text-xs text-muted-foreground">Passwordless login via email for the user</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => sendMagicLinkMutation.mutate()}
disabled={sendMagicLinkMutation.isPending || !user.email}
>
{sendMagicLinkMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<SendHorizonal className="h-4 w-4 mr-2" />
Send magic link
</>
)}
</Button>
</div>
</div>
</div>
<Separator />
{/* Danger Zone Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-destructive">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="border border-destructive/20 rounded-md p-4 flex justify-between items-center">
<div>
<h4 className="font-medium">Ban user</h4>
<p className="text-xs text-muted-foreground">Revoke access to the project for a set duration</p>
</div>
<Button
variant={user.banned_until ? "outline" : "outline"}
size="sm"
onClick={() => toggleBanMutation.mutate()}
disabled={toggleBanMutation.isPending}
>
{toggleBanMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{user.banned_until ? "Unbanning..." : "Banning..."}
</>
) : (
<>
<Ban className="h-4 w-4 mr-2" />
{user.banned_until ? "Unban user" : "Ban user"}
</>
)}
</Button>
</div>
<div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center">
<div>
<h4 className="font-medium">Delete user</h4>
<p className="text-xs text-muted-foreground">User will no longer have access to the project</p>
</div>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="destructive" className="w-full"> <Button variant="destructive" size="sm">
Delete User <Trash2 className="h-4 w-4 mr-2" />
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 data This action cannot be undone. This will permanently delete the user account and remove their
from our servers. data from our servers.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => deleteUserMutation.mutate()} onClick={() => {
setIsDeleting(true)
deleteUserMutation.mutate()
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
> >
Delete {isDeleting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
"Delete"
)}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
</TabsContent> </div>
</Tabs> </div>
</div>
<SheetFooter className="mt-4"> <SheetFooter className="mt-6">
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)}>
Close Close
</Button> </Button>

View File

@ -1,67 +1,78 @@
"use client" "use client";
import { useState } from "react" import { useState } from "react";
import { PlusCircle, Search, Filter, MoreHorizontal, X, ChevronDown } from 'lucide-react' import {
import { Button } from "@/components/ui/button" PlusCircle,
import { Input } from "@/components/ui/input" Search,
import { Badge } from "@/components/ui/badge" Filter,
MoreHorizontal,
X,
ChevronDown,
UserPlus,
Mail,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu";
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action" import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action";
import { User } from "@/src/models/users/users.model" import { User } from "@/src/models/users/users.model";
import { toast } from "sonner" import { toast } from "sonner";
import { DataTable } from "./data-table" import { DataTable } from "./data-table";
import { UserSheet } from "./sheet" 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";
export default function UserManagement() { export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("");
const [selectedUser, setSelectedUser] = useState<User | null>(null) const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false) const [isSheetOpen, setIsSheetOpen] = useState(false);
const [isAddUserOpen, setIsAddUserOpen] = useState(false) const [isAddUserOpen, setIsAddUserOpen] = useState(false);
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false) const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
// Use React Query to fetch users // Use React Query to fetch users
const { data: users = [], isLoading, refetch } = useQuery({ const {
queryKey: ['users'], data: users = [],
isLoading,
refetch,
} = useQuery({
queryKey: ["users"],
queryFn: async () => { queryFn: async () => {
try { try {
return await fetchUsers() return await fetchUsers();
} catch (error) { } catch (error) {
toast.error("Failed to fetch users") toast.error("Failed to fetch users");
return [] return [];
} }
} },
}) });
const handleUserClick = (user: User) => { const handleUserClick = (user: User) => {
setSelectedUser(user) setSelectedUser(user);
setIsSheetOpen(true) setIsSheetOpen(true);
} };
const handleUserUpdate = () => { const handleUserUpdate = () => {
refetch() refetch();
setIsSheetOpen(false) setIsSheetOpen(false);
} };
const filteredUsers = users.filter((user) => { const filteredUsers = users.filter((user) => {
if (!searchQuery) return true if (!searchQuery) return true;
const query = searchQuery.toLowerCase() const query = searchQuery.toLowerCase();
return ( return (
user.email?.toLowerCase().includes(query) || user.email?.toLowerCase().includes(query) ||
user.phone?.toLowerCase().includes(query) || user.phone?.toLowerCase().includes(query) ||
user.id.toLowerCase().includes(query) user.id.toLowerCase().includes(query)
) );
}) });
const columns = [ const columns = [
{ {
@ -73,13 +84,17 @@ export default function UserManagement() {
{row.original.email?.[0]?.toUpperCase() || "?"} {row.original.email?.[0]?.toUpperCase() || "?"}
</div> </div>
<div> <div>
<div className="font-medium">{row.original.email || "No email"}</div> <div className="font-medium">
<div className="text-xs text-muted-foreground">{row.original.id}</div> {row.original.email || "No email"}
</div>
<div className="text-xs text-muted-foreground">
{row.original.id}
</div>
</div> </div>
</div> </div>
), ),
filterFn: (row: any, id: string, value: string) => { filterFn: (row: any, id: string, value: string) => {
return row.original.email?.toLowerCase().includes(value.toLowerCase()) return row.original.email?.toLowerCase().includes(value.toLowerCase());
}, },
}, },
{ {
@ -87,7 +102,7 @@ export default function UserManagement() {
header: "Phone", header: "Phone",
cell: ({ row }: { row: { original: User } }) => row.original.phone || "-", cell: ({ row }: { row: { original: User } }) => row.original.phone || "-",
filterFn: (row: any, id: string, value: string) => { filterFn: (row: any, id: string, value: string) => {
return row.original.phone?.toLowerCase().includes(value.toLowerCase()) return row.original.phone?.toLowerCase().includes(value.toLowerCase());
}, },
}, },
{ {
@ -96,14 +111,14 @@ export default function UserManagement() {
cell: ({ row }: { row: { original: User } }) => { cell: ({ row }: { row: { original: User } }) => {
return row.original.last_sign_in_at return row.original.last_sign_in_at
? new Date(row.original.last_sign_in_at).toLocaleString() ? new Date(row.original.last_sign_in_at).toLocaleString()
: "Never" : "Never";
}, },
}, },
{ {
id: "createdAt", id: "createdAt",
header: "Created At", header: "Created At",
cell: ({ row }: { row: { original: User } }) => { cell: ({ row }: { row: { original: User } }) => {
return new Date(row.original.created_at).toLocaleString() return new Date(row.original.created_at).toLocaleString();
}, },
}, },
{ {
@ -111,35 +126,39 @@ export default function UserManagement() {
header: "Status", header: "Status",
cell: ({ row }: { row: { original: User } }) => { cell: ({ row }: { row: { original: User } }) => {
if (row.original.banned_until) { if (row.original.banned_until) {
return <Badge variant="destructive">Banned</Badge> return <Badge variant="destructive">Banned</Badge>;
} }
if (!row.original.email_confirmed_at) { if (!row.original.email_confirmed_at) {
return <Badge variant="outline">Unconfirmed</Badge> return <Badge variant="outline">Unconfirmed</Badge>;
} }
return <Badge variant="default">Active</Badge> return <Badge variant="default">Active</Badge>;
}, },
filterFn: (row: any, id: string, value: string) => { filterFn: (row: any, id: string, value: string) => {
const status = row.original.banned_until const status = row.original.banned_until
? "banned" ? "banned"
: !row.original.email_confirmed_at : !row.original.email_confirmed_at
? "unconfirmed" ? "unconfirmed"
: "active" : "active";
return status.includes(value.toLowerCase()) return status.includes(value.toLowerCase());
}, },
}, },
{ {
id: "actions", id: "actions",
header: "", header: "",
cell: ({ row }: { row: { original: User } }) => ( cell: ({ row }: { row: { original: User } }) => (
<Button variant="ghost" size="icon" onClick={(e) => { <Button
e.stopPropagation() variant="ghost"
handleUserClick(row.original) size="icon"
}}> onClick={(e) => {
e.stopPropagation();
handleUserClick(row.original);
}}
>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button> </Button>
), ),
}, },
] ];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -174,10 +193,12 @@ export default function UserManagement() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsAddUserOpen(true)}> <DropdownMenuItem onClick={() => setIsAddUserOpen(true)}>
Add User <UserPlus className="h-4 w-4 mr-2" />
Create new user
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsInviteUserOpen(true)}> <DropdownMenuItem onClick={() => setIsInviteUserOpen(true)}>
Invite User <Mail className="h-4 w-4 mr-2" />
Send invitation
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -195,7 +216,7 @@ export default function UserManagement() {
/> />
{selectedUser && ( {selectedUser && (
<UserSheet <UserDetailsSheet
user={selectedUser} user={selectedUser}
open={isSheetOpen} open={isSheetOpen}
onOpenChange={setIsSheetOpen} onOpenChange={setIsSheetOpen}
@ -215,5 +236,5 @@ export default function UserManagement() {
onUserInvited={() => refetch()} onUserInvited={() => refetch()}
/> />
</div> </div>
) );
} }

View File

@ -1,45 +1,88 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" "use client"
import { Users, UserCheck, UserX } from 'lucide-react'
import { useQuery } from "@tanstack/react-query"
import { Card, CardContent } from "@/components/ui/card"
import { Users, UserCheck, UserX } from "lucide-react"
import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action"
import { User } from "@/src/models/users/users.model"
function calculateUserStats(users: User[]) {
const totalUsers = users.length
const activeUsers = users.filter((user) => !user.banned_until && user.email_confirmed_at).length
const inactiveUsers = totalUsers - activeUsers
return {
totalUsers,
activeUsers,
inactiveUsers,
activePercentage: totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
inactivePercentage: totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
}
}
export function UserStats() { export function UserStats() {
const { data: users = [], isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
})
const stats = calculateUserStats(users)
if (isLoading) {
return ( return (
<> <>
<Card> {[...Array(3)].map((_, i) => (
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <Card key={i} className="bg-background border-border">
<CardTitle className="text-sm font-medium">Total Users</CardTitle> <CardContent className="p-6">
<Users className="h-4 w-4 text-muted-foreground" /> <div className="space-y-4 animate-pulse">
</CardHeader> <div className="h-5 w-24 bg-muted rounded" />
<CardContent> <div className="h-8 w-16 bg-muted rounded" />
<div className="text-2xl font-bold">1,234</div> <div className="h-4 w-32 bg-muted rounded" />
<p className="text-xs text-muted-foreground"> </div>
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Users</CardTitle>
<UserCheck className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,105</div>
<p className="text-xs text-muted-foreground">
89.5% of total users
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Inactive Users</CardTitle>
<UserX className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">129</div>
<p className="text-xs text-muted-foreground">
10.5% of total users
</p>
</CardContent> </CardContent>
</Card> </Card>
))}
</> </>
) )
} }
const cards = [
{
title: "Total Users",
value: stats.totalUsers,
subtitle: "Updated just now",
icon: Users,
},
{
title: "Active Users",
value: stats.activeUsers,
subtitle: `${stats.activePercentage}% of total users`,
icon: UserCheck,
},
{
title: "Inactive Users",
value: stats.inactiveUsers,
subtitle: `${stats.inactivePercentage}% of total users`,
icon: UserX,
},
]
return (
<>
{cards.map((card, index) => (
<Card key={index} className="bg-background border-border">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="font-medium text-sm text-muted-foreground">{card.title}</div>
<card.icon className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-3xl font-bold mb-2">{card.value}</div>
<div className="text-sm text-muted-foreground">{card.subtitle}</div>
</CardContent>
</Card>
))}
</>
)
}

View File

@ -1,36 +1,36 @@
"use client" // "use client"
import { useState } from "react" // import { useState } from "react"
import { DataTable } from "./data-table" // import { DataTable } from "./data-table"
import { columns, User } from "./column" // import { columns, User } from "./column"
import { useQuery } from "react-query" // import { useQuery } from "react-query"
import { getUsers } from "../../user-management/action" // import { getUsers } from "../../user-management/action"
import { UserDetailSheet } from "./sheet" // import { UserDetailSheet } from "./sheet"
export function UsersTable() { // export function UsersTable() {
const [selectedUser, setSelectedUser] = useState<User | null>(null) // const [selectedUser, setSelectedUser] = useState<User | null>(null)
const [sheetOpen, setSheetOpen] = useState(false) // const [sheetOpen, setSheetOpen] = useState(false)
const { data: users = [], isLoading } = useQuery({ // const { data: users = [], isLoading } = useQuery({
queryKey: ["users"], // queryKey: ["users"],
queryFn: getUsers, // queryFn: getUsers,
}) // })
const handleRowClick = (user: User) => { // const handleRowClick = (user: User) => {
setSelectedUser(user) // setSelectedUser(user)
setSheetOpen(true) // setSheetOpen(true)
} // }
if (isLoading) { // if (isLoading) {
return <div>Loading...</div> // return <div>Loading...</div>
} // }
return ( // return (
<div className="w-full"> // <div className="w-full">
<DataTable columns={columns} data={users} onRowClick={handleRowClick} /> // <DataTable columns={columns} data={users} onRowClick={handleRowClick} />
{selectedUser && <UserDetailSheet user={selectedUser} open={sheetOpen} onOpenChange={setSheetOpen} />} // {selectedUser && <UserDetailSheet user={selectedUser} open={sheetOpen} onOpenChange={setSheetOpen} />}
</div> // </div>
) // )
} // }

View File

@ -1,18 +1,18 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react" import { X } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -64,8 +64,8 @@ const DialogHeader = ({
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -78,8 +78,8 @@ const DialogFooter = ({
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@ -119,4 +119,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} };

View File

@ -7,6 +7,8 @@ export interface User {
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
confirmation_sent_at?: string
banned_until?: string banned_until?: string
factors?: { factors?: {
id: string id: string