Prevent employee/volunteer from editting and revamp loading spinner (#54)

This commit is contained in:
Prajwal Moharana 2025-01-05 00:25:18 -05:00 committed by GitHub
parent f6b0838c99
commit 0daf80d222
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 371 additions and 205 deletions

View File

@ -4,45 +4,98 @@ import { PageLayout } from "@/components/PageLayout";
import UserTable from "@/components/Table/UserTable"; import UserTable from "@/components/Table/UserTable";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { UsersIcon } from "@heroicons/react/24/solid"; import { UsersIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Page() { export default function Page() {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [uuid, setUuid] = useState<string>(""); const [currUser, setCurrUser] = useState<User | undefined>(undefined);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
async function getUser() { async function getUsers() {
const supabase = createClient(); try {
setIsLoading(true);
setError(null);
const { data, error } = await supabase.auth.getUser(); const supabase = createClient();
const { data: userData, error: authError } =
await supabase.auth.getUser();
if (error) { if (authError) {
console.log("Accessed admin page but not logged in"); throw new Error("Authentication failed. Please sign in.");
return; }
// Fetch users list and current user data in parallel
const [usersResponse, userResponse] = await Promise.all([
fetch(`/api/user/all?uuid=${userData.user.id}`),
fetch(`/api/user?uuid=${userData.user.id}`),
]);
// Check for HTTP errors
if (!usersResponse.ok) {
throw new Error(
`Failed to fetch users: ${usersResponse.statusText}`
);
}
if (!userResponse.ok) {
throw new Error(
`Failed to fetch user data: ${userResponse.statusText}`
);
}
// Parse the responses
const [usersAPI, currUserData] = await Promise.all([
usersResponse.json(),
userResponse.json(),
]);
// Verify admin status
if (currUserData.role !== "ADMIN") {
throw new Error("Unauthorized: Admin access required");
}
setUsers(usersAPI);
setCurrUser(currUserData);
} catch (err) {
console.error("Error fetching data:", err);
setError(
err instanceof Error
? err.message
: "An unexpected error occurred"
);
setUsers([]);
setCurrUser(undefined);
} finally {
setIsLoading(false);
} }
setUuid(data.user.id);
const userListData = await fetch(
`/api/user/all?uuid=${data.user.id}`
);
const users: User[] = await userListData.json();
setUsers(users);
} }
getUser(); getUsers();
}, []); }, []);
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}> <PageLayout title="Users" icon={<UsersIcon />}>
{/* TODO: REPLACE UUID WITH HTTP BEARER TOKEN */} {isLoading ? (
<UserTable data={users} setData={setUsers} uuid={uuid} /> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
</div>
) : error ? (
<div className="flex justify-center items-center h-64">
<div className="text-red-500 text-center">
<p className="text-lg font-semibold">Error</p>
<p className="text-sm">{error}</p>
</div>
</div>
) : (
<UserTable
data={users}
setData={setUsers}
user={currUser}
/>
)}
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -4,34 +4,68 @@ import { PageLayout } from "@/components/PageLayout";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import ResourceTable from "@/components/Table/ResourceTable"; import ResourceTable from "@/components/Table/ResourceTable";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { BookmarkIcon } from "@heroicons/react/24/solid"; import { BookmarkIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import User from "@/utils/models/User";
export default function Page() { export default function Page() {
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
const [uuid, setUuid] = useState<string>(""); const [currUser, setCurrUser] = useState<User | undefined>(undefined);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
async function getResources() { async function getResources() {
const supabase = createClient(); try {
setIsLoading(true);
setError(null);
const { data, error } = await supabase.auth.getUser(); const supabase = createClient();
const { data: userData, error: authError } =
await supabase.auth.getUser();
if (error) { if (authError) {
console.log("Accessed admin page but not logged in"); throw new Error("Authentication failed. Please sign in.");
return; }
// Fetch resources and user data in parallel
const [resourceResponse, userResponse] = await Promise.all([
fetch(`/api/resource/all?uuid=${userData.user.id}`),
fetch(`/api/user?uuid=${userData.user.id}`),
]);
// Check for HTTP errors
if (!resourceResponse.ok) {
throw new Error(
`Failed to fetch resources: ${resourceResponse.statusText}`
);
}
if (!userResponse.ok) {
throw new Error(
`Failed to fetch user data: ${userResponse.statusText}`
);
}
// Parse the responses
const [resourcesAPI, currUserData] = await Promise.all([
resourceResponse.json(),
userResponse.json(),
]);
setResources(resourcesAPI);
setCurrUser(currUserData);
} catch (err) {
console.error("Error fetching data:", err);
setError(
err instanceof Error
? err.message
: "An unexpected error occurred"
);
setResources([]);
setCurrUser(undefined);
} finally {
setIsLoading(false);
} }
setUuid(data.user.id);
const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}`
);
const resourcesAPI: Resource[] = await userListData.json();
setResources(resourcesAPI);
} }
getResources(); getResources();
@ -39,13 +73,25 @@ export default function Page() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}> <PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable {isLoading ? (
data={resources} <div className="flex justify-center items-center h-64">
setData={setResources} <div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
uuid={uuid} </div>
/> ) : error ? (
<div className="flex justify-center items-center h-64">
<div className="text-red-500 text-center">
<p className="text-lg font-semibold">Error</p>
<p className="text-sm">{error}</p>
</div>
</div>
) : (
<ResourceTable
data={resources}
setData={setResources}
user={currUser}
/>
)}
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -3,34 +3,69 @@
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import ServiceTable from "@/components/Table/ServiceTable"; import ServiceTable from "@/components/Table/ServiceTable";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { ClipboardIcon } from "@heroicons/react/24/solid"; import { ClipboardIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Page() { export default function Page() {
const [services, setServices] = useState<Service[]>([]); const [services, setServices] = useState<Service[]>([]);
const [uuid, setUuid] = useState<string>(""); const [currUser, setCurrUser] = useState<User | undefined>(undefined);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
async function getServices() { async function getServices() {
const supabase = createClient(); try {
setIsLoading(true);
setError(null);
const { data, error } = await supabase.auth.getUser(); const supabase = createClient();
const { data: userData, error: authError } =
await supabase.auth.getUser();
if (error) { if (authError) {
console.log("Accessed admin page but not logged in"); throw new Error("Authentication failed. Please sign in.");
return; }
// Fetch services and user data in parallel
const [serviceResponse, userResponse] = await Promise.all([
fetch(`/api/service/all?uuid=${userData.user.id}`),
fetch(`/api/user?uuid=${userData.user.id}`),
]);
// Check for HTTP errors
if (!serviceResponse.ok) {
throw new Error(
`Failed to fetch services: ${serviceResponse.statusText}`
);
}
if (!userResponse.ok) {
throw new Error(
`Failed to fetch user data: ${userResponse.statusText}`
);
}
// Parse the responses
const [servicesAPI, currUserData] = await Promise.all([
serviceResponse.json(),
userResponse.json(),
]);
setCurrUser(currUserData);
setServices(servicesAPI);
} catch (err) {
console.error("Error fetching data:", err);
setError(
err instanceof Error
? err.message
: "An unexpected error occurred"
);
setServices([]);
setCurrUser(undefined);
} finally {
setIsLoading(false);
} }
setUuid(data.user.id);
const serviceListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}`
);
const servicesAPI: Service[] = await serviceListData.json();
setServices(servicesAPI);
} }
getServices(); getServices();
@ -38,13 +73,25 @@ export default function Page() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}> <PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable {isLoading ? (
data={services} <div className="flex justify-center items-center h-64">
setData={setServices} <div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
uuid={uuid} </div>
/> ) : error ? (
<div className="flex justify-center items-center h-64">
<div className="text-red-500 text-center">
<p className="text-lg font-semibold">Error</p>
<p className="text-sm">{error}</p>
</div>
</div>
) : (
<ServiceTable
data={services}
setData={setServices}
user={currUser}
/>
)}
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -1,18 +1,14 @@
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import { import { StarIcon as SolidStarIcon, UserIcon } from "@heroicons/react/24/solid";
StarIcon as SolidStarIcon,
EnvelopeIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { import {
ArrowsPointingOutIcon, ArrowsPointingOutIcon,
ArrowsPointingInIcon, ArrowsPointingInIcon,
StarIcon as OutlineStarIcon, StarIcon as OutlineStarIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
import { Tag } from "../TagsInput/Tag";
type InputType = type InputType =
| "text" | "text"
@ -35,6 +31,7 @@ type DrawerProps = {
details: Details[]; details: Details[];
rowContent?: any; rowContent?: any;
setRowContent?: Dispatch<SetStateAction<any>>; setRowContent?: Dispatch<SetStateAction<any>>;
isAdmin?: boolean;
}; };
const Drawer: FunctionComponent<DrawerProps> = ({ const Drawer: FunctionComponent<DrawerProps> = ({
@ -42,6 +39,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
details, details,
rowContent, rowContent,
setRowContent, setRowContent,
isAdmin,
}: DrawerProps) => { }: DrawerProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false); const [isFull, setIsFull] = useState(false);
@ -161,35 +159,48 @@ const Drawer: FunctionComponent<DrawerProps> = ({
valueToRender = ( valueToRender = (
<div className="flex-1"> <div className="flex-1">
<div className="rounded-md px-2 py-1"> <div className="rounded-md px-2 py-1">
<TagsInput {isAdmin ? (
presetValue={ <TagsInput
typeof value === presetValue={
"string" typeof value ===
? [value] "string"
: value || [] ? [value]
} : value || []
presetOptions={ }
detail.presetOptionsValues || presetOptions={
[] detail.presetOptionsValues ||
} []
setPresetOptions={ }
detail.presetOptionsSetter || setPresetOptions={
(() => {}) detail.presetOptionsSetter ||
} (() => {})
singleValue={true} }
onTagsChange={( singleValue={true}
tags: Set<string> onTagsChange={(
) => { tags: Set<string>
const tagsArray = ) => {
Array.from(tags); const tagsArray =
handleTempRowContentChange( Array.from(
detail.key, tags
tagsArray.length > 0 );
? tagsArray[0] handleTempRowContentChange(
: null detail.key,
); tagsArray.length >
}} 0
/> ? tagsArray[0]
: null
);
}}
/>
) : (
<div className="flex">
<Tag>
{value
? value
: "no value"}
</Tag>
</div>
)}
</div> </div>
</div> </div>
); );
@ -198,30 +209,56 @@ const Drawer: FunctionComponent<DrawerProps> = ({
valueToRender = ( valueToRender = (
<div className="flex-1"> <div className="flex-1">
<div className="rounded-md px-2 py-1"> <div className="rounded-md px-2 py-1">
<TagsInput {isAdmin ? (
presetValue={ <TagsInput
typeof value === presetValue={
"string" typeof value ===
? [value] "string"
: value || [] ? [value]
} : value || []
presetOptions={ }
detail.presetOptionsValues || presetOptions={
[] detail.presetOptionsValues ||
} []
setPresetOptions={ }
detail.presetOptionsSetter || setPresetOptions={
(() => {}) detail.presetOptionsSetter ||
} (() => {})
onTagsChange={( }
tags: Set<string> onTagsChange={(
) => { tags: Set<string>
handleTempRowContentChange( ) => {
detail.key, handleTempRowContentChange(
Array.from(tags) detail.key,
); Array.from(tags)
}} );
/> }}
/>
) : (
<div className="flex flex-wrap gap-2">
{value &&
value.length > 0 ? (
value.map(
(
tag: string,
index: number
) => (
<Tag
key={
index
}
>
{tag}
</Tag>
)
)
) : (
<Tag>
no requirements
</Tag>
)}
</div>
)}
</div> </div>
</div> </div>
); );
@ -238,6 +275,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
} }
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
rows={4} rows={4}
disabled={!isAdmin}
onInput={(e) => { onInput={(e) => {
const target = const target =
e.target as HTMLTextAreaElement; e.target as HTMLTextAreaElement;
@ -261,6 +299,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
type={detail.inputType} type={detail.inputType}
name={detail.key} name={detail.key}
value={value} value={value}
disabled={!isAdmin}
onChange={ onChange={
handleTempRowContentChangeHTML handleTempRowContentChangeHTML
} }
@ -283,6 +322,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
handleTempRowContentChangeHTML handleTempRowContentChangeHTML
} }
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
disabled={!isAdmin}
className="w-full p-1 font-normal hover:text-gray-400 focus:outline-gray-200 underline text-gray-500 bg-transparent" className="w-full p-1 font-normal hover:text-gray-400 focus:outline-gray-200 underline text-gray-500 bg-transparent"
/> />
</div> </div>

View File

@ -1,19 +0,0 @@
/* components/LoadingIcon.module.css */
.loader {
width: 24px; /* Larger for better visibility */
height: 24px;
border: 4px solid #5b21b6; /* Primary color */
border-top: 4px solid #ffffff; /* Contrasting color */
border-radius: 50%;
animation: spin 1s linear infinite; /* Smooth continuous spin */
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg); /* Start position */
}
100% {
transform: rotate(360deg); /* Full rotation */
}
}

View File

@ -1,14 +0,0 @@
// components/Loading.js
import styles from "./LoadingIcon.module.css";
const LoadingIcon = () => {
return (
<div className={styles.loadingOverlay}>
<div className={styles.loadingContent}>
<div className={styles.loader}></div>
</div>
</div>
);
};
export default LoadingIcon;

View File

@ -10,7 +10,6 @@ import {
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { SidebarItem } from "./SidebarItem"; import { SidebarItem } from "./SidebarItem";
import { UserProfile } from "../resource/UserProfile"; import { UserProfile } from "../resource/UserProfile";
import LoadingIcon from "./LoadingIcon";
interface SidebarProps { interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -67,7 +66,9 @@ const Sidebar: React.FC<SidebarProps> = ({
{/* Loading indicator*/} {/* Loading indicator*/}
{isLoading && ( {isLoading && (
<div className="fixed top-2 left-2"> <div className="fixed top-2 left-2">
<LoadingIcon /> <div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-700" />
</div>
</div> </div>
)} )}

View File

@ -6,19 +6,17 @@ import {
UserIcon, UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { RowOpenAction } from "@/components/Table/RowOpenAction"; import { RowOpenAction } from "@/components/Table/RowOpenAction";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import { Details } from "../Drawer/Drawer"; import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag"; import { Tag } from "../TagsInput/Tag";
import User from "@/utils/models/User";
type ResourceTableProps = { type ResourceTableProps = {
data: Resource[]; data: Resource[];
setData: Dispatch<SetStateAction<Resource[]>>; setData: Dispatch<SetStateAction<Resource[]>>;
uuid: string; user?: User;
}; };
/** /**
@ -29,7 +27,7 @@ type ResourceTableProps = {
export default function ResourceTable({ export default function ResourceTable({
data, data,
setData, setData,
uuid, user,
}: ResourceTableProps) { }: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
@ -82,6 +80,7 @@ export default function ResourceTable({
rowData={info.row.original} rowData={info.row.original}
setData={setData} setData={setData}
details={resourceDetails} details={resourceDetails}
isAdmin={user?.role === "ADMIN"}
/> />
), ),
}), }),
@ -142,7 +141,8 @@ export default function ResourceTable({
setData={setData} setData={setData}
columns={columns} columns={columns}
details={resourceDetails} details={resourceDetails}
createEndpoint={`/api/resource/create?uuid=${uuid}`} createEndpoint={`/api/resource/create?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"}
/> />
); );
} }

View File

@ -13,6 +13,7 @@ type RowOpenActionProps<T extends DataPoint> = {
rowData: T; rowData: T;
setData: Dispatch<SetStateAction<T[]>>; setData: Dispatch<SetStateAction<T[]>>;
details: Details[]; details: Details[];
isAdmin?: boolean;
}; };
export function RowOpenAction<T extends DataPoint>({ export function RowOpenAction<T extends DataPoint>({
@ -21,6 +22,7 @@ export function RowOpenAction<T extends DataPoint>({
rowData, rowData,
setData, setData,
details, details,
isAdmin,
}: RowOpenActionProps<T>) { }: RowOpenActionProps<T>) {
return ( return (
<div className="font-semibold group flex flex-row items-center justify-between pr-2"> <div className="font-semibold group flex flex-row items-center justify-between pr-2">
@ -31,6 +33,7 @@ export function RowOpenAction<T extends DataPoint>({
rowContent={rowData} rowContent={rowData}
details={details} details={details}
setRowContent={setData} setRowContent={setData}
isAdmin={isAdmin}
/> />
</span> </span>
</div> </div>

View File

@ -12,11 +12,12 @@ import { RowOpenAction } from "@/components/Table/RowOpenAction";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import { Details } from "../Drawer/Drawer"; import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag"; import { Tag } from "../TagsInput/Tag";
import User from "@/utils/models/User";
type ServiceTableProps = { type ServiceTableProps = {
data: Service[]; data: Service[];
setData: Dispatch<SetStateAction<Service[]>>; setData: Dispatch<SetStateAction<Service[]>>;
uuid: string; user?: User;
}; };
/** /**
@ -27,7 +28,7 @@ type ServiceTableProps = {
export default function ServiceTable({ export default function ServiceTable({
data, data,
setData, setData,
uuid, user,
}: ServiceTableProps) { }: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
@ -170,7 +171,8 @@ export default function ServiceTable({
setData={setData} setData={setData}
columns={columns} columns={columns}
details={serviceDetails} details={serviceDetails}
createEndpoint={`/api/service/create?uuid=${uuid}`} createEndpoint={`/api/service/create?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"}
/> />
); );
} }

View File

@ -28,6 +28,7 @@ type TableProps<T extends DataPoint> = {
columns: ColumnDef<T, any>[]; columns: ColumnDef<T, any>[];
details: Details[]; details: Details[];
createEndpoint: string; createEndpoint: string;
isAdmin?: boolean;
}; };
/** Validates that all required fields in a new item have values */ /** Validates that all required fields in a new item have values */
@ -78,6 +79,7 @@ export default function Table<T extends DataPoint>({
columns, columns,
details, details,
createEndpoint, createEndpoint,
isAdmin = false,
}: TableProps<T>) { }: TableProps<T>) {
const columnHelper = createColumnHelper<T>(); const columnHelper = createColumnHelper<T>();
@ -226,33 +228,37 @@ export default function Table<T extends DataPoint>({
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> {isAdmin && ( // Only show create drawer for admins
<td <tr>
className="p-3 border-y border-gray-200" <td
colSpan={100} className="p-3 border-y border-gray-200"
> colSpan={100}
<CreateDrawer >
details={details} <CreateDrawer
onCreate={(newItem) => { details={details}
if (!validateNewItem(newItem, details)) { onCreate={(newItem) => {
return false; if (
} !validateNewItem(newItem, details)
) {
createRow(newItem).then((response) => { return false;
if (response.ok) {
newItem.visible = true;
setData((prev) => [
...prev,
newItem,
]);
} }
});
return true; createRow(newItem).then((response) => {
}} if (response.ok) {
/> newItem.visible = true;
</td> setData((prev) => [
</tr> ...prev,
newItem,
]);
}
});
return true;
}}
/>
</td>
</tr>
)}
</tfoot> </tfoot>
</table> </table>
</div> </div>

View File

@ -1,17 +1,12 @@
import { import {
ArrowDownCircleIcon,
AtSymbolIcon,
Bars2Icon,
EnvelopeIcon, EnvelopeIcon,
ListBulletIcon, ListBulletIcon,
UserIcon, UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction"; import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
import { Details } from "../Drawer/Drawer"; import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag"; import { Tag } from "../TagsInput/Tag";
@ -19,7 +14,7 @@ import { Tag } from "../TagsInput/Tag";
type UserTableProps = { type UserTableProps = {
data: User[]; data: User[];
setData: Dispatch<SetStateAction<User[]>>; setData: Dispatch<SetStateAction<User[]>>;
uuid: string; user?: User;
}; };
/** /**
@ -27,7 +22,7 @@ type UserTableProps = {
* @param props.data Stateful list of users to be displayed by the table * @param props.data Stateful list of users to be displayed by the table
* @param props.setData State setter for the list of users * @param props.setData State setter for the list of users
*/ */
export default function UserTable({ data, setData, uuid }: UserTableProps) { export default function UserTable({ data, setData, user }: UserTableProps) {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
const [rolePresets, setRolePresets] = useState([ const [rolePresets, setRolePresets] = useState([
@ -88,6 +83,7 @@ export default function UserTable({ data, setData, uuid }: UserTableProps) {
rowData={info.row.original} rowData={info.row.original}
setData={setData} setData={setData}
details={userDetails} details={userDetails}
isAdmin={user?.role === "ADMIN"}
/> />
), ),
}), }),
@ -145,7 +141,8 @@ export default function UserTable({ data, setData, uuid }: UserTableProps) {
setData={setData} setData={setData}
columns={columns} columns={columns}
details={userDetails} details={userDetails}
createEndpoint={`/api/user/create?uuid=${uuid}`} createEndpoint={`/api/user/create?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"}
/> />
); );
} }

View File

@ -14,8 +14,12 @@ const Loading = () => {
style={{ height: "auto", width: "auto" }} style={{ height: "auto", width: "auto" }}
priority priority
/> />
<h1 className={styles.loadingTitle}>Loading...</h1> <h1 className="text-2xl font-semibold text-gray-700 mt-4 mb-6">
<div className={styles.loadingSpinner}></div> Loading...
</h1>
<div className="flex justify-center">
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-gray-700"></div>
</div>
</div> </div>
</div> </div>
); );