diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 40c5bb9..60b1696 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -44,7 +44,7 @@ RUN mkdir -p /etc/apt/keyrings \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list \ && apt-get update \ && apt-get install nodejs -y \ - && npm install -g npm@latest \ + && npm install -g npm@10.8.2 \ && rm -rf /var/lib/apt/lists/* # Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user diff --git a/README.md b/README.md index 463741d..41208a4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ NEXT_PUBLIC_HOST=http://localhost:3000 Open a new terminal and run this script in sequence to setup the dependencies and database ``` +chmod +x ./start.sh ./start.sh ``` diff --git a/backend/api/resource.py b/backend/api/resource.py index 97d25af..d698a0d 100644 --- a/backend/api/resource.py +++ b/backend/api/resource.py @@ -19,7 +19,10 @@ # TODO: Create custom exceptions @api.post("", response_model=Resource, tags=["Resource"]) def create( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + resource: Resource, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.create(subject, resource) @@ -27,14 +30,20 @@ def create( @api.get("", response_model=List[Resource], tags=["Resource"]) def get_all( - uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.get_resource_by_user(subject) + @api.get("/{name}", response_model=Resource, tags=["Resource"]) def get_by_name( - name:str, uuid:str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + name: str, + uuid: str, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.get_resource_by_name(name, subject) @@ -42,15 +51,21 @@ def get_by_name( @api.put("", response_model=Resource, tags=["Resource"]) def update( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + resource: Resource, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.update(subject, resource) -@api.delete("", response_model=None, tags=["Resource"]) +@api.delete("", response_model=dict, tags=["Resource"]) def delete( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + id: int, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) - resource_svc.delete(subject, resource) + return resource_svc.delete(subject, id) diff --git a/backend/api/service.py b/backend/api/service.py index bd3c4dc..844c840 100644 --- a/backend/api/service.py +++ b/backend/api/service.py @@ -19,7 +19,10 @@ # TODO: Create custom exceptions @api.post("", response_model=Service, tags=["Service"]) def create( - uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() + uuid: str, + service: Service, + user_svc: UserService = Depends(), + service_svc: ServiceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return service_svc.create(subject, service) @@ -27,28 +30,42 @@ def create( @api.get("", response_model=List[Service], tags=["Service"]) def get_all( - uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() + uuid: str, + user_svc: UserService = Depends(), + service_svc: ServiceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return service_svc.get_service_by_user(subject) + @api.get("/{name}", response_model=Service, tags=["Service"]) def get_by_name( - name: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() + name: str, + uuid: str, + user_svc: UserService = Depends(), + service_svc: ServiceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return service_svc.get_service_by_name(name, subject) + @api.put("", response_model=Service, tags=["Service"]) def update( - uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() + uuid: str, + service: Service, + user_svc: UserService = Depends(), + service_svc: ServiceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return service_svc.update(subject, service) -@api.delete("", response_model=None, tags=["Service"]) + +@api.delete("", response_model=dict, tags=["Service"]) def delete( - uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() + uuid: str, + id: int, + user_svc: UserService = Depends(), + service_svc: ServiceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) - service_svc.delete(subject, service) + return service_svc.delete(subject, id) diff --git a/backend/api/user.py b/backend/api/user.py index b7ec372..36e2d15 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from ..services import UserService from ..models.user_model import User, UserTypeEnum @@ -16,8 +16,8 @@ # TODO: Enable authorization by passing user uuid to API # TODO: Create custom exceptions @api.get("/all", response_model=List[User], tags=["Users"]) -def get_all(user_id: str, user_svc: UserService = Depends()): - subject = user_svc.get_user_by_uuid(user_id) +def get_all(uuid: str, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) if subject.role != UserTypeEnum.ADMIN: raise Exception(f"Insufficient permissions for user {subject.uuid}") @@ -25,6 +25,35 @@ def get_all(user_id: str, user_svc: UserService = Depends()): return user_svc.all() -@api.get("/{user_id}", response_model=User, tags=["Users"]) -def get_by_uuid(user_id: str, user_svc: UserService = Depends()): - return user_svc.get_user_by_uuid(user_id) +@api.get("/{uuid}", response_model=User, tags=["Users"]) +def get_by_uuid(uuid: str, user_svc: UserService = Depends()): + return user_svc.get_user_by_uuid(uuid) + + +@api.post("/", response_model=User, tags=["Users"]) +def create_user(uuid: str, user: User, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + + return user_svc.create(user) + + +@api.put("/", response_model=User, tags=["Users"]) +def update_user(uuid: str, user: User, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + + return user_svc.update(user) + + +@api.delete("/", response_model=dict, tags=["Users"]) +def delete_user(uuid: str, id: int, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) + + try: + user_svc.delete_by_id(id, subject) + return {"message": "User deleted successfully"} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index ca66ee9..3d4cdd7 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -38,8 +38,8 @@ class UserEntity(EntityBase): program: Mapped[list[ProgramTypeEnum]] = mapped_column( ARRAY(Enum(ProgramTypeEnum)), nullable=False ) - experience: Mapped[int] = mapped_column(Integer, nullable=False) - group: Mapped[str] = mapped_column(String(50)) + experience: Mapped[int] = mapped_column(Integer, nullable=True) + group: Mapped[str] = mapped_column(String(50), nullable=True) uuid: Mapped[str] = mapped_column(String, nullable=True) @classmethod diff --git a/backend/models/user_model.py b/backend/models/user_model.py index e2c25da..60c3007 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -10,9 +10,9 @@ class User(BaseModel): id: int | None = None username: str = Field(..., description="The username of the user") email: str = Field(..., description="The e-mail of the user") - experience: int = Field(..., description="Years of Experience of the User") - group: str - program: List[ProgramTypeEnum] - role: UserTypeEnum + experience: int | None = Field(None, description="Years of Experience of the User") + group: str | None = Field(None, description="The group of the user") + program: List[ProgramTypeEnum] | None = None + role: UserTypeEnum | None = None created_at: Optional[datetime] = datetime.now() uuid: str | None = None diff --git a/backend/services/resource.py b/backend/services/resource.py index c06af66..de8e657 100644 --- a/backend/services/resource.py +++ b/backend/services/resource.py @@ -17,7 +17,7 @@ def __init__(self, session: Session = Depends(db_session)): def get_resource_by_user(self, subject: User) -> list[Resource]: """Resource method getting all of the resources that a user has access to based on role""" if subject.role != UserTypeEnum.VOLUNTEER: - query = select(ResourceEntity) + query = select(ResourceEntity).order_by(ResourceEntity.id) entities = self._session.scalars(query).all() return [resource.to_model() for resource in entities] else: @@ -86,14 +86,14 @@ def update(self, subject: User, resource: Resource) -> Resource: raise ResourceNotFoundException( f"No resource found with matching id: {resource.id}" ) - entity.name = resource.name - entity.summary = resource.summary - entity.link = resource.link - entity.program = resource.program + entity.name = resource.name if resource.name else entity.name + entity.summary = resource.summary if resource.summary else entity.summary + entity.link = resource.link if resource.link else entity.link + entity.program = resource.program if resource.program else entity.program self._session.commit() return entity.to_model() - def delete(self, subject: User, resource: Resource) -> None: + def delete(self, subject: User, id: int) -> None: """ Delete resource based on id that the user has access to Parameters: @@ -106,15 +106,16 @@ def delete(self, subject: User, resource: Resource) -> None: raise ProgramNotAssignedException( f"User is not {UserTypeEnum.ADMIN}, cannot update service" ) - query = select(ResourceEntity).where(ResourceEntity.id == resource.id) + + query = select(ResourceEntity).where(ResourceEntity.id == id) entity = self._session.scalars(query).one_or_none() if entity is None: - raise ResourceNotFoundException( - f"No resource found with matching id: {resource.id}" - ) + raise ResourceNotFoundException(f"No resource found with matching id: {id}") self._session.delete(entity) self._session.commit() + return {"message": "Resource deleted successfully"} + def get_by_slug(self, user: User, search_string: str) -> list[Resource]: """ Get a list of resources given a search string that the user has access to diff --git a/backend/services/service.py b/backend/services/service.py index e10309e..28ad493 100644 --- a/backend/services/service.py +++ b/backend/services/service.py @@ -22,14 +22,18 @@ def __init__(self, session: Session = Depends(db_session)): def get_service_by_user(self, subject: User) -> list[Service]: """Resource method getting all of the resources that a user has access to based on role""" if subject.role != UserTypeEnum.VOLUNTEER: - query = select(ServiceEntity) + query = select(ServiceEntity).order_by(ServiceEntity.id) entities = self._session.scalars(query).all() return [service.to_model() for service in entities] else: programs = subject.program services = [] for program in programs: - entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all() + entities = ( + self._session.query(ServiceEntity) + .where(ServiceEntity.program == program) + .all() + ) for entity in entities: services.append(entity.to_model()) return [service for service in services] @@ -37,14 +41,14 @@ def get_service_by_user(self, subject: User) -> list[Service]: def get_service_by_name(self, name: str, subject: User) -> Service: """Service method getting services by id.""" query = select(ServiceEntity).where( - and_( - ServiceEntity.name == name, ServiceEntity.program.in_(subject.program) - ) + and_(ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)) ) entity = self._session.scalars(query).one_or_none() if entity is None: - raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned") + raise ServiceNotFoundException( + f"Service with name: {name} does not exist or program has not been assigned" + ) return entity.to_model() @@ -66,7 +70,7 @@ def update(self, subject: User, service: Service) -> Service: raise ProgramNotAssignedException( f"User is not {UserTypeEnum.ADMIN}, cannot update service" ) - + query = select(ServiceEntity).where(ServiceEntity.id == service.id) entity = self._session.scalars(query).one_or_none() @@ -75,21 +79,23 @@ def update(self, subject: User, service: Service) -> Service: "The service you are searching for does not exist." ) - entity.name = service.name - entity.status = service.status - entity.summary = service.summary - entity.requirements = service.requirements - entity.program = service.program + entity.name = service.name if service.name else entity.name + entity.status = service.status if service.status else entity.status + entity.summary = service.summary if service.summary else entity.summary + entity.requirements = ( + service.requirements if service.requirements else entity.requirements + ) + entity.program = service.program if service.program else entity.program self._session.commit() return entity.to_model() - def delete(self, subject: User, service: Service) -> None: + def delete(self, subject: User, id: int) -> None: """Deletes a service from the table.""" if subject.role != UserTypeEnum.ADMIN: raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") - - query = select(ServiceEntity).where(ServiceEntity.id == service.id) + + query = select(ServiceEntity).where(ServiceEntity.id == id) entity = self._session.scalars(query).one_or_none() if entity is None: @@ -99,3 +105,5 @@ def delete(self, subject: User, service: Service) -> None: self._session.delete(entity) self._session.commit() + + return {"message": "Service deleted successfully"} diff --git a/backend/services/user.py b/backend/services/user.py index 360db01..119fafa 100644 --- a/backend/services/user.py +++ b/backend/services/user.py @@ -4,6 +4,7 @@ from ..entities.user_entity import UserEntity from ..models.user_model import User from sqlalchemy import select +from ..models.enum_for_models import UserTypeEnum class UserService: @@ -43,10 +44,10 @@ def get_user_by_uuid(self, uuid: str) -> User: def all(self) -> list[User]: """ - Returns a list of all Users + Returns a list of all Users ordered by id """ - query = select(UserEntity) + query = select(UserEntity).order_by(UserEntity.id) entities = self._session.scalars(query).all() return [entity.to_model() for entity in entities] @@ -61,22 +62,21 @@ def create(self, user: User) -> User: """ try: - if (user.id != None): + if user.id != None: user = self.get_user_by_id(user.id) + else: + user_entity = UserEntity.from_model(user) + # add new user to table + self._session.add(user_entity) + self._session.commit() except: - # if does not exist, create new object - user_entity = UserEntity.from_model(user) - - # add new user to table - self._session.add(user_entity) - self._session.commit() - finally: - # return added object - return user - - def delete(self, user: User) -> None: + raise Exception(f"Failed to create user") + + return user + + def delete(self, user: User) -> None: """ - Delete a user + Delete a user Args: the user to delete @@ -86,34 +86,46 @@ def delete(self, user: User) -> None: if obj is None: raise Exception(f"No matching user found") - + self._session.delete(obj) self._session.commit() + def delete_by_id(self, id: int, user: User) -> None: + """ + Delete a user by id + Args: the id of the user to delete - def update(self, user: User) -> User: + Returns: none + """ + + if user.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {user.uuid}") + + obj = self._session.get(UserEntity, id) + self._session.delete(obj) + self._session.commit() + + def update(self, user: User) -> User: """ Updates a user Args: User to be updated Returns: The updated User - """ + """ obj = self._session.get(UserEntity, user.id) if obj is None: raise Exception(f"No matching user found") - - obj.username = user.username - obj.role = user.role - obj.email = user.email - obj.program = user.program - obj.experience = user.experience - obj.group = user.group + + obj.username = user.username if user.username else obj.username + obj.role = user.role if user.role else obj.role + obj.email = user.email if user.email else obj.email + obj.program = user.program if user.program else obj.program + obj.experience = user.experience if user.experience else obj.experience + obj.group = user.group if user.group else obj.group self._session.commit() return obj.to_model() - - diff --git a/compass/README.md b/compass/README.md index 5ce4a7c..c403366 100644 --- a/compass/README.md +++ b/compass/README.md @@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti To learn more about Next.js, take a look at the following resources: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! diff --git a/compass/app/admin/page.tsx b/compass/app/admin/page.tsx index abe2a8c..efc5e10 100644 --- a/compass/app/admin/page.tsx +++ b/compass/app/admin/page.tsx @@ -4,41 +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 [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}`), + ]); - const userListData = await fetch( - `${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.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}` + ); + } - const users: User[] = await userListData.json(); + // Parse the responses + const [usersAPI, currUserData] = await Promise.all([ + usersResponse.json(), + userResponse.json(), + ]); - setUsers(users); + // 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); + } } - getUser(); + getUsers(); }, []); return (
- {/* icon + title */} }> - + {isLoading ? ( +
+
+
+ ) : error ? ( +
+
+

Error

+

{error}

+
+
+ ) : ( + + )}
); diff --git a/compass/app/api/resource/create/route.ts b/compass/app/api/resource/create/route.ts new file mode 100644 index 0000000..6f19b37 --- /dev/null +++ b/compass/app/api/resource/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import Resource from "@/utils/models/Resource"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`; + + try { + const resourceData = await request.json(); + + console.log("RESOURCE DATA", JSON.stringify(resourceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(resourceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdResource: Resource = await response.json(); + return NextResponse.json(createdResource, { status: response.status }); + } catch (error) { + console.error("Error creating resource:", error); + return NextResponse.json( + { error: "Failed to create resource" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/resource/delete/route.ts b/compass/app/api/resource/delete/route.ts new file mode 100644 index 0000000..5e908e0 --- /dev/null +++ b/compass/app/api/resource/delete/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; + +export async function DELETE(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`; + + try { + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + const resource_id = searchParams.get("id"); + + console.log("Resource to be deleted", resource_id); + + // Send the POST request to the backend + const response = await fetch( + `${apiEndpoint}?uuid=${uuid}&id=${resource_id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return NextResponse.json( + { message: "Resource deleted successfully" }, + { status: response.status } + ); + } catch (error) { + console.error("Error deleting resource:", error); + return NextResponse.json( + { error: "Failed to delete resource" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/resource/update/route.ts b/compass/app/api/resource/update/route.ts new file mode 100644 index 0000000..d500b3d --- /dev/null +++ b/compass/app/api/resource/update/route.ts @@ -0,0 +1,37 @@ +import Resource from "@/utils/models/Resource"; +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`; + + try { + const resourceData = await request.json(); + + console.log("RESOURCE DATA", JSON.stringify(resourceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(resourceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const resource: Resource = await response.json(); + return NextResponse.json(resource, { status: response.status }); + } catch (error) { + console.error("Error creating user:", error); + return NextResponse.json( + { error: "Failed to update resource" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/service/create/route.ts b/compass/app/api/service/create/route.ts new file mode 100644 index 0000000..957911d --- /dev/null +++ b/compass/app/api/service/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`; + + try { + const serviceData = await request.json(); + + console.log("SERVICE DATA", JSON.stringify(serviceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(serviceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdService: User = await response.json(); + return NextResponse.json(createdService, { status: response.status }); + } catch (error) { + console.error("Error creating service:", error); + return NextResponse.json( + { error: "Failed to create service" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/service/delete/route.ts b/compass/app/api/service/delete/route.ts new file mode 100644 index 0000000..088e5e4 --- /dev/null +++ b/compass/app/api/service/delete/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; + +export async function DELETE(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`; + + try { + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + const service_id = searchParams.get("id"); + + console.log("Service to be deleted", service_id); + + // Send the POST request to the backend + const response = await fetch( + `${apiEndpoint}?uuid=${uuid}&id=${service_id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return NextResponse.json( + { message: "Service deleted successfully" }, + { status: response.status } + ); + } catch (error) { + console.error("Error deleting service:", error); + return NextResponse.json( + { error: "Failed to delete service" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/service/update/route.ts b/compass/app/api/service/update/route.ts new file mode 100644 index 0000000..6f3f281 --- /dev/null +++ b/compass/app/api/service/update/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import Service from "@/utils/models/Service"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`; + + try { + const serviceData = await request.json(); + + console.log("SERVICE DATA", JSON.stringify(serviceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(serviceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdService: Service = await response.json(); + return NextResponse.json(createdService, { status: response.status }); + } catch (error) { + console.error("Error creating service:", error); + return NextResponse.json( + { error: "Failed to update service" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/user/all/route.ts b/compass/app/api/user/all/route.ts index 5f7259a..0f072eb 100644 --- a/compass/app/api/user/all/route.ts +++ b/compass/app/api/user/all/route.ts @@ -9,7 +9,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); const uuid = searchParams.get("uuid"); - const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); + const data = await fetch(`${apiEndpoint}?uuid=${uuid}`); const userData: User[] = await data.json(); // TODO: Remove make every user visible diff --git a/compass/app/api/user/create/route.ts b/compass/app/api/user/create/route.ts new file mode 100644 index 0000000..d7ea091 --- /dev/null +++ b/compass/app/api/user/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`; + + try { + const userData = await request.json(); + + console.log("USER DATA", JSON.stringify(userData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdUser: User = await response.json(); + return NextResponse.json(createdUser, { status: response.status }); + } catch (error) { + console.error("Error creating user:", error); + return NextResponse.json( + { error: "Failed to create user" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/user/delete/route.ts b/compass/app/api/user/delete/route.ts new file mode 100644 index 0000000..ac24724 --- /dev/null +++ b/compass/app/api/user/delete/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; + +export async function DELETE(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`; + + try { + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + const user_id = searchParams.get("id"); + + console.log("User to be deleted", user_id); + + // Send the POST request to the backend + const response = await fetch( + `${apiEndpoint}?uuid=${uuid}&id=${user_id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return NextResponse.json( + { message: "User deleted successfully" }, + { status: response.status } + ); + } catch (error) { + console.error("Error deleting user:", error); + return NextResponse.json( + { error: "Failed to delete user" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/user/update/route.ts b/compass/app/api/user/update/route.ts new file mode 100644 index 0000000..9c640bc --- /dev/null +++ b/compass/app/api/user/update/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`; + + try { + const userData = await request.json(); + + console.log("USER DATA", JSON.stringify(userData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdUser: User = await response.json(); + return NextResponse.json(createdUser, { status: response.status }); + } catch (error) { + console.error("Error creating user:", error); + return NextResponse.json( + { error: "Failed to create user" }, + { status: 500 } + ); + } +} diff --git a/compass/app/auth/login/page.tsx b/compass/app/auth/login/page.tsx index fa945c9..9dab449 100644 --- a/compass/app/auth/login/page.tsx +++ b/compass/app/auth/login/page.tsx @@ -77,6 +77,8 @@ export default function Page() { alt="Compass Center logo." width={100} height={91} + style={{ height: "auto", width: "auto" }} + priority />

Login

diff --git a/compass/app/home/page.tsx b/compass/app/home/page.tsx index 86be3ef..c656fd4 100644 --- a/compass/app/home/page.tsx +++ b/compass/app/home/page.tsx @@ -2,60 +2,20 @@ import Callout from "@/components/resource/Callout"; import Card from "@/components/resource/Card"; import { LandingSearchBar } from "@/components/resource/LandingSearchBar"; -import { - BookOpenIcon, - BookmarkIcon, - ClipboardIcon, -} from "@heroicons/react/24/solid"; +import { SearchResult } from "@/components/resource/SearchResult"; import Image from "next/image"; import Link from "next/link"; export default function Page() { return ( -
- {/* icon + title */} -
-
- Compass Center logo. -

- Compass Center Advocate Landing Page -

-
- - Welcome! Below you will find a list of resources for the - Compass Center's trained advocates. These materials - serve to virtually provide a collection of advocacy, - resource, and hotline manuals and information. - - {" "} - If you are an advocate looking for the contact - information of a particular Compass Center employee, - please directly contact your staff back-up or the person - in charge of your training. - - -
-
- {/* link to different pages */} -
- - } text="Resources" /> - - - } text="Services" /> - - - } text="Training Manuals" /> - -
- {/* search bar */} - +
+
+

+ Good evening! +

+ +
); } diff --git a/compass/app/resource/page.tsx b/compass/app/resource/page.tsx index 7cbd2c1..5b6e09b 100644 --- a/compass/app/resource/page.tsx +++ b/compass/app/resource/page.tsx @@ -4,31 +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 [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."); + } - const userListData = await fetch( - `${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}` - ); + // 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}`), + ]); - const resourcesAPI: Resource[] = await userListData.json(); + // 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}` + ); + } - setResources(resourcesAPI); + // 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); + } } getResources(); @@ -36,9 +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 efe6337..cbd02fc 100644 --- a/compass/app/service/page.tsx +++ b/compass/app/service/page.tsx @@ -3,31 +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 [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}`), + ]); - const serviceListData = await fetch( - `${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.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}` + ); + } - const servicesAPI: Service[] = await serviceListData.json(); - setServices(servicesAPI); + // 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); + } } getServices(); @@ -35,9 +73,25 @@ export default function Page() { return (
- {/* icon + title */} }> - + {isLoading ? ( +
+
+
+ ) : error ? ( +
+
+

Error

+

{error}

+
+
+ ) : ( + + )}
); diff --git a/compass/app/test/page.tsx b/compass/app/test/page.tsx index 37271df..9ae938e 100644 --- a/compass/app/test/page.tsx +++ b/compass/app/test/page.tsx @@ -76,6 +76,8 @@ export default function Page() { alt="Compass Center logo." width={100} height={91} + style={{ height: "auto", width: "auto" }} + priority />

Login

diff --git a/compass/components/Drawer/CreateDrawer.tsx b/compass/components/Drawer/CreateDrawer.tsx new file mode 100644 index 0000000..d8511b7 --- /dev/null +++ b/compass/components/Drawer/CreateDrawer.tsx @@ -0,0 +1,232 @@ +import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; +import React, { useState } from "react"; +import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, +} from "@heroicons/react/24/outline"; +import TagsInput from "../TagsInput/Index"; +import { Details } from "./Drawer"; + +type CreateDrawerProps = { + details: Details[]; + onCreate: (newItem: any) => boolean; +}; + +const CreateDrawer: FunctionComponent = ({ + details, + onCreate, +}: CreateDrawerProps) => { + const [isOpen, setIsOpen] = useState(false); + const [isFull, setIsFull] = useState(false); + const [newItemContent, setNewItemContent] = useState({}); + const [renderKey, setRenderKey] = useState(0); + + const handleContentChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setNewItemContent((prev: any) => ({ + ...prev, + [name]: value, + })); + }; + + const initializeSelectField = (key: string) => { + if (!newItemContent[key]) { + setNewItemContent((prev: any) => ({ + ...prev, + [key]: [], + })); + } + }; + + const handleCreate = () => { + if (onCreate(newItemContent)) { + console.log("newItemContent", newItemContent); + setNewItemContent({}); + setIsOpen(false); + } + }; + + const toggleDrawer = () => { + setIsOpen(!isOpen); + if (isFull) { + setIsFull(!isFull); + } + if (!isOpen) { + setRenderKey((prev) => prev + 1); + } + }; + + const toggleDrawerFullScreen = () => setIsFull(!isFull); + + const drawerClassName = `fixed top-0 right-0 h-full bg-white transform ease-in-out duration-300 z-20 overflow-y-auto ${ + isOpen ? "translate-x-0 shadow-2xl" : "translate-x-full" + } ${isFull ? "w-full" : "w-[600px]"}`; + + const iconComponent = isFull ? ( + + ) : ( + + ); + + return ( +
+ +
+
+

+ Create New Item +

+
+ + +
+
+
+
+ {details.map((detail, index) => { + const value = newItemContent[detail.key] || ""; + let inputField; + + switch (detail.inputType) { + case "select-one": + initializeSelectField(detail.key); + inputField = ( + {}) + } + singleValue={true} + onTagsChange={( + tags: Set + ) => { + setNewItemContent( + (prev: any) => ({ + ...prev, + [detail.key]: + tags.size > 0 + ? Array.from( + tags + )[0] + : null, + }) + ); + }} + /> + ); + break; + case "select-multiple": + initializeSelectField(detail.key); + inputField = ( + {}) + } + onTagsChange={( + tags: Set + ) => { + setNewItemContent( + (prev: any) => ({ + ...prev, + [detail.key]: + Array.from(tags), + }) + ); + }} + /> + ); + break; + case "textarea": + inputField = ( +