Remove reactQuery

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
"use client"
"use client";
import { useState } from "react"
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 { useState } from "react";
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,
@ -17,118 +23,118 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
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"
// // 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 }
// }
} from "@/components/ui/alert-dialog";
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";
import { format } from "date-fns";
interface UserDetailsSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
user: any
onUserUpdate: () => void
open: boolean;
onOpenChange: (open: boolean) => void;
user: any;
onUserUpdate: () => void;
}
export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: UserDetailsSheetProps) {
const [isDeleting, setIsDeleting] = useState(false)
export function UserDetailsSheet({
open,
onOpenChange,
user,
onUserUpdate,
}: UserDetailsSheetProps) {
const [isDeleting, setIsDeleting] = useState(false);
const [isLoading, setIsLoading] = useState({
deleteUser: false,
sendPasswordRecovery: false,
sendMagicLink: false,
toggleBan: false,
});
const deleteUserMutation = useMutation({
mutationFn: () => deleteUser(user.id),
onSuccess: () => {
toast.success("User deleted successfully")
onUserUpdate()
onOpenChange(false)
},
onError: () => {
toast.error("Failed to delete user")
},
onSettled: () => {
setIsDeleting(false)
},
})
const handleDeleteUser = async () => {
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
setIsDeleting(true);
try {
await deleteUser(user.id);
toast.success("User deleted successfully");
onUserUpdate();
onOpenChange(false);
} catch {
toast.error("Failed to delete user");
} finally {
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
setIsDeleting(false);
}
};
const sendPasswordRecoveryMutation = useMutation({
mutationFn: () => {
const handleSendPasswordRecovery = async () => {
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
try {
if (!user.email) {
throw new Error("User does not have an email address")
throw new Error("User does not have an email address");
}
return sendPasswordRecovery(user.email)
},
onSuccess: () => {
toast.success("Password recovery email sent")
},
onError: () => {
toast.error("Failed to send password recovery email")
},
})
await sendPasswordRecovery(user.email);
toast.success("Password recovery email sent");
} catch {
toast.error("Failed to send password recovery email");
} finally {
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
}
};
const sendMagicLinkMutation = useMutation({
mutationFn: () => {
const handleSendMagicLink = async () => {
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
try {
if (!user.email) {
throw new Error("User does not have an email address")
throw new Error("User does not have an email address");
}
return sendMagicLink(user.email)
},
onSuccess: () => {
toast.success("Magic link sent successfully")
},
onError: () => {
toast.error("Failed to send magic link")
},
})
await sendMagicLink(user.email);
toast.success("Magic link sent successfully");
} catch {
toast.error("Failed to send magic link");
} finally {
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
}
};
const toggleBanMutation = useMutation({
mutationFn: () => {
const handleToggleBan = async () => {
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
try {
if (user.banned_until) {
return unbanUser(user.id)
await unbanUser(user.id);
} else {
return banUser(user.id)
await banUser(user.id);
}
},
onSuccess: () => {
toast.success("User ban status updated")
onUserUpdate()
},
onError: () => {
toast.error("Failed to update user ban status")
},
})
toast.success("User ban status updated");
onUserUpdate();
} catch {
toast.error("Failed to update user ban status");
} finally {
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
}
};
const handleCopyItem = (item: string) => {
navigator.clipboard.writeText(item);
toast.success("Copied to clipboard");
};
const formatDate = (date: string | undefined | null) => {
return date ? format(new Date(date), "PPpp") : "-";
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
@ -140,16 +146,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
variant="ghost"
size="icon"
className="h-4 w-4"
onClick={() => {
navigator.clipboard.writeText(user.email)
// Optionally add a toast notification here
}}
onClick={() => handleCopyItem(user.email)}
>
<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>}
{!user.email_confirmed_at && (
<Badge variant="outline">Unconfirmed</Badge>
)}
{!user.banned_until && user.email_confirmed_at && (
<Badge variant="default">Active</Badge>
)}
</SheetTitle>
</SheetHeader>
@ -167,10 +174,7 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
variant="ghost"
size="icon"
className="h-4 w-4 ml-2"
onClick={() => {
navigator.clipboard.writeText(user.id)
// Optionally add a toast notification here
}}
onClick={() => handleCopyItem(user.id)}
>
<Copy className="h-4 w-4" />
</Button>
@ -184,27 +188,33 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<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>
<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>
<span>{formatDate(user.invited_at)}</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>
<span className="text-muted-foreground">
Confirmation sent at
</span>
<span>{formatDate(user.email_confirmation_sent_at)}</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>
<span>{formatDate(user.email_confirmed_at)}</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>
<span>{formatDate(user.last_sign_in_at)}</span>
</div>
<div className="flex justify-between items-center py-1">
@ -219,7 +229,9 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* 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>
<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">
@ -227,10 +239,15 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<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 className="text-xs text-muted-foreground">
Signed in with a email account via OAuth
</div>
</div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
</div>
<Badge
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
<CheckCircle className="h-3 w-3 mr-1" /> Enabled
</Badge>
</div>
@ -240,15 +257,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<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>
<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}
onClick={handleSendPasswordRecovery}
disabled={isLoading.sendPasswordRecovery || !user.email}
>
{sendPasswordRecoveryMutation.isPending ? (
{isLoading.sendPasswordRecovery ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
@ -267,15 +286,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<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>
<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}
onClick={handleSendMagicLink}
disabled={isLoading.sendMagicLink || !user.email}
>
{sendMagicLinkMutation.isPending ? (
{isLoading.sendMagicLink ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
@ -295,22 +316,28 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* 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>
<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>
<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}
onClick={handleToggleBan}
disabled={isLoading.toggleBan}
>
{toggleBanMutation.isPending ? (
{isLoading.toggleBan ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{user.banned_until ? "Unbanning..." : "Banning..."}
@ -327,31 +354,47 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
<div className="border border-destructive/20 rounded-md p-4 flex justify-between items-center">
<div>
<h4 className="font-medium">Delete user</h4>
<p className="text-xs text-muted-foreground">User will no longer have access to the project</p>
<p className="text-xs text-muted-foreground">
User will no longer have access to the project
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Button
variant="destructive"
size="sm"
disabled={isDeleting}
>
{isDeleting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4 mr-2" />
Delete user
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<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.
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()
}}
onClick={handleDeleteUser}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isDeleting}
>
{isDeleting ? (
<>
@ -377,6 +420,5 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
</SheetFooter> */}
</SheetContent>
</Sheet>
)
);
}

View File

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

View File

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

View File

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

View File

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