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> */}
<div className="flex flex-col max-w-full p-0">
{children}
<Toaster position="top-right" />
<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">

View File

@ -112,7 +112,7 @@ export async function banUser(userId: string): Promise<User> {
banUntil.setFullYear(banUntil.getFullYear() + 100)
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "100y",
ban_duration: "100h",
})
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 {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
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 { createUser } from "@/app/protected/(admin)/dashboard/user-management/action"
import { toast } from "sonner"
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/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";
interface AddUserDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onUserAdded: () => void
open: boolean;
onOpenChange: (open: boolean) => void;
onUserAdded: () => void;
}
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) {
const [loading, setLoading] = useState(false)
export function AddUserDialog({
open,
onOpenChange,
onUserAdded,
}: AddUserDialogProps) {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
email: "",
password: "",
phone: "",
metadata: "{}",
emailConfirm: true,
})
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleSwitchChange = (checked: boolean) => {
setFormData((prev) => ({ ...prev, emailConfirm: checked }))
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
e.preventDefault();
setLoading(true);
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({
email: formData.email,
password: formData.password,
phone: formData.phone,
user_metadata: metadata,
email_confirm: formData.emailConfirm,
})
});
toast.success("User created successfully.")
onUserAdded()
onOpenChange(false)
toast.success("User created successfully.");
onUserAdded();
onOpenChange(false);
setFormData({
email: "",
password: "",
phone: "",
metadata: "{}",
emailConfirm: true,
})
});
} catch (error) {
toast.error("Failed to create user.")
toast.error("Failed to create user.");
} finally {
setLoading(false)
setLoading(false);
}
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add User</DialogTitle>
<DialogDescription>
Create a new user account with email and password.
</DialogDescription>
<DialogContent className="sm:max-w-md border-0 text-white">
<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">
Create a new user
</DialogTitle>
{/* <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>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3">
<div className="space-y-2">
<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
id="email"
name="email"
type="email"
required
placeholder="user@example.com"
value={formData.email}
onChange={handleInputChange}
className="pl-10 bg-zinc-900 border-zinc-800 text-white placeholder:text-zinc-500 "
/>
</div>
</div>
<div className="space-y-2">
<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
id="password"
name="password"
type="password"
required
placeholder="••••••••"
value={formData.password}
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">
<Label htmlFor="email">Email *</Label>
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
/>
<div className="flex items-center space-x-2">
<Checkbox
id="email-confirm"
checked={formData.emailConfirm}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
emailConfirm: checked as boolean,
}))
}
className="border-zinc-700"
/>
<label htmlFor="email-confirm" className="text-sm text-white">
Auto Confirm User?
</label>
</div>
<p className="text-sm text-zinc-500 pl-6">
A confirmation email will not be sent when creating a user via
this form.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password *</Label>
<Input
id="password"
name="password"
type="password"
required
value={formData.password}
onChange={handleInputChange}
/>
</div>
<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">
<Switch
id="email-confirm"
checked={formData.emailConfirm}
onCheckedChange={handleSwitchChange}
/>
<Label htmlFor="email-confirm">Auto-confirm email</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create User"}
</Button>
</DialogFooter>
<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>
</form>
</DialogContent>
</Dialog>
)
);
}

View File

@ -10,19 +10,22 @@ import {
type SortingState,
getFilteredRowModel,
type ColumnFiltersState,
getPaginationRowModel,
} from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, 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 { Filter } from "lucide-react"
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> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
loading?: boolean
onRowClick?: (row: TData) => void
pageSize?: number
}
export function DataTable<TData, TValue>({
@ -30,10 +33,15 @@ export function DataTable<TData, TValue>({
data,
loading = false,
onRowClick,
pageSize = 5,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useState({})
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: pageSize,
})
const table = useReactTable({
data,
@ -44,10 +52,13 @@ export function DataTable<TData, TValue>({
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
pagination,
},
})
@ -67,7 +78,7 @@ export function DataTable<TData, TValue>({
))}
</TableHeader>
<TableBody>
{Array.from({ length: 5 }).map((_, index) => (
{Array.from({ length: pagination.pageSize }).map((_, index) => (
<TableRow key={index}>
{columns.map((_, colIndex) => (
<TableCell key={colIndex}>
@ -158,6 +169,83 @@ export function DataTable<TData, TValue>({
)}
</TableBody>
</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>
)
}

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

View File

@ -1,20 +1,12 @@
"use client"
import type React 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 { 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 {
AlertDialog,
AlertDialogAction,
@ -26,59 +18,69 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { updateUser, deleteUser, sendPasswordRecovery, sendMagicLink, banUser, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action"
import { User } from "@/src/models/users/users.model"
import { toast } from "sonner"
import { Mail, Trash2, Ban, SendHorizonal, CheckCircle, XCircle, Copy, Loader2 } from "lucide-react"
import { banUser, deleteUser, sendMagicLink, sendPasswordRecovery, unbanUser } from "@/app/protected/(admin)/dashboard/user-management/action"
interface UserSheetProps {
user: User
// // Mock functions (replace with your actual API calls)
// 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
onOpenChange: (open: boolean) => void
user: any
onUserUpdate: () => void
}
export function UserSheet({ user, open, onOpenChange, onUserUpdate }: UserSheetProps) {
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")
},
})
export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: UserDetailsSheetProps) {
const [isDeleting, setIsDeleting] = useState(false)
const deleteUserMutation = useMutation({
mutationFn: () => deleteUser(user.id),
onSuccess: () => {
toast.success("User deleted successfully")
onUserUpdate()
onOpenChange(false)
},
onError: () => {
toast.error("Failed to delete user")
},
onSettled: () => {
setIsDeleting(false)
},
})
const sendPasswordRecoveryMutation = useMutation({
@ -104,7 +106,7 @@ export function UserSheet({ user, open, onOpenChange, onUserUpdate }: UserSheetP
return sendMagicLink(user.email)
},
onSuccess: () => {
toast.success("Magic link sent successfully")
toast.success("Magic link sent successfully")
},
onError: () => {
toast.error("Failed to send magic link")
@ -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 (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md md:max-w-lg overflow-y-auto">
<SheetHeader className="space-y-1">
<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.email_confirmed_at && <Badge variant="outline">Unconfirmed</Badge>}
{!user.banned_until && user.email_confirmed_at && <Badge variant="default">Active</Badge>}
</SheetTitle>
<SheetDescription>ID: {user.id}</SheetDescription>
</SheetHeader>
<Tabs defaultValue="details" className="mt-6">
<TabsList className="grid grid-cols-3 mb-4">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="actions">Actions</TabsTrigger>
</TabsList>
<div className="mt-6 space-y-8">
{/* User Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">User Information</h3>
<TabsContent value="details" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" value={formData.email} onChange={handleInputChange} />
</div>
<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
onClick={() => updateUserMutation.mutate()}
disabled={updateUserMutation.isPending}
className="w-full"
>
{updateUserMutation.isPending ? "Saving..." : "Save Changes"}
</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>
{user.email_confirmed_at && (
<div className="text-xs text-muted-foreground">
Confirmed at: {new Date(user.email_confirmed_at).toLocaleString()}
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="phone-confirmed">Phone Confirmed</Label>
<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>
<Separator />
<div className="space-y-2">
<Label>Authentication Factors</Label>
<div className="text-sm text-muted-foreground">
{user.factors?.length
? user.factors.map((factor, i) => (
<div key={i} className="flex items-center gap-2">
<Badge variant="outline">{factor.factor_type}</Badge>
<span>{new Date(factor.created_at).toLocaleString()}</span>
</div>
))
: "No authentication factors"}
</div>
</div>
<Separator />
<div className="space-y-2">
<Label>Password Reset</Label>
<Button
variant="outline"
onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={sendPasswordRecoveryMutation.isPending || !user.email}
className="w-full"
>
Send Password Recovery Email
</Button>
</div>
<div className="space-y-2">
<Label>Magic Link</Label>
<Button
variant="outline"
onClick={() => sendMagicLinkMutation.mutate()}
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>
)}
</div>
<Separator />
<div className="space-y-2">
<Label>Delete User</Label>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-full">
Delete User
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">User UID</span>
<div className="flex items-center">
<span className="font-mono">{user.id}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 ml-2"
onClick={() => {
navigator.clipboard.writeText(user.id)
// Optionally add a toast notification here
}}
>
<Copy className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the user account and remove their data
from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteUserMutation.mutate()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<SheetFooter className="mt-4">
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Created at</span>
<span>{new Date(user.created_at).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Updated at</span>
<span>{new Date(user.updated_at || user.created_at).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Invited at</span>
<span>{user.invited_at ? new Date(user.invited_at).toLocaleString() : "-"}</span>
</div>
<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>
</div>
<Separator />
{/* Provider Information Section */}
<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
variant="outline"
size="sm"
onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={sendPasswordRecoveryMutation.isPending || !user.email}
>
{sendPasswordRecoveryMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="h-4 w-4 mr-2" />
Send password recovery
</>
)}
</Button>
</div>
<Separator />
<div className="flex justify-between items-center">
<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>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash2 className="h-4 w-4 mr-2" />
Delete user
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the user account and remove their
data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setIsDeleting(true)
deleteUserMutation.mutate()
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
"Delete"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</div>
</div>
<SheetFooter className="mt-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>

View File

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

View File

@ -1,45 +1,88 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Users, UserCheck, UserX } from 'lucide-react'
"use client"
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() {
const { data: users = [], isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
})
const stats = calculateUserStats(users)
if (isLoading) {
return (
<>
{[...Array(3)].map((_, i) => (
<Card key={i} className="bg-background border-border">
<CardContent className="p-6">
<div className="space-y-4 animate-pulse">
<div className="h-5 w-24 bg-muted rounded" />
<div className="h-8 w-16 bg-muted rounded" />
<div className="h-4 w-32 bg-muted rounded" />
</div>
</CardContent>
</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 (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground">
+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>
</Card>
{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 { DataTable } from "./data-table"
import { columns, User } from "./column"
import { useQuery } from "react-query"
import { getUsers } from "../../user-management/action"
import { UserDetailSheet } from "./sheet"
// 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)
// 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 { data: users = [], isLoading } = useQuery({
// queryKey: ["users"],
// queryFn: getUsers,
// })
const handleRowClick = (user: User) => {
setSelectedUser(user)
setSheetOpen(true)
}
// const handleRowClick = (user: User) => {
// setSelectedUser(user)
// setSheetOpen(true)
// }
if (isLoading) {
return <div>Loading...</div>
}
// 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>
)
}
// return (
// <div className="w-full">
// <DataTable columns={columns} data={users} onRowClick={handleRowClick} />
// {selectedUser && <UserDetailSheet user={selectedUser} open={sheetOpen} onOpenChange={setSheetOpen} />}
// </div>
// )
// }

View File

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

View File

@ -7,6 +7,8 @@ export interface User {
last_sign_in_at?: string
email_confirmed_at?: string
phone_confirmed_at?: string
invited_at?: string
confirmation_sent_at?: string
banned_until?: string
factors?: {
id: string