Prevent employee/volunteer from editting and revamp loading spinner

This commit is contained in:
pmoharana-cmd 2025-01-05 00:24:39 -05:00
parent f6b0838c99
commit 04e23626be
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 User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client";
import { UsersIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
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(() => {
async function getUser() {
const supabase = createClient();
async function getUsers() {
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) {
console.log("Accessed admin page but not logged in");
return;
if (authError) {
throw new Error("Authentication failed. Please sign in.");
}
// 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 (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}>
{/* TODO: REPLACE UUID WITH HTTP BEARER TOKEN */}
<UserTable data={users} setData={setUsers} uuid={uuid} />
{isLoading ? (
<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>
</div>
);

View File

@ -4,34 +4,68 @@ import { PageLayout } from "@/components/PageLayout";
import Resource from "@/utils/models/Resource";
import ResourceTable from "@/components/Table/ResourceTable";
import { createClient } from "@/utils/supabase/client";
import { BookmarkIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
import User from "@/utils/models/User";
export default function Page() {
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(() => {
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) {
console.log("Accessed admin page but not logged in");
return;
if (authError) {
throw new Error("Authentication failed. Please sign in.");
}
// 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();
@ -39,13 +73,25 @@ export default function Page() {
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable
data={resources}
setData={setResources}
uuid={uuid}
/>
{isLoading ? (
<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>
) : (
<ResourceTable
data={resources}
setData={setResources}
user={currUser}
/>
)}
</PageLayout>
</div>
);

View File

@ -3,34 +3,69 @@
import { PageLayout } from "@/components/PageLayout";
import ServiceTable from "@/components/Table/ServiceTable";
import Service from "@/utils/models/Service";
import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client";
import { ClipboardIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
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(() => {
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) {
console.log("Accessed admin page but not logged in");
return;
if (authError) {
throw new Error("Authentication failed. Please sign in.");
}
// 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();
@ -38,13 +73,25 @@ export default function Page() {
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable
data={services}
setData={setServices}
uuid={uuid}
/>
{isLoading ? (
<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>
) : (
<ServiceTable
data={services}
setData={setServices}
user={currUser}
/>
)}
</PageLayout>
</div>
);

View File

@ -1,18 +1,14 @@
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
import React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import {
StarIcon as SolidStarIcon,
EnvelopeIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { StarIcon as SolidStarIcon, UserIcon } from "@heroicons/react/24/solid";
import {
ArrowsPointingOutIcon,
ArrowsPointingInIcon,
StarIcon as OutlineStarIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index";
import { Tag } from "../TagsInput/Tag";
type InputType =
| "text"
@ -35,6 +31,7 @@ type DrawerProps = {
details: Details[];
rowContent?: any;
setRowContent?: Dispatch<SetStateAction<any>>;
isAdmin?: boolean;
};
const Drawer: FunctionComponent<DrawerProps> = ({
@ -42,6 +39,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
details,
rowContent,
setRowContent,
isAdmin,
}: DrawerProps) => {
const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false);
@ -161,35 +159,48 @@ const Drawer: FunctionComponent<DrawerProps> = ({
valueToRender = (
<div className="flex-1">
<div className="rounded-md px-2 py-1">
<TagsInput
presetValue={
typeof value ===
"string"
? [value]
: value || []
}
presetOptions={
detail.presetOptionsValues ||
[]
}
setPresetOptions={
detail.presetOptionsSetter ||
(() => {})
}
singleValue={true}
onTagsChange={(
tags: Set<string>
) => {
const tagsArray =
Array.from(tags);
handleTempRowContentChange(
detail.key,
tagsArray.length > 0
? tagsArray[0]
: null
);
}}
/>
{isAdmin ? (
<TagsInput
presetValue={
typeof value ===
"string"
? [value]
: value || []
}
presetOptions={
detail.presetOptionsValues ||
[]
}
setPresetOptions={
detail.presetOptionsSetter ||
(() => {})
}
singleValue={true}
onTagsChange={(
tags: Set<string>
) => {
const tagsArray =
Array.from(
tags
);
handleTempRowContentChange(
detail.key,
tagsArray.length >
0
? tagsArray[0]
: null
);
}}
/>
) : (
<div className="flex">
<Tag>
{value
? value
: "no value"}
</Tag>
</div>
)}
</div>
</div>
);
@ -198,30 +209,56 @@ const Drawer: FunctionComponent<DrawerProps> = ({
valueToRender = (
<div className="flex-1">
<div className="rounded-md px-2 py-1">
<TagsInput
presetValue={
typeof value ===
"string"
? [value]
: value || []
}
presetOptions={
detail.presetOptionsValues ||
[]
}
setPresetOptions={
detail.presetOptionsSetter ||
(() => {})
}
onTagsChange={(
tags: Set<string>
) => {
handleTempRowContentChange(
detail.key,
Array.from(tags)
);
}}
/>
{isAdmin ? (
<TagsInput
presetValue={
typeof value ===
"string"
? [value]
: value || []
}
presetOptions={
detail.presetOptionsValues ||
[]
}
setPresetOptions={
detail.presetOptionsSetter ||
(() => {})
}
onTagsChange={(
tags: Set<string>
) => {
handleTempRowContentChange(
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>
);
@ -238,6 +275,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
}
onKeyDown={handleEnterPress}
rows={4}
disabled={!isAdmin}
onInput={(e) => {
const target =
e.target as HTMLTextAreaElement;
@ -261,6 +299,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
type={detail.inputType}
name={detail.key}
value={value}
disabled={!isAdmin}
onChange={
handleTempRowContentChangeHTML
}
@ -283,6 +322,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
handleTempRowContentChangeHTML
}
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"
/>
</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";
import { SidebarItem } from "./SidebarItem";
import { UserProfile } from "../resource/UserProfile";
import LoadingIcon from "./LoadingIcon";
interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -67,7 +66,9 @@ const Sidebar: React.FC<SidebarProps> = ({
{/* Loading indicator*/}
{isLoading && (
<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>
)}

View File

@ -6,19 +6,17 @@ import {
UserIcon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { RowOpenAction } from "@/components/Table/RowOpenAction";
import Table from "@/components/Table/Table";
import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
import User from "@/utils/models/User";
type ResourceTableProps = {
data: Resource[];
setData: Dispatch<SetStateAction<Resource[]>>;
uuid: string;
user?: User;
};
/**
@ -29,7 +27,7 @@ type ResourceTableProps = {
export default function ResourceTable({
data,
setData,
uuid,
user,
}: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>();
@ -82,6 +80,7 @@ export default function ResourceTable({
rowData={info.row.original}
setData={setData}
details={resourceDetails}
isAdmin={user?.role === "ADMIN"}
/>
),
}),
@ -142,7 +141,8 @@ export default function ResourceTable({
setData={setData}
columns={columns}
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;
setData: Dispatch<SetStateAction<T[]>>;
details: Details[];
isAdmin?: boolean;
};
export function RowOpenAction<T extends DataPoint>({
@ -21,6 +22,7 @@ export function RowOpenAction<T extends DataPoint>({
rowData,
setData,
details,
isAdmin,
}: RowOpenActionProps<T>) {
return (
<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}
details={details}
setRowContent={setData}
isAdmin={isAdmin}
/>
</span>
</div>

View File

@ -12,11 +12,12 @@ import { RowOpenAction } from "@/components/Table/RowOpenAction";
import Service from "@/utils/models/Service";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
import User from "@/utils/models/User";
type ServiceTableProps = {
data: Service[];
setData: Dispatch<SetStateAction<Service[]>>;
uuid: string;
user?: User;
};
/**
@ -27,7 +28,7 @@ type ServiceTableProps = {
export default function ServiceTable({
data,
setData,
uuid,
user,
}: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>();
@ -170,7 +171,8 @@ export default function ServiceTable({
setData={setData}
columns={columns}
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>[];
details: Details[];
createEndpoint: string;
isAdmin?: boolean;
};
/** Validates that all required fields in a new item have values */
@ -78,6 +79,7 @@ export default function Table<T extends DataPoint>({
columns,
details,
createEndpoint,
isAdmin = false,
}: TableProps<T>) {
const columnHelper = createColumnHelper<T>();
@ -226,33 +228,37 @@ export default function Table<T extends DataPoint>({
})}
</tbody>
<tfoot>
<tr>
<td
className="p-3 border-y border-gray-200"
colSpan={100}
>
<CreateDrawer
details={details}
onCreate={(newItem) => {
if (!validateNewItem(newItem, details)) {
return false;
}
createRow(newItem).then((response) => {
if (response.ok) {
newItem.visible = true;
setData((prev) => [
...prev,
newItem,
]);
{isAdmin && ( // Only show create drawer for admins
<tr>
<td
className="p-3 border-y border-gray-200"
colSpan={100}
>
<CreateDrawer
details={details}
onCreate={(newItem) => {
if (
!validateNewItem(newItem, details)
) {
return false;
}
});
return true;
}}
/>
</td>
</tr>
createRow(newItem).then((response) => {
if (response.ok) {
newItem.visible = true;
setData((prev) => [
...prev,
newItem,
]);
}
});
return true;
}}
/>
</td>
</tr>
)}
</tfoot>
</table>
</div>

View File

@ -1,17 +1,12 @@
import {
ArrowDownCircleIcon,
AtSymbolIcon,
Bars2Icon,
EnvelopeIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import Table from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
@ -19,7 +14,7 @@ import { Tag } from "../TagsInput/Tag";
type UserTableProps = {
data: 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.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 [rolePresets, setRolePresets] = useState([
@ -88,6 +83,7 @@ export default function UserTable({ data, setData, uuid }: UserTableProps) {
rowData={info.row.original}
setData={setData}
details={userDetails}
isAdmin={user?.role === "ADMIN"}
/>
),
}),
@ -145,7 +141,8 @@ export default function UserTable({ data, setData, uuid }: UserTableProps) {
setData={setData}
columns={columns}
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" }}
priority
/>
<h1 className={styles.loadingTitle}>Loading...</h1>
<div className={styles.loadingSpinner}></div>
<h1 className="text-2xl font-semibold text-gray-700 mt-4 mb-6">
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>
);