- {flexRender(header.column.columnDef.header, header.getContext())}
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
{{
asc: " 🔼",
desc: " 🔽",
@@ -131,8 +164,13 @@ export function DataTable
({
header.column.setFilterValue(e.target.value)}
+ value={
+ (header.column.getFilterValue() as string) ??
+ ""
+ }
+ onChange={(e) =>
+ header.column.setFilterValue(e.target.value)
+ }
className="h-8"
/>
@@ -156,7 +194,9 @@ export function DataTable({
onClick={() => onRowClick && onRowClick(row.original)}
>
{row.getVisibleCells().map((cell) => (
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
))}
))
@@ -174,10 +214,15 @@ export function DataTable({
- 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
@@ -188,11 +233,13 @@ export function DataTable
({
- )
+ );
}
-
diff --git a/sigap-website/components/admin/users/invite-user.tsx b/sigap-website/components/admin/users/invite-user.tsx
index 97b349d..c301432 100644
--- a/sigap-website/components/admin/users/invite-user.tsx
+++ b/sigap-website/components/admin/users/invite-user.tsx
@@ -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
@@ -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
-
{user.banned_until && Banned}
- {!user.email_confirmed_at && Unconfirmed}
- {!user.banned_until && user.email_confirmed_at && Active}
+ {!user.email_confirmed_at && (
+ Unconfirmed
+ )}
+ {!user.banned_until && user.email_confirmed_at && (
+ Active
+ )}
@@ -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)}
>
@@ -184,27 +188,33 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
Updated at
- {new Date(user.updated_at || user.created_at).toLocaleString()}
+
+ {new Date(
+ user.updated_at || user.created_at
+ ).toLocaleString()}
+
Invited at
- {user.invited_at ? new Date(user.invited_at).toLocaleString() : "-"}
+ {formatDate(user.invited_at)}
- Confirmation sent at
- {user.confirmation_sent_at ? new Date(user.confirmation_sent_at).toLocaleString() : "-"}
+
+ Confirmation sent at
+
+ {formatDate(user.email_confirmation_sent_at)}
Confirmed at
- {user.email_confirmed_at ? new Date(user.email_confirmed_at).toLocaleString() : "-"}
+ {formatDate(user.email_confirmed_at)}
Last signed in
- {user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : "Never"}
+ {formatDate(user.last_sign_in_at)}
@@ -219,7 +229,9 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* Provider Information Section */}
Provider Information
-
The user has the following providers
+
+ The user has the following providers
+
@@ -227,10 +239,15 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
Email
-
Signed in with a email account via OAuth
+
+ Signed in with a email account via OAuth
+
-
+
Enabled
@@ -240,15 +257,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
Reset password
-
Send a password recovery email to the user
+
+ Send a password recovery email to the user
+
sendPasswordRecoveryMutation.mutate()}
- disabled={sendPasswordRecoveryMutation.isPending || !user.email}
+ onClick={handleSendPasswordRecovery}
+ disabled={isLoading.sendPasswordRecovery || !user.email}
>
- {sendPasswordRecoveryMutation.isPending ? (
+ {isLoading.sendPasswordRecovery ? (
<>
Sending...
@@ -267,15 +286,17 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
Send magic link
-
Passwordless login via email for the user
+
+ Passwordless login via email for the user
+
sendMagicLinkMutation.mutate()}
- disabled={sendMagicLinkMutation.isPending || !user.email}
+ onClick={handleSendMagicLink}
+ disabled={isLoading.sendMagicLink || !user.email}
>
- {sendMagicLinkMutation.isPending ? (
+ {isLoading.sendMagicLink ? (
<>
Sending...
@@ -295,22 +316,28 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
{/* Danger Zone Section */}
-
Danger zone
-
Be wary of the following features as they cannot be undone.
+
+ Danger zone
+
+
+ Be wary of the following features as they cannot be undone.
+
Ban user
-
Revoke access to the project for a set duration
+
+ Revoke access to the project for a set duration
+
toggleBanMutation.mutate()}
- disabled={toggleBanMutation.isPending}
+ onClick={handleToggleBan}
+ disabled={isLoading.toggleBan}
>
- {toggleBanMutation.isPending ? (
+ {isLoading.toggleBan ? (
<>
{user.banned_until ? "Unbanning..." : "Banning..."}
@@ -327,31 +354,47 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
Delete user
-
User will no longer have access to the project
+
+ User will no longer have access to the project
+
-
-
- Delete user
+
+ {isDeleting ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ <>
+
+ Delete user
+ >
+ )}
- Are you absolutely sure?
+
+ Are you absolutely sure?
+
- 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.
Cancel
{
- 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
*/}
- )
+ );
}
-
diff --git a/sigap-website/components/admin/users/user-management.tsx b/sigap-website/components/admin/users/user-management.tsx
index bf78c06..cbc820a 100644
--- a/sigap-website/components/admin/users/user-management.tsx
+++ b/sigap-website/components/admin/users/user-management.tsx
@@ -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([]);
+ 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) => (
-
Email
@@ -234,9 +249,19 @@ export default function UserManagement() {
),
cell: ({ row }: { row: { original: User } }) => (
-
- {row.original.email?.[0]?.toUpperCase() || "?"}
-
+
+ {row.original.profile?.avatar ? (
+
+ ) : (
+ row.original.email?.[0]?.toUpperCase() || "?"
+ )}
+
{row.original.email || "No email"}
@@ -252,7 +277,6 @@ export default function UserManagement() {
id: "phone",
header: ({ column }: any) => (
-
Phone
@@ -287,7 +311,6 @@ export default function UserManagement() {
id: "lastSignIn",
header: ({ column }: any) => (
-
Last Sign In
@@ -360,7 +383,6 @@ export default function UserManagement() {
id: "createdAt",
header: ({ column }: any) => (
-
Created At
@@ -420,7 +442,6 @@ export default function UserManagement() {
id: "status",
header: ({ column }: any) => (
-
Status
diff --git a/sigap-website/next.config.ts b/sigap-website/next.config.ts
index e9ffa30..42663ed 100644
--- a/sigap-website/next.config.ts
+++ b/sigap-website/next.config.ts
@@ -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;
diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma
index 48f3295..3f3cf5b 100644
--- a/sigap-website/prisma/schema.prisma
+++ b/sigap-website/prisma/schema.prisma
@@ -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])
diff --git a/sigap-website/src/models/users/users.model.ts b/sigap-website/src/models/users/users.model.ts
index 25d60b2..b38df47 100644
--- a/sigap-website/src/models/users/users.model.ts
+++ b/sigap-website/src/models/users/users.model.ts
@@ -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
- factors?: {
- id: string
- factor_type: string
- created_at: string
- updated_at: string
- }[]
- raw_user_meta_data?: Record
- raw_app_meta_data?: Record
- }
-
- export interface CreateUserParams {
- email: string
- password: string
- phone?: string
- user_metadata?: Record
- email_confirm?: boolean
- }
-
- export interface UpdateUserParams {
- email?: string
- phone?: string
- password?: string
- user_metadata?: Record
- }
-
- export interface InviteUserParams {
- email: string
- user_metadata?: Record
- }
-
-
\ No newline at end of file
+ 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;
+ raw_app_meta_data?: Record;
+ profile?: Profile;
+}
+
+export interface CreateUserParams {
+ email: string;
+ password: string;
+ phone?: string;
+ user_metadata?: Record;
+ email_confirm?: boolean;
+}
+
+export interface UpdateUserParams {
+ email?: string;
+ phone?: string;
+ password?: string;
+ user_metadata?: Record;
+}
+
+export interface InviteUserParams {
+ email: string;
+ user_metadata?: Record;
+}
+
+export interface Profile {
+ id: string;
+ user_id: string;
+ avatar?: string;
+ first_name?: string;
+ last_name?: string;
+ bio: string;
+ address?: string;
+ birthdate?: string;
+}