From 04e23626be63ed05ed95a8b64fe57eef087ce097 Mon Sep 17 00:00:00 2001 From: pmoharana-cmd Date: Sun, 5 Jan 2025 00:24:39 -0500 Subject: [PATCH] Prevent employee/volunteer from editting and revamp loading spinner --- compass/app/admin/page.tsx | 97 ++++++++--- compass/app/resource/page.tsx | 92 +++++++--- compass/app/service/page.tsx | 91 +++++++--- compass/components/Drawer/Drawer.tsx | 158 +++++++++++------- .../components/Sidebar/LoadingIcon.module.css | 19 --- compass/components/Sidebar/LoadingIcon.tsx | 14 -- compass/components/Sidebar/Sidebar.tsx | 5 +- compass/components/Table/ResourceTable.tsx | 12 +- compass/components/Table/RowOpenAction.tsx | 3 + compass/components/Table/ServiceTable.tsx | 8 +- compass/components/Table/Table.tsx | 56 ++++--- compass/components/Table/UserTable.tsx | 13 +- compass/components/auth/Loading.tsx | 8 +- 13 files changed, 371 insertions(+), 205 deletions(-) delete mode 100644 compass/components/Sidebar/LoadingIcon.module.css delete mode 100644 compass/components/Sidebar/LoadingIcon.tsx diff --git a/compass/app/admin/page.tsx b/compass/app/admin/page.tsx index 4d4781d..efc5e10 100644 --- a/compass/app/admin/page.tsx +++ b/compass/app/admin/page.tsx @@ -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([]); - const [uuid, setUuid] = useState(""); + const [currUser, setCurrUser] = useState(undefined); + const [error, setError] = useState(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 (
- {/* icon + title */} }> - {/* TODO: REPLACE UUID WITH HTTP BEARER TOKEN */} - + {isLoading ? ( +
+
+
+ ) : error ? ( +
+
+

Error

+

{error}

+
+
+ ) : ( + + )}
); diff --git a/compass/app/resource/page.tsx b/compass/app/resource/page.tsx index 6a886d3..5b6e09b 100644 --- a/compass/app/resource/page.tsx +++ b/compass/app/resource/page.tsx @@ -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([]); - const [uuid, setUuid] = useState(""); + const [currUser, setCurrUser] = useState(undefined); + const [error, setError] = useState(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 (
- {/* icon + title */} }> - + {isLoading ? ( +
+
+
+ ) : error ? ( +
+
+

Error

+

{error}

+
+
+ ) : ( + + )}
); diff --git a/compass/app/service/page.tsx b/compass/app/service/page.tsx index 3366430..cbd02fc 100644 --- a/compass/app/service/page.tsx +++ b/compass/app/service/page.tsx @@ -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([]); - const [uuid, setUuid] = useState(""); + const [currUser, setCurrUser] = useState(undefined); + const [error, setError] = useState(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 (
- {/* icon + title */} }> - + {isLoading ? ( +
+
+
+ ) : error ? ( +
+
+

Error

+

{error}

+
+
+ ) : ( + + )}
); diff --git a/compass/components/Drawer/Drawer.tsx b/compass/components/Drawer/Drawer.tsx index a572036..ebfa0c0 100644 --- a/compass/components/Drawer/Drawer.tsx +++ b/compass/components/Drawer/Drawer.tsx @@ -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>; + isAdmin?: boolean; }; const Drawer: FunctionComponent = ({ @@ -42,6 +39,7 @@ const Drawer: FunctionComponent = ({ details, rowContent, setRowContent, + isAdmin, }: DrawerProps) => { const [isOpen, setIsOpen] = useState(false); const [isFull, setIsFull] = useState(false); @@ -161,35 +159,48 @@ const Drawer: FunctionComponent = ({ valueToRender = (
- {}) - } - singleValue={true} - onTagsChange={( - tags: Set - ) => { - const tagsArray = - Array.from(tags); - handleTempRowContentChange( - detail.key, - tagsArray.length > 0 - ? tagsArray[0] - : null - ); - }} - /> + {isAdmin ? ( + {}) + } + singleValue={true} + onTagsChange={( + tags: Set + ) => { + const tagsArray = + Array.from( + tags + ); + handleTempRowContentChange( + detail.key, + tagsArray.length > + 0 + ? tagsArray[0] + : null + ); + }} + /> + ) : ( +
+ + {value + ? value + : "no value"} + +
+ )}
); @@ -198,30 +209,56 @@ const Drawer: FunctionComponent = ({ valueToRender = (
- {}) - } - onTagsChange={( - tags: Set - ) => { - handleTempRowContentChange( - detail.key, - Array.from(tags) - ); - }} - /> + {isAdmin ? ( + {}) + } + onTagsChange={( + tags: Set + ) => { + handleTempRowContentChange( + detail.key, + Array.from(tags) + ); + }} + /> + ) : ( +
+ {value && + value.length > 0 ? ( + value.map( + ( + tag: string, + index: number + ) => ( + + {tag} + + ) + ) + ) : ( + + no requirements + + )} +
+ )}
); @@ -238,6 +275,7 @@ const Drawer: FunctionComponent = ({ } onKeyDown={handleEnterPress} rows={4} + disabled={!isAdmin} onInput={(e) => { const target = e.target as HTMLTextAreaElement; @@ -261,6 +299,7 @@ const Drawer: FunctionComponent = ({ type={detail.inputType} name={detail.key} value={value} + disabled={!isAdmin} onChange={ handleTempRowContentChangeHTML } @@ -283,6 +322,7 @@ const Drawer: FunctionComponent = ({ 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" />
diff --git a/compass/components/Sidebar/LoadingIcon.module.css b/compass/components/Sidebar/LoadingIcon.module.css deleted file mode 100644 index 7c2947e..0000000 --- a/compass/components/Sidebar/LoadingIcon.module.css +++ /dev/null @@ -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 */ - } -} diff --git a/compass/components/Sidebar/LoadingIcon.tsx b/compass/components/Sidebar/LoadingIcon.tsx deleted file mode 100644 index 508d4dc..0000000 --- a/compass/components/Sidebar/LoadingIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// components/Loading.js -import styles from "./LoadingIcon.module.css"; - -const LoadingIcon = () => { - return ( -
-
-
-
-
- ); -}; - -export default LoadingIcon; diff --git a/compass/components/Sidebar/Sidebar.tsx b/compass/components/Sidebar/Sidebar.tsx index cdd46c6..34a8b57 100644 --- a/compass/components/Sidebar/Sidebar.tsx +++ b/compass/components/Sidebar/Sidebar.tsx @@ -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>; @@ -67,7 +66,9 @@ const Sidebar: React.FC = ({ {/* Loading indicator*/} {isLoading && (
- +
+
+
)} diff --git a/compass/components/Table/ResourceTable.tsx b/compass/components/Table/ResourceTable.tsx index a85ffdd..9111512 100644 --- a/compass/components/Table/ResourceTable.tsx +++ b/compass/components/Table/ResourceTable.tsx @@ -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>; - uuid: string; + user?: User; }; /** @@ -29,7 +27,7 @@ type ResourceTableProps = { export default function ResourceTable({ data, setData, - uuid, + user, }: ResourceTableProps) { const columnHelper = createColumnHelper(); @@ -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"} /> ); } diff --git a/compass/components/Table/RowOpenAction.tsx b/compass/components/Table/RowOpenAction.tsx index bf0bab3..34b9c98 100644 --- a/compass/components/Table/RowOpenAction.tsx +++ b/compass/components/Table/RowOpenAction.tsx @@ -13,6 +13,7 @@ type RowOpenActionProps = { rowData: T; setData: Dispatch>; details: Details[]; + isAdmin?: boolean; }; export function RowOpenAction({ @@ -21,6 +22,7 @@ export function RowOpenAction({ rowData, setData, details, + isAdmin, }: RowOpenActionProps) { return (
@@ -31,6 +33,7 @@ export function RowOpenAction({ rowContent={rowData} details={details} setRowContent={setData} + isAdmin={isAdmin} />
diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx index a4cfad3..ecbf876 100644 --- a/compass/components/Table/ServiceTable.tsx +++ b/compass/components/Table/ServiceTable.tsx @@ -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>; - uuid: string; + user?: User; }; /** @@ -27,7 +28,7 @@ type ServiceTableProps = { export default function ServiceTable({ data, setData, - uuid, + user, }: ServiceTableProps) { const columnHelper = createColumnHelper(); @@ -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"} /> ); } diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 2b208fc..3b27be4 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -28,6 +28,7 @@ type TableProps = { columns: ColumnDef[]; 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({ columns, details, createEndpoint, + isAdmin = false, }: TableProps) { const columnHelper = createColumnHelper(); @@ -226,33 +228,37 @@ export default function Table({ })} - - - { - 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 + + + { + if ( + !validateNewItem(newItem, details) + ) { + return false; } - }); - return true; - }} - /> - - + createRow(newItem).then((response) => { + if (response.ok) { + newItem.visible = true; + setData((prev) => [ + ...prev, + newItem, + ]); + } + }); + + return true; + }} + /> + + + )}
diff --git a/compass/components/Table/UserTable.tsx b/compass/components/Table/UserTable.tsx index 6914e46..972fb3d 100644 --- a/compass/components/Table/UserTable.tsx +++ b/compass/components/Table/UserTable.tsx @@ -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>; - 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(); 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"} /> ); } diff --git a/compass/components/auth/Loading.tsx b/compass/components/auth/Loading.tsx index a56537b..b3e1db8 100644 --- a/compass/components/auth/Loading.tsx +++ b/compass/components/auth/Loading.tsx @@ -14,8 +14,12 @@ const Loading = () => { style={{ height: "auto", width: "auto" }} priority /> -

Loading...

-
+

+ Loading... +

+
+
+
);