initial commit

This commit is contained in:
Nick A 2024-10-14 17:06:09 -04:00
parent f9abc9169f
commit 5c604e2a5a
136 changed files with 14488 additions and 16387 deletions

View File

@ -1,4 +1,4 @@
cd compass cd compass
npm run lint npm run lint
npm run prettier npm run prettier
git add . git add .

12
.vscode/settings.json vendored
View File

@ -1,7 +1,7 @@
{ {
"python.testing.pytestArgs": [ "python.testing.pytestArgs": [
"backend" "backend"
], ],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true "python.testing.pytestEnabled": true
} }

View File

@ -1 +1 @@
"""Expose API routes via FastAPI routers from this package.""" """Expose API routes via FastAPI routers from this package."""

View File

@ -1,19 +1,19 @@
"""Confirm system health via monitorable API end points. """Confirm system health via monitorable API end points.
Production systems monitor these end points upon deployment, and at regular intervals, to ensure the service is running. Production systems monitor these end points upon deployment, and at regular intervals, to ensure the service is running.
""" """
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services.health import HealthService from ..services.health import HealthService
openapi_tags = { openapi_tags = {
"name": "System Health", "name": "System Health",
"description": "Production systems monitor these end points upon deployment, and at regular intervals, to ensure the service is running.", "description": "Production systems monitor these end points upon deployment, and at regular intervals, to ensure the service is running.",
} }
api = APIRouter(prefix="/api/health") api = APIRouter(prefix="/api/health")
@api.get("", tags=["System Health"]) @api.get("", tags=["System Health"])
def health_check(health_svc: HealthService = Depends()) -> str: def health_check(health_svc: HealthService = Depends()) -> str:
return health_svc.check() return health_svc.check()

View File

@ -1,26 +1,26 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services import ResourceService, UserService from ..services import ResourceService, UserService
from ..models.resource_model import Resource from ..models.resource_model import Resource
from typing import List from typing import List
api = APIRouter(prefix="/api/resource") api = APIRouter(prefix="/api/resource")
openapi_tags = { openapi_tags = {
"name": "Resource", "name": "Resource",
"description": "Resource search and related operations.", "description": "Resource search and related operations.",
} }
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.get("", response_model=List[Resource], tags=["Resource"]) @api.get("", response_model=List[Resource], tags=["Resource"])
def get_all( def get_all(
user_id: str, user_id: str,
resource_svc: ResourceService = Depends(), resource_svc: ResourceService = Depends(),
user_svc: UserService = Depends(), user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(user_id)
return resource_svc.get_resource_by_user(subject) return resource_svc.get_resource_by_user(subject)

View File

@ -1,26 +1,26 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services import ServiceService, UserService from ..services import ServiceService, UserService
from ..models.service_model import Service from ..models.service_model import Service
from typing import List from typing import List
api = APIRouter(prefix="/api/service") api = APIRouter(prefix="/api/service")
openapi_tags = { openapi_tags = {
"name": "Service", "name": "Service",
"description": "Service search and related operations.", "description": "Service search and related operations.",
} }
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.get("", response_model=List[Service], tags=["Service"]) @api.get("", response_model=List[Service], tags=["Service"])
def get_all( def get_all(
user_id: str, user_id: str,
service_svc: ServiceService = Depends(), service_svc: ServiceService = Depends(),
user_svc: UserService = Depends(), user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(user_id)
return service_svc.get_service_by_user(subject) return service_svc.get_service_by_user(subject)

View File

@ -1,30 +1,30 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services import UserService from ..services import UserService
from ..models.user_model import User, UserTypeEnum from ..models.user_model import User, UserTypeEnum
from typing import List from typing import List
api = APIRouter(prefix="/api/user") api = APIRouter(prefix="/api/user")
openapi_tags = { openapi_tags = {
"name": "Users", "name": "Users",
"description": "User profile search and related operations.", "description": "User profile search and related operations.",
} }
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.get("/all", response_model=List[User], tags=["Users"]) @api.get("/all", response_model=List[User], tags=["Users"])
def get_all(user_id: str, user_svc: UserService = Depends()): def get_all(user_id: str, user_svc: UserService = Depends()):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(user_id)
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise Exception(f"Insufficient permissions for user {subject.uuid}") raise Exception(f"Insufficient permissions for user {subject.uuid}")
return user_svc.all() return user_svc.all()
@api.get("/{user_id}", response_model=User, tags=["Users"]) @api.get("/{user_id}", response_model=User, tags=["Users"])
def get_by_uuid(user_id: str, user_svc: UserService = Depends()): def get_by_uuid(user_id: str, user_svc: UserService = Depends()):
return user_svc.get_user_by_uuid(user_id) return user_svc.get_user_by_uuid(user_id)

View File

@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
class Program_Enum(Enum): class Program_Enum(Enum):
ECONOMIC = "ECONOMIC" ECONOMIC = "ECONOMIC"
DOMESTIC = "DOMESTIC" DOMESTIC = "DOMESTIC"
COMMUNITY = "COMMUNITY" COMMUNITY = "COMMUNITY"

View File

@ -1,67 +1,67 @@
""" Defines the table for storing resources """ """ Defines the table for storing resources """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime, Enum from sqlalchemy import Integer, String, DateTime, Enum
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import self for to model # Import self for to model
from typing import Self from typing import Self
from backend.entities.program_enum import Program_Enum from backend.entities.program_enum import Program_Enum
from ..models.resource_model import Resource from ..models.resource_model import Resource
class ResourceEntity(EntityBase): class ResourceEntity(EntityBase):
# set table name # set table name
__tablename__ = "resource" __tablename__ = "resource"
# set fields # set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
name: Mapped[str] = mapped_column(String(64), nullable=False) name: Mapped[str] = mapped_column(String(64), nullable=False)
summary: Mapped[str] = mapped_column(String(100), nullable=False) summary: Mapped[str] = mapped_column(String(100), nullable=False)
link: Mapped[str] = mapped_column(String, nullable=False) link: Mapped[str] = mapped_column(String, nullable=False)
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
# relationships # relationships
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
back_populates="resource", cascade="all,delete" back_populates="resource", cascade="all,delete"
) )
@classmethod @classmethod
def from_model(cls, model: Resource) -> Self: def from_model(cls, model: Resource) -> Self:
""" """
Create a UserEntity from a User model. Create a UserEntity from a User model.
Args: Args:
model (User): The model to create the entity from. model (User): The model to create the entity from.
Returns: Returns:
Self: The entity (not yet persisted). Self: The entity (not yet persisted).
""" """
return cls( return cls(
id=model.id, id=model.id,
created_at=model.created_at, created_at=model.created_at,
name=model.name, name=model.name,
summary=model.summary, summary=model.summary,
link=model.link, link=model.link,
program=model.program, program=model.program,
) )
def to_model(self) -> Resource: def to_model(self) -> Resource:
return Resource( return Resource(
id=self.id, id=self.id,
created_at=self.created_at, created_at=self.created_at,
name=self.name, name=self.name,
summary=self.summary, summary=self.summary,
link=self.link, link=self.link,
program=self.program, program=self.program,
) )

View File

@ -1,46 +1,46 @@
""" Defines the table for resource tags """ """ Defines the table for resource tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import ForeignKey, Integer, String, DateTime from sqlalchemy import ForeignKey, Integer, String, DateTime
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import self for to model # Import self for to model
from typing import Self from typing import Self
class ResourceTagEntity(EntityBase): class ResourceTagEntity(EntityBase):
# set table name to user in the database # set table name to user in the database
__tablename__ = "resource_tag" __tablename__ = "resource_tag"
# set fields or 'columns' for the user table # set fields or 'columns' for the user table
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id")) resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
# relationships # relationships
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags") resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags") tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
# @classmethod # @classmethod
# def from_model (cls, model: resource_tag_model) -> Self: # def from_model (cls, model: resource_tag_model) -> Self:
# return cls ( # return cls (
# id = model.id, # id = model.id,
# resourceId = model.resourceId, # resourceId = model.resourceId,
# tagId = model.tagId, # tagId = model.tagId,
# ) # )
# def to_model (self) -> resource_tag_model: # def to_model (self) -> resource_tag_model:
# return user_model( # return user_model(
# id = self.id, # id = self.id,
# resourceId = self.resourceId, # resourceId = self.resourceId,
# tagId = self.tagId, # tagId = self.tagId,
# ) # )

View File

@ -1,12 +1,12 @@
from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from .entity_base import EntityBase from .entity_base import EntityBase
class SampleEntity(EntityBase): class SampleEntity(EntityBase):
__tablename__ = "persons" __tablename__ = "persons"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False)
age: Mapped[int] = mapped_column(Integer) age: Mapped[int] = mapped_column(Integer)
email: Mapped[str] = mapped_column(String, unique=True, nullable=False) email: Mapped[str] = mapped_column(String, unique=True, nullable=False)

View File

@ -1,47 +1,47 @@
""" Defines the table for storing services """ """ Defines the table for storing services """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime, ARRAY from sqlalchemy import Integer, String, DateTime, ARRAY
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import enums for Program # Import enums for Program
import enum import enum
from sqlalchemy import Enum from sqlalchemy import Enum
from backend.models.service_model import Service from backend.models.service_model import Service
from typing import Self from typing import Self
from backend.models.enum_for_models import ProgramTypeEnum from backend.models.enum_for_models import ProgramTypeEnum
class ServiceEntity(EntityBase): class ServiceEntity(EntityBase):
# set table name # set table name
__tablename__ = "service" __tablename__ = "service"
# set fields # set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
name: Mapped[str] = mapped_column(String(32), nullable=False) name: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False)
summary: Mapped[str] = mapped_column(String(100), nullable=False) summary: Mapped[str] = mapped_column(String(100), nullable=False)
requirements: Mapped[list[str]] = mapped_column(ARRAY(String)) requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
# relationships # relationships
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship( serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
back_populates="service", cascade="all,delete" back_populates="service", cascade="all,delete"
) )
def to_model(self) -> Service: def to_model(self) -> Service:
return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program) return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program)
@classmethod @classmethod
def from_model(cls, model:Service) -> Self: def from_model(cls, model:Service) -> Self:
return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program) return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program)

View File

@ -1,25 +1,25 @@
""" Defines the table for service tags """ """ Defines the table for service tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import ForeignKey, Integer from sqlalchemy import ForeignKey, Integer
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
class ServiceTagEntity(EntityBase): class ServiceTagEntity(EntityBase):
# set table name to user in the database # set table name to user in the database
__tablename__ = "service_tag" __tablename__ = "service_tag"
# set fields or 'columns' for the user table # set fields or 'columns' for the user table
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id")) serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
# relationships # relationships
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")

View File

@ -1,65 +1,65 @@
""" Defines the table for storing tags """ """ Defines the table for storing tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime from sqlalchemy import Integer, String, DateTime
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
from ..models.tag_model import Tag from ..models.tag_model import Tag
from typing import Self from typing import Self
class TagEntity(EntityBase): class TagEntity(EntityBase):
#set table name #set table name
__tablename__ = "tag" __tablename__ = "tag"
#set fields #set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
content: Mapped[str] = mapped_column(String(100), nullable=False) content: Mapped[str] = mapped_column(String(100), nullable=False)
#relationships #relationships
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
@classmethod @classmethod
def from_model(cls, model: Tag) -> Self: def from_model(cls, model: Tag) -> Self:
""" """
Create a user entity from model Create a user entity from model
Args: model (User): the model to create the entity from Args: model (User): the model to create the entity from
Returns: Returns:
self: The entity self: The entity
""" """
return cls( return cls(
id=model.id, id=model.id,
content=model.id, content=model.id,
) )
def to_model(self) -> Tag: def to_model(self) -> Tag:
""" """
Create a user model from entity Create a user model from entity
Returns: Returns:
User: A User model for API usage User: A User model for API usage
""" """
return Tag( return Tag(
id=self.id, id=self.id,
content=self.content, content=self.content,
) )

View File

@ -1,88 +1,88 @@
""" Defines the table for storing users """ """ Defines the table for storing users """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime, ARRAY, Enum from sqlalchemy import Integer, String, DateTime, ARRAY, Enum
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import enums for Role and Program # Import enums for Role and Program
from backend.models.enum_for_models import UserTypeEnum, ProgramTypeEnum from backend.models.enum_for_models import UserTypeEnum, ProgramTypeEnum
# Import models for User methods # Import models for User methods
from ..models.user_model import User from ..models.user_model import User
from typing import Self from typing import Self
class UserEntity(EntityBase): class UserEntity(EntityBase):
"""Serves as the database model for User table""" """Serves as the database model for User table"""
# set table name to user in the database # set table name to user in the database
__tablename__ = "user" __tablename__ = "user"
# set fields or 'columns' for the user table # set fields or 'columns' for the user table
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
username: Mapped[str] = mapped_column( username: Mapped[str] = mapped_column(
String(32), nullable=False, default="", unique=True String(32), nullable=False, default="", unique=True
) )
role: Mapped[UserTypeEnum] = mapped_column(Enum(UserTypeEnum), nullable=False) role: Mapped[UserTypeEnum] = mapped_column(Enum(UserTypeEnum), nullable=False)
email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True) email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
program: Mapped[list[ProgramTypeEnum]] = mapped_column( program: Mapped[list[ProgramTypeEnum]] = mapped_column(
ARRAY(Enum(ProgramTypeEnum)), nullable=False ARRAY(Enum(ProgramTypeEnum)), nullable=False
) )
experience: Mapped[int] = mapped_column(Integer, nullable=False) experience: Mapped[int] = mapped_column(Integer, nullable=False)
group: Mapped[str] = mapped_column(String(50)) group: Mapped[str] = mapped_column(String(50))
uuid: Mapped[str] = mapped_column(String, nullable=True) uuid: Mapped[str] = mapped_column(String, nullable=True)
@classmethod @classmethod
def from_model(cls, model: User) -> Self: def from_model(cls, model: User) -> Self:
""" """
Create a user entity from model Create a user entity from model
Args: model (User): the model to create the entity from Args: model (User): the model to create the entity from
Returns: Returns:
self: The entity self: The entity
""" """
return cls( return cls(
id=model.id, id=model.id,
created_at=model.created_at, created_at=model.created_at,
username=model.username, username=model.username,
role=model.role, role=model.role,
email=model.email, email=model.email,
program=model.program, program=model.program,
experience=model.experience, experience=model.experience,
group=model.group, group=model.group,
uuid=model.uuid, uuid=model.uuid,
) )
def to_model(self) -> User: def to_model(self) -> User:
""" """
Create a user model from entity Create a user model from entity
Returns: Returns:
User: A User model for API usage User: A User model for API usage
""" """
return User( return User(
id=self.id, id=self.id,
username=self.username, username=self.username,
email=self.email, email=self.email,
experience=self.experience, experience=self.experience,
group=self.group, group=self.group,
program=self.program, program=self.program,
role=self.role, role=self.role,
created_at=self.created_at, created_at=self.created_at,
uuid=self.uuid, uuid=self.uuid,
) )

View File

@ -1,9 +1,9 @@
from enum import Enum from enum import Enum
class Role_Enum(Enum): class Role_Enum(Enum):
"""Determine role for User""" """Determine role for User"""
ADMIN = "ADMIN" ADMIN = "ADMIN"
EMPLOYEE = "EMPLOYEE" EMPLOYEE = "EMPLOYEE"
VOLUNTEER = "VOLUNTEER" VOLUNTEER = "VOLUNTEER"

View File

@ -1,34 +1,34 @@
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from .api import user, health, service, resource from .api import user, health, service, resource
description = """ description = """
Welcome to the **COMPASS** RESTful Application Programming Interface. Welcome to the **COMPASS** RESTful Application Programming Interface.
""" """
app = FastAPI( app = FastAPI(
title="Compass API", title="Compass API",
version="0.0.1", version="0.0.1",
description=description, description=description,
openapi_tags=[ openapi_tags=[
user.openapi_tags, user.openapi_tags,
health.openapi_tags, health.openapi_tags,
service.openapi_tags, service.openapi_tags,
resource.openapi_tags, resource.openapi_tags,
], ],
) )
app.add_middleware(GZipMiddleware) app.add_middleware(GZipMiddleware)
feature_apis = [user, health, service, resource] feature_apis = [user, health, service, resource]
for feature_api in feature_apis: for feature_api in feature_apis:
app.include_router(feature_api.api) app.include_router(feature_api.api)
# Add application-wide exception handling middleware for commonly encountered API Exceptions # Add application-wide exception handling middleware for commonly encountered API Exceptions
@app.exception_handler(Exception) @app.exception_handler(Exception)
def permission_exception_handler(request: Request, e: Exception): def permission_exception_handler(request: Request, e: Exception):
return JSONResponse(status_code=403, content={"message": str(e)}) return JSONResponse(status_code=403, content={"message": str(e)})

View File

@ -1,13 +1,13 @@
from enum import Enum from enum import Enum
class ProgramTypeEnum(str, Enum): class ProgramTypeEnum(str, Enum):
DOMESTIC = "DOMESTIC" DOMESTIC = "DOMESTIC"
ECONOMIC = "ECONOMIC" ECONOMIC = "ECONOMIC"
COMMUNITY = "COMMUNITY" COMMUNITY = "COMMUNITY"
class UserTypeEnum(str, Enum): class UserTypeEnum(str, Enum):
ADMIN = "ADMIN" ADMIN = "ADMIN"
EMPLOYEE = "EMPLOYEE" EMPLOYEE = "EMPLOYEE"
VOLUNTEER = "VOLUNTEER" VOLUNTEER = "VOLUNTEER"

View File

@ -1,15 +1,15 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from .enum_for_models import ProgramTypeEnum from .enum_for_models import ProgramTypeEnum
class Resource(BaseModel): class Resource(BaseModel):
id: int | None = None id: int | None = None
name: str = Field(..., max_length=150, description="The name of the resource") name: str = Field(..., max_length=150, description="The name of the resource")
summary: str = Field(..., max_length=300, description="The summary of the resource") summary: str = Field(..., max_length=300, description="The summary of the resource")
link: str = Field(..., max_length=150, description="link to the resource") link: str = Field(..., max_length=150, description="link to the resource")
program: ProgramTypeEnum program: ProgramTypeEnum
created_at: Optional[datetime] created_at: Optional[datetime]

View File

@ -1,13 +1,13 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from .tag_model import Tag from .tag_model import Tag
from .resource_model import Resource from .resource_model import Resource
class ResourceTag(Resource, BaseModel): class ResourceTag(Resource, BaseModel):
id: int | None = None id: int | None = None
resourceid: int | None = None resourceid: int | None = None
tagid: List[Tag] tagid: List[Tag]

View File

@ -1,16 +1,16 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from .enum_for_models import ProgramTypeEnum from .enum_for_models import ProgramTypeEnum
class Service(BaseModel): class Service(BaseModel):
id: int | None = None id: int | None = None
created_at: datetime | None = None created_at: datetime | None = None
name: str name: str
status: str status: str
summary: str summary: str
requirements: List[str] requirements: List[str]
program: ProgramTypeEnum program: ProgramTypeEnum

View File

@ -1,19 +1,19 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from .enum_for_models import ProgramTypeEnum from .enum_for_models import ProgramTypeEnum
from .enum_for_models import UserTypeEnum from .enum_for_models import UserTypeEnum
from .service_model import Service from .service_model import Service
from .tag_model import Tag from .tag_model import Tag
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime from datetime import datetime
class ServiceTag(Service, BaseModel): class ServiceTag(Service, BaseModel):
id: int | None = None id: int | None = None
serviceid: int | None = None serviceid: int | None = None
tagId: List[Tag] tagId: List[Tag]

View File

@ -1,13 +1,13 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
class Tag(BaseModel): class Tag(BaseModel):
id: int | None = None id: int | None = None
content: str = Field( content: str = Field(
..., max_length=600, description="content associated with the tag" ..., max_length=600, description="content associated with the tag"
) )
created_at: datetime | None = None created_at: datetime | None = None

View File

@ -1,18 +1,18 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from .enum_for_models import UserTypeEnum, ProgramTypeEnum from .enum_for_models import UserTypeEnum, ProgramTypeEnum
class User(BaseModel): class User(BaseModel):
id: int | None = None id: int | None = None
username: str = Field(..., description="The username of the user") username: str = Field(..., description="The username of the user")
email: str = Field(..., description="The e-mail of the user") email: str = Field(..., description="The e-mail of the user")
experience: int = Field(..., description="Years of Experience of the User") experience: int = Field(..., description="Years of Experience of the User")
group: str group: str
program: List[ProgramTypeEnum] program: List[ProgramTypeEnum]
role: UserTypeEnum role: UserTypeEnum
created_at: Optional[datetime] created_at: Optional[datetime]
uuid: str | None = None uuid: str | None = None

View File

@ -1,4 +1,4 @@
from .user import UserService from .user import UserService
from .resource import ResourceService from .resource import ResourceService
from .tag import TagService from .tag import TagService
from .service import ServiceService from .service import ServiceService

View File

@ -1,25 +1,25 @@
""" """
This file contains exceptions found in the service layer. This file contains exceptions found in the service layer.
These custom exceptions can then be handled peoperly These custom exceptions can then be handled peoperly
at the API level. at the API level.
""" """
class ResourceNotFoundException(Exception): class ResourceNotFoundException(Exception):
"""ResourceNotFoundException is raised when a user attempts to access a resource that does not exist.""" """ResourceNotFoundException is raised when a user attempts to access a resource that does not exist."""
class UserPermissionException(Exception): class UserPermissionException(Exception):
"""UserPermissionException is raised when a user attempts to perform an action they are not authorized to perform.""" """UserPermissionException is raised when a user attempts to perform an action they are not authorized to perform."""
def __init__(self, action: str, resource: str): def __init__(self, action: str, resource: str):
super().__init__(f"Not authorized to perform `{action}` on `{resource}`") super().__init__(f"Not authorized to perform `{action}` on `{resource}`")
class ServiceNotFoundException(Exception): class ServiceNotFoundException(Exception):
"""Exception for when the service being requested is not in the table.""" """Exception for when the service being requested is not in the table."""
class ProgramNotAssignedException(Exception): class ProgramNotAssignedException(Exception):
"""Exception for when the user does not have correct access for requested services.""" """Exception for when the user does not have correct access for requested services."""

View File

@ -1,27 +1,27 @@
""" """
Verify connectivity to the database from the service layer for health check purposes. Verify connectivity to the database from the service layer for health check purposes.
The production system will regularly check the health of running containers via accessing an API endpoint. The production system will regularly check the health of running containers via accessing an API endpoint.
The API endpoint is backed by this service which executes a simple statement against our backing database. The API endpoint is backed by this service which executes a simple statement against our backing database.
In more complex deployments, where multiple backing services may be depended upon, the health check process In more complex deployments, where multiple backing services may be depended upon, the health check process
would necessarily also become more complex to reflect the health of all subsystems. would necessarily also become more complex to reflect the health of all subsystems.
In this context health does not refer to correctness as much as running, connected, and responsive. In this context health does not refer to correctness as much as running, connected, and responsive.
""" """
from fastapi import Depends from fastapi import Depends
from sqlalchemy import text from sqlalchemy import text
from ..database import Session, db_session from ..database import Session, db_session
class HealthService: class HealthService:
_session: Session _session: Session
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def check(self): def check(self):
stmt = text("SELECT 'OK', NOW()") stmt = text("SELECT 'OK', NOW()")
result = self._session.execute(stmt) result = self._session.execute(stmt)
row = result.all()[0] row = result.all()[0]
return str(f"{row[0]} @ {row[1]}") return str(f"{row[0]} @ {row[1]}")

View File

@ -1,37 +1,37 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..models.user_model import User from ..models.user_model import User
from ..entities.user_entity import UserEntity from ..entities.user_entity import UserEntity
from exceptions import ResourceNotFoundException, UserPermissionException from exceptions import ResourceNotFoundException, UserPermissionException
from ..models.enum_for_models import UserTypeEnum from ..models.enum_for_models import UserTypeEnum
class PermissionsService: class PermissionsService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_role_permissions(self, user: User) -> str: def get_role_permissions(self, user: User) -> str:
""" """
Gets a str group based on the user Gets a str group based on the user
Returns: Returns:
str str
""" """
# Query the resource table with id # Query the resource table with id
obj = ( obj = (
self._session.query(UserEntity) self._session.query(UserEntity)
.filter(UserEntity.id == user.id) .filter(UserEntity.id == user.id)
.one_or_none() .one_or_none()
) )
# Check if result is null # Check if result is null
if obj is None: if obj is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(
f"No user permissions found for user with id: {user.id}" f"No user permissions found for user with id: {user.id}"
) )
return obj.role return obj.role

View File

@ -1,165 +1,165 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select from sqlalchemy import select
from ..models.resource_model import Resource from ..models.resource_model import Resource
from ..entities.resource_entity import ResourceEntity from ..entities.resource_entity import ResourceEntity
from ..models.user_model import User, UserTypeEnum from ..models.user_model import User, UserTypeEnum
from .exceptions import ResourceNotFoundException from .exceptions import ResourceNotFoundException
class ResourceService: class ResourceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_resource_by_user(self, subject: User): def get_resource_by_user(self, subject: User):
"""Resource method getting all of the resources that a user has access to based on role""" """Resource method getting all of the resources that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ResourceEntity) query = select(ResourceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [resource.to_model() for resource in entities] return [resource.to_model() for resource in entities]
else: else:
programs = subject.program programs = subject.program
resources = [] resources = []
for program in programs: for program in programs:
query = select(ResourceEntity).filter(ResourceEntity.program == program) query = select(ResourceEntity).filter(ResourceEntity.program == program)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
for entity in entities: for entity in entities:
resources.append(entity) resources.append(entity)
return [resource.to_model() for resource in resources] return [resource.to_model() for resource in resources]
def create(self, user: User, resource: Resource) -> Resource: def create(self, user: User, resource: Resource) -> Resource:
""" """
Creates a resource based on the input object and adds it to the table if the user has the right permissions. Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
resource: Resource object to add to table resource: Resource object to add to table
Returns: Returns:
Resource: Object added to table Resource: Object added to table
""" """
if resource.role != user.role or resource.group != user.group: if resource.role != user.role or resource.group != user.group:
raise PermissionError( raise PermissionError(
"User does not have permission to add resources in this role or group." "User does not have permission to add resources in this role or group."
) )
resource_entity = ResourceEntity.from_model(resource) resource_entity = ResourceEntity.from_model(resource)
self._session.add(resource_entity) self._session.add(resource_entity)
self._session.commit() self._session.commit()
return resource_entity.to_model() return resource_entity.to_model()
def get_by_id(self, user: User, id: int) -> Resource: def get_by_id(self, user: User, id: int) -> Resource:
""" """
Gets a resource based on the resource id that the user has access to Gets a resource based on the resource id that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
id: int, the id of the resource id: int, the id of the resource
Returns: Returns:
Resource Resource
Raises: Raises:
ResourceNotFoundException: If no resource is found with id ResourceNotFoundException: If no resource is found with id
""" """
resource = ( resource = (
self._session.query(ResourceEntity) self._session.query(ResourceEntity)
.filter( .filter(
ResourceEntity.id == id, ResourceEntity.id == id,
ResourceEntity.role == user.role, ResourceEntity.role == user.role,
ResourceEntity.group == user.group, ResourceEntity.group == user.group,
) )
.one_or_none() .one_or_none()
) )
if resource is None: if resource is None:
raise ResourceNotFoundException(f"No resource found with id: {id}") raise ResourceNotFoundException(f"No resource found with id: {id}")
return resource.to_model() return resource.to_model()
def update(self, user: User, resource: ResourceEntity) -> Resource: def update(self, user: User, resource: ResourceEntity) -> Resource:
""" """
Update the resource if the user has access Update the resource if the user has access
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
resource (ResourceEntity): Resource to update resource (ResourceEntity): Resource to update
Returns: Returns:
Resource: Updated resource object Resource: Updated resource object
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID ResourceNotFoundException: If no resource is found with the corresponding ID
""" """
if resource.role != user.role or resource.group != user.group: if resource.role != user.role or resource.group != user.group:
raise PermissionError( raise PermissionError(
"User does not have permission to update this resource." "User does not have permission to update this resource."
) )
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
if obj is None: if obj is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(
f"No resource found with matching id: {resource.id}" f"No resource found with matching id: {resource.id}"
) )
obj.update_from_model(resource) # Assuming an update method exists obj.update_from_model(resource) # Assuming an update method exists
self._session.commit() self._session.commit()
return obj.to_model() return obj.to_model()
def delete(self, user: User, id: int) -> None: def delete(self, user: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
id: int, a unique resource id id: int, a unique resource id
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding id ResourceNotFoundException: If no resource is found with the corresponding id
""" """
resource = ( resource = (
self._session.query(ResourceEntity) self._session.query(ResourceEntity)
.filter( .filter(
ResourceEntity.id == id, ResourceEntity.id == id,
ResourceEntity.role == user.role, ResourceEntity.role == user.role,
ResourceEntity.group == user.group, ResourceEntity.group == user.group,
) )
.one_or_none() .one_or_none()
) )
if resource is None: if resource is None:
raise ResourceNotFoundException(f"No resource found with matching id: {id}") raise ResourceNotFoundException(f"No resource found with matching id: {id}")
self._session.delete(resource) self._session.delete(resource)
self._session.commit() self._session.commit()
def get_by_slug(self, user: User, search_string: str) -> list[Resource]: 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 Get a list of resources given a search string that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
search_string: a string to search resources by search_string: a string to search resources by
Returns: Returns:
list[Resource]: list of resources relating to the string list[Resource]: list of resources relating to the string
Raises: Raises:
ResourceNotFoundException if no resource is found with the corresponding slug ResourceNotFoundException if no resource is found with the corresponding slug
""" """
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
ResourceEntity.title.ilike(f"%{search_string}%"), ResourceEntity.title.ilike(f"%{search_string}%"),
ResourceEntity.role == user.role, ResourceEntity.role == user.role,
ResourceEntity.group == user.group, ResourceEntity.group == user.group,
) )
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]

View File

@ -1,127 +1,127 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import func, select, and_, func, or_, exists, or_ from sqlalchemy import func, select, and_, func, or_, exists, or_
from backend.models.service_model import Service from backend.models.service_model import Service
from backend.models.user_model import User from backend.models.user_model import User
from backend.entities.service_entity import ServiceEntity from backend.entities.service_entity import ServiceEntity
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
from backend.services.exceptions import ( from backend.services.exceptions import (
ServiceNotFoundException, ServiceNotFoundException,
ProgramNotAssignedException, ProgramNotAssignedException,
) )
class ServiceService: class ServiceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]: def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
"""Service method getting services belonging to a particular program.""" """Service method getting services belonging to a particular program."""
query = select(ServiceEntity).filter(ServiceEntity.program == program) query = select(ServiceEntity).filter(ServiceEntity.program == program)
entities = self._session.scalars(query) entities = self._session.scalars(query)
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]
def get_service_by_id(self, id: int) -> Service: def get_service_by_id(self, id: int) -> Service:
"""Service method getting services by id.""" """Service method getting services by id."""
query = select(ServiceEntity).filter(ServiceEntity.id == id) query = select(ServiceEntity).filter(ServiceEntity.id == id)
entity = self._session.scalars(query).one_or_none() entity = self._session.scalars(query).one_or_none()
if entity is None: if entity is None:
raise ServiceNotFoundException(f"Service with id: {id} does not exist") raise ServiceNotFoundException(f"Service with id: {id} does not exist")
return entity.to_model() return entity.to_model()
def get_service_by_name(self, name: str) -> Service: def get_service_by_name(self, name: str) -> Service:
"""Service method getting services by id.""" """Service method getting services by id."""
query = select(ServiceEntity).filter(ServiceEntity.name == name) query = select(ServiceEntity).filter(ServiceEntity.name == name)
entity = self._session.scalars(query).one_or_none() entity = self._session.scalars(query).one_or_none()
if entity is None: if entity is None:
raise ServiceNotFoundException(f"Service with name: {name} does not exist") raise ServiceNotFoundException(f"Service with name: {name} does not exist")
return entity.to_model() return entity.to_model()
def get_service_by_user(self, subject: User): def get_service_by_user(self, subject: User):
"""Service method getting all of the services that a user has access to based on role""" """Service method getting all of the services that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ServiceEntity) query = select(ServiceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [service.to_model() for service in entities] return [service.to_model() for service in entities]
else: else:
programs = subject.program programs = subject.program
services = [] services = []
for program in programs: for program in programs:
query = select(ServiceEntity).filter(ServiceEntity.program == program) query = select(ServiceEntity).filter(ServiceEntity.program == program)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
for entity in entities: for entity in entities:
services.append(entity) services.append(entity)
return [service.to_model() for service in services] return [service.to_model() for service in services]
def get_all(self, subject: User) -> list[Service]: def get_all(self, subject: User) -> list[Service]:
"""Service method retrieving all of the services in the table.""" """Service method retrieving all of the services in the table."""
if subject.role == UserTypeEnum.VOLUNTEER: if subject.role == UserTypeEnum.VOLUNTEER:
raise ProgramNotAssignedException( raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all" f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
) )
query = select(ServiceEntity) query = select(ServiceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [service.to_model() for service in entities] return [service.to_model() for service in entities]
def create(self, subject: User, service: Service) -> Service: def create(self, subject: User, service: Service) -> Service:
"""Creates/adds a service to the table.""" """Creates/adds a service to the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException( raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot create service" f"User is not {UserTypeEnum.ADMIN}, cannot create service"
) )
service_entity = ServiceEntity.from_model(service) service_entity = ServiceEntity.from_model(service)
self._session.add(service_entity) self._session.add(service_entity)
self._session.commit() self._session.commit()
return service_entity.to_model() return service_entity.to_model()
def update(self, subject: User, service: Service) -> Service: def update(self, subject: User, service: Service) -> Service:
"""Updates a service if in the table.""" """Updates a service if in the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException( raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
service_entity = self._session.get(ServiceEntity, service.id) service_entity = self._session.get(ServiceEntity, service.id)
if service_entity is None: if service_entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
service_entity.name = service.name service_entity.name = service.name
service_entity.status = service.status service_entity.status = service.status
service_entity.summary = service.summary service_entity.summary = service.summary
service_entity.requirements = service.requirements service_entity.requirements = service.requirements
service_entity.program = service.program service_entity.program = service.program
self._session.commit() self._session.commit()
return service_entity.to_model() return service_entity.to_model()
def delete(self, subject: User, service: Service) -> None: def delete(self, subject: User, service: Service) -> None:
"""Deletes a service from the table.""" """Deletes a service from the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
service_entity = self._session.get(ServiceEntity, service.id) service_entity = self._session.get(ServiceEntity, service.id)
if service_entity is None: if service_entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
self._session.delete(service_entity) self._session.delete(service_entity)
self._session.commit() self._session.commit()

View File

@ -1,20 +1,20 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..models.tag_model import Tag from ..models.tag_model import Tag
from ..entities.tag_entity import TagEntity from ..entities.tag_entity import TagEntity
from sqlalchemy import select from sqlalchemy import select
class TagService: class TagService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def all(self) -> list[Tag]: def all(self) -> list[Tag]:
"""Returns a list of all Tags""" """Returns a list of all Tags"""
query = select(TagEntity) query = select(TagEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]

View File

@ -1,119 +1,119 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..entities.user_entity import UserEntity from ..entities.user_entity import UserEntity
from ..models.user_model import User from ..models.user_model import User
from sqlalchemy import select from sqlalchemy import select
class UserService: class UserService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_user_by_id(self, id: int) -> User: def get_user_by_id(self, id: int) -> User:
""" """
Gets a user by id from the database Gets a user by id from the database
Returns: A User Pydantic model Returns: A User Pydantic model
""" """
query = select(UserEntity).where(UserEntity.id == id) query = select(UserEntity).where(UserEntity.id == id)
user_entity: UserEntity | None = self._session.scalar(query) user_entity: UserEntity | None = self._session.scalar(query)
if user_entity is None: if user_entity is None:
raise Exception(f"No user found with matching id: {id}") raise Exception(f"No user found with matching id: {id}")
return user_entity.to_model() return user_entity.to_model()
def get_user_by_uuid(self, uuid: str) -> User: def get_user_by_uuid(self, uuid: str) -> User:
""" """
Gets a user by uuid from the database Gets a user by uuid from the database
Returns: A User Pydantic model Returns: A User Pydantic model
""" """
query = select(UserEntity).where(UserEntity.uuid == uuid) query = select(UserEntity).where(UserEntity.uuid == uuid)
user_entity: UserEntity | None = self._session.scalar(query) user_entity: UserEntity | None = self._session.scalar(query)
if user_entity is None: if user_entity is None:
raise Exception(f"No user found with matching uuid: {uuid}") raise Exception(f"No user found with matching uuid: {uuid}")
return user_entity.to_model() return user_entity.to_model()
def all(self) -> list[User]: def all(self) -> list[User]:
""" """
Returns a list of all Users Returns a list of all Users
""" """
query = select(UserEntity) query = select(UserEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]
def create(self, user: User) -> User: def create(self, user: User) -> User:
""" """
Creates a new User Entity and adds to database Creates a new User Entity and adds to database
Args: User model Args: User model
Returns: User model Returns: User model
""" """
try: try:
if (user.id != None): if (user.id != None):
user = self.get_user_by_id(user.id) user = self.get_user_by_id(user.id)
except: except:
# if does not exist, create new object # if does not exist, create new object
user_entity = UserEntity.from_model(user) user_entity = UserEntity.from_model(user)
# add new user to table # add new user to table
self._session.add(user_entity) self._session.add(user_entity)
self._session.commit() self._session.commit()
finally: finally:
# return added object # return added object
return user return user
def delete(self, user: User) -> None: def delete(self, user: User) -> None:
""" """
Delete a user Delete a user
Args: the user to delete Args: the user to delete
Returns: none Returns: none
""" """
obj = self._session.get(UserEntity, user.id) obj = self._session.get(UserEntity, user.id)
if obj is None: if obj is None:
raise Exception(f"No matching user found") raise Exception(f"No matching user found")
self._session.delete(obj) self._session.delete(obj)
self._session.commit() self._session.commit()
def update(self, user: User) -> User: def update(self, user: User) -> User:
""" """
Updates a user Updates a user
Args: User to be updated Args: User to be updated
Returns: The updated User Returns: The updated User
""" """
obj = self._session.get(UserEntity, user.id) obj = self._session.get(UserEntity, user.id)
if obj is None: if obj is None:
raise Exception(f"No matching user found") raise Exception(f"No matching user found")
obj.username = user.username obj.username = user.username
obj.role = user.role obj.role = user.role
obj.email = user.email obj.email = user.email
obj.program = user.program obj.program = user.program
obj.experience = user.experience obj.experience = user.experience
obj.group = user.group obj.group = user.group
self._session.commit() self._session.commit()
return obj.to_model() return obj.to_model()

View File

@ -1,53 +1,53 @@
# Testing # Testing
## Backend ## Backend
### Organization ### Organization
Tests for `backend` code use [Pytest](https://doc.pytest.org/) and are organized in the `backend/test` directory Tests for `backend` code use [Pytest](https://doc.pytest.org/) and are organized in the `backend/test` directory
with subdirectories that mirror the package structure. with subdirectories that mirror the package structure.
The file `backend/test/conftest.py` defines fixtures for automatically setting up and tearing down a test database for backend services to use. The file `backend/test/conftest.py` defines fixtures for automatically setting up and tearing down a test database for backend services to use.
At present, we do not have automated front-end testing instrumented; this remains a goal. At present, we do not have automated front-end testing instrumented; this remains a goal.
### Pytest CLI ### Pytest CLI
The `pytest` command-line program will run all tests in the command-line. The `pytest` command-line program will run all tests in the command-line.
To see `print` output, run Pytest with the special extra output flag `pytest -rP`. To see `print` output, run Pytest with the special extra output flag `pytest -rP`.
To limit the scope of your tests to a specific file, include the path to the file following the command, eg: To limit the scope of your tests to a specific file, include the path to the file following the command, eg:
`pytest backend/test/services/user_test.py` `pytest backend/test/services/user_test.py`
To run a specific test within a test suite, use the [`-k` option of `pytest`](https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name) to match all or part of the filtered test name(s): To run a specific test within a test suite, use the [`-k` option of `pytest`](https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name) to match all or part of the filtered test name(s):
`pytest backend/test/services/user_test.py -k test_get` `pytest backend/test/services/user_test.py -k test_get`
### Pytest VSCode with Debugger ### Pytest VSCode with Debugger
VSCode's Python plugin has great support for testing. Click the test tube icon, configure VSCode to use Pytest and select the workspace. VSCode's Python plugin has great support for testing. Click the test tube icon, configure VSCode to use Pytest and select the workspace.
When you refresh, you will see tests you can run individually, or in the debugger and with breakpoints. When you encounter a bug or failing test and having a difficult time pinning down exactly why it is failing, developing the instinct to run the test in the VSCode debugger, setting a break point, and stepping through is encouraged. When you refresh, you will see tests you can run individually, or in the debugger and with breakpoints. When you encounter a bug or failing test and having a difficult time pinning down exactly why it is failing, developing the instinct to run the test in the VSCode debugger, setting a break point, and stepping through is encouraged.
For more, see the [official documentation](https://code.visualstudio.com/docs/python/testing). For more, see the [official documentation](https://code.visualstudio.com/docs/python/testing).
### Code Coverage ### Code Coverage
We expect 100% test coverage of backend services code and as much coverage for other code in the backend. We expect 100% test coverage of backend services code and as much coverage for other code in the backend.
To generate a test coverage report, run the following command in your development container: To generate a test coverage report, run the following command in your development container:
`pytest --cov-report html:coverage --cov=backend/services backend/test/services` `pytest --cov-report html:coverage --cov=backend/services backend/test/services`
This command generates a directory with an HTML report. To view it, on your _host machine_, open the `coverage` directory's `index.html` file. Click on the service file you are working on to see the lines not covered by test cases if you are below 100%. After adding test cases that cover the missing lines, rerun the coverage command to generate a new report and confirm your progress. This command generates a directory with an HTML report. To view it, on your _host machine_, open the `coverage` directory's `index.html` file. Click on the service file you are working on to see the lines not covered by test cases if you are below 100%. After adding test cases that cover the missing lines, rerun the coverage command to generate a new report and confirm your progress.
## Writing Tests ## Writing Tests
1. Depending on what you are writing tests for, create the testing file in the associated directory (e.g. writing tests for services should be in the backend/tests/services directory 1. Depending on what you are writing tests for, create the testing file in the associated directory (e.g. writing tests for services should be in the backend/tests/services directory
2. Name the file as [tested_item]\_test.py and any functions inside the file should be prefixed with test\_[tested_function] to be recognized by **pytest** 2. Name the file as [tested_item]\_test.py and any functions inside the file should be prefixed with test\_[tested_function] to be recognized by **pytest**
3. Tests should be created in a way to test the main functionality, edge cases, and error handling (look at test's on the csxl repo for inspiration) 3. Tests should be created in a way to test the main functionality, edge cases, and error handling (look at test's on the csxl repo for inspiration)
4. Run specific functions by running this command while in the /workspace directory 4. Run specific functions by running this command while in the /workspace directory
- pytest backend/test/[test_directory]/[test_file]::[function_name] -s - pytest backend/test/[test_directory]/[test_file]::[function_name] -s
- -s flag allows you to show print statements in the console which is defaulted to not show - -s flag allows you to show print statements in the console which is defaulted to not show

View File

@ -1,4 +1,4 @@

View File

@ -1,26 +1,26 @@
"""Fixtures used for testing the core services.""" """Fixtures used for testing the core services."""
import pytest import pytest
from unittest.mock import create_autospec from unittest.mock import create_autospec
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...services import UserService from ...services import UserService
from ...services import TagService from ...services import TagService
from ...services import ServiceService from ...services import ServiceService
@pytest.fixture() @pytest.fixture()
def user_svc(session: Session): def user_svc(session: Session):
"""This fixture is used to test the UserService class""" """This fixture is used to test the UserService class"""
return UserService(session) return UserService(session)
@pytest.fixture() @pytest.fixture()
def tag_svc(session: Session): def tag_svc(session: Session):
"""This fixture is used to test the TagService class""" """This fixture is used to test the TagService class"""
return TagService(session) return TagService(session)
@pytest.fixture() @pytest.fixture()
def service_svc(session: Session): def service_svc(session: Session):
"""This fixture is used to test the ServiceService class""" """This fixture is used to test the ServiceService class"""
return ServiceService(session) return ServiceService(session)

View File

@ -1,315 +1,315 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from datetime import datetime from datetime import datetime
from ...entities import ResourceEntity from ...entities import ResourceEntity
from ...models.enum_for_models import ProgramTypeEnum from ...models.enum_for_models import ProgramTypeEnum
from ...models.resource_model import Resource from ...models.resource_model import Resource
resource1 = Resource( resource1 = Resource(
id=1, id=1,
name="Resource 1", name="Resource 1",
summary="Helpful information for victims of domestic violence", summary="Helpful information for victims of domestic violence",
link="https://example.com/resource1", link="https://example.com/resource1",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 1, 10, 0, 0), created_at=datetime(2023, 6, 1, 10, 0, 0),
) )
resource2 = Resource( resource2 = Resource(
id=2, id=2,
name="Resource 2", name="Resource 2",
summary="Legal assistance resources", summary="Legal assistance resources",
link="https://example.com/resource2", link="https://example.com/resource2",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 2, 12, 30, 0), created_at=datetime(2023, 6, 2, 12, 30, 0),
) )
resource3 = Resource( resource3 = Resource(
id=3, id=3,
name="Resource 3", name="Resource 3",
summary="Financial aid resources", summary="Financial aid resources",
link="https://example.com/resource3", link="https://example.com/resource3",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 3, 15, 45, 0), created_at=datetime(2023, 6, 3, 15, 45, 0),
) )
resource4 = Resource( resource4 = Resource(
id=4, id=4,
name="Resource 4", name="Resource 4",
summary="Counseling and support groups", summary="Counseling and support groups",
link="https://example.com/resource4", link="https://example.com/resource4",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 4, 9, 15, 0), created_at=datetime(2023, 6, 4, 9, 15, 0),
) )
resource5 = Resource( resource5 = Resource(
id=5, id=5,
name="Resource 5", name="Resource 5",
summary="Shelter and housing resources", summary="Shelter and housing resources",
link="https://example.com/resource5", link="https://example.com/resource5",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 5, 11, 30, 0), created_at=datetime(2023, 6, 5, 11, 30, 0),
) )
resources = [resource1, resource2, resource3, resource4, resource5] resources = [resource1, resource2, resource3, resource4, resource5]
resource_1 = Resource( resource_1 = Resource(
id=1, id=1,
name="National Domestic Violence Hotline", name="National Domestic Violence Hotline",
summary="24/7 confidential support for victims of domestic violence", summary="24/7 confidential support for victims of domestic violence",
link="https://www.thehotline.org", link="https://www.thehotline.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 1, 10, 0, 0), created_at=datetime(2023, 6, 1, 10, 0, 0),
) )
resource_2 = Resource( resource_2 = Resource(
id=2, id=2,
name="Legal Aid Society", name="Legal Aid Society",
summary="Free legal assistance for low-income individuals", summary="Free legal assistance for low-income individuals",
link="https://www.legalaidnyc.org", link="https://www.legalaidnyc.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 2, 12, 30, 0), created_at=datetime(2023, 6, 2, 12, 30, 0),
) )
resource_3 = Resource( resource_3 = Resource(
id=3, id=3,
name="Financial Empowerment Center", name="Financial Empowerment Center",
summary="Free financial counseling and education services", summary="Free financial counseling and education services",
link="https://www1.nyc.gov/site/dca/consumers/get-free-financial-counseling.page", link="https://www1.nyc.gov/site/dca/consumers/get-free-financial-counseling.page",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 3, 15, 45, 0), created_at=datetime(2023, 6, 3, 15, 45, 0),
) )
resource_4 = Resource( resource_4 = Resource(
id=4, id=4,
name="National Coalition Against Domestic Violence", name="National Coalition Against Domestic Violence",
summary="Resources and support for victims of domestic violence", summary="Resources and support for victims of domestic violence",
link="https://ncadv.org", link="https://ncadv.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 4, 9, 15, 0), created_at=datetime(2023, 6, 4, 9, 15, 0),
) )
resource_5 = Resource( resource_5 = Resource(
id=5, id=5,
name="Safe Horizon", name="Safe Horizon",
summary="Shelter and support services for victims of violence", summary="Shelter and support services for victims of violence",
link="https://www.safehorizon.org", link="https://www.safehorizon.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 5, 11, 30, 0), created_at=datetime(2023, 6, 5, 11, 30, 0),
) )
resource_6 = Resource( resource_6 = Resource(
id=6, id=6,
name="National Sexual Assault Hotline", name="National Sexual Assault Hotline",
summary="24/7 confidential support for survivors of sexual assault", summary="24/7 confidential support for survivors of sexual assault",
link="https://www.rainn.org", link="https://www.rainn.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 6, 14, 0, 0), created_at=datetime(2023, 6, 6, 14, 0, 0),
) )
resource_7 = Resource( resource_7 = Resource(
id=7, id=7,
name="Victim Compensation Fund", name="Victim Compensation Fund",
summary="Financial assistance for victims of crime", summary="Financial assistance for victims of crime",
link="https://ovc.ojp.gov/program/victim-compensation", link="https://ovc.ojp.gov/program/victim-compensation",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 7, 16, 45, 0), created_at=datetime(2023, 6, 7, 16, 45, 0),
) )
resource_8 = Resource( resource_8 = Resource(
id=8, id=8,
name="Battered Women's Justice Project", name="Battered Women's Justice Project",
summary="Legal and technical assistance for victims of domestic violence", summary="Legal and technical assistance for victims of domestic violence",
link="https://www.bwjp.org", link="https://www.bwjp.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 8, 10, 30, 0), created_at=datetime(2023, 6, 8, 10, 30, 0),
) )
resource_9 = Resource( resource_9 = Resource(
id=9, id=9,
name="National Network to End Domestic Violence", name="National Network to End Domestic Violence",
summary="Advocacy and resources for ending domestic violence", summary="Advocacy and resources for ending domestic violence",
link="https://nnedv.org", link="https://nnedv.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 9, 13, 0, 0), created_at=datetime(2023, 6, 9, 13, 0, 0),
) )
resource_10 = Resource( resource_10 = Resource(
id=10, id=10,
name="Economic Justice Project", name="Economic Justice Project",
summary="Promoting economic security for survivors of domestic violence", summary="Promoting economic security for survivors of domestic violence",
link="https://www.njep.org", link="https://www.njep.org",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 10, 15, 15, 0), created_at=datetime(2023, 6, 10, 15, 15, 0),
) )
resource_11 = Resource( resource_11 = Resource(
id=11, id=11,
name="Domestic Violence Legal Hotline", name="Domestic Violence Legal Hotline",
summary="Free legal advice for victims of domestic violence", summary="Free legal advice for victims of domestic violence",
link="https://www.womenslaw.org/find-help/national/hotlines", link="https://www.womenslaw.org/find-help/national/hotlines",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 11, 9, 0, 0), created_at=datetime(2023, 6, 11, 9, 0, 0),
) )
resource_12 = Resource( resource_12 = Resource(
id=12, id=12,
name="National Resource Center on Domestic Violence", name="National Resource Center on Domestic Violence",
summary="Comprehensive information and resources on domestic violence", summary="Comprehensive information and resources on domestic violence",
link="https://nrcdv.org", link="https://nrcdv.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 12, 11, 30, 0), created_at=datetime(2023, 6, 12, 11, 30, 0),
) )
resource_13 = Resource( resource_13 = Resource(
id=13, id=13,
name="Financial Assistance for Victims of Crime", name="Financial Assistance for Victims of Crime",
summary="Funding for expenses related to victimization", summary="Funding for expenses related to victimization",
link="https://ovc.ojp.gov/program/victim-assistance-funding", link="https://ovc.ojp.gov/program/victim-assistance-funding",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 13, 14, 45, 0), created_at=datetime(2023, 6, 13, 14, 45, 0),
) )
resource_14 = Resource( resource_14 = Resource(
id=14, id=14,
name="National Clearinghouse for the Defense of Battered Women", name="National Clearinghouse for the Defense of Battered Women",
summary="Legal resources and support for battered women", summary="Legal resources and support for battered women",
link="https://www.ncdbw.org", link="https://www.ncdbw.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 14, 10, 0, 0), created_at=datetime(2023, 6, 14, 10, 0, 0),
) )
resource_15 = Resource( resource_15 = Resource(
id=15, id=15,
name="Victim Connect Resource Center", name="Victim Connect Resource Center",
summary="Referral helpline for crime victims", summary="Referral helpline for crime victims",
link="https://victimconnect.org", link="https://victimconnect.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 15, 13, 15, 0), created_at=datetime(2023, 6, 15, 13, 15, 0),
) )
resource_16 = Resource( resource_16 = Resource(
id=16, id=16,
name="Economic Empowerment Program", name="Economic Empowerment Program",
summary="Financial literacy and job readiness training for survivors", summary="Financial literacy and job readiness training for survivors",
link="https://www.purplepurse.com", link="https://www.purplepurse.com",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 16, 16, 30, 0), created_at=datetime(2023, 6, 16, 16, 30, 0),
) )
resource_17 = Resource( resource_17 = Resource(
id=17, id=17,
name="National Domestic Violence Law Project", name="National Domestic Violence Law Project",
summary="Legal information and resources for domestic violence survivors", summary="Legal information and resources for domestic violence survivors",
link="https://www.womenslaw.org", link="https://www.womenslaw.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 17, 9, 45, 0), created_at=datetime(2023, 6, 17, 9, 45, 0),
) )
resource_18 = Resource( resource_18 = Resource(
id=18, id=18,
name="Victim Rights Law Center", name="Victim Rights Law Center",
summary="Free legal services for victims of sexual assault", summary="Free legal services for victims of sexual assault",
link="https://victimrights.org", link="https://victimrights.org",
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
created_at=datetime(2023, 6, 18, 12, 0, 0), created_at=datetime(2023, 6, 18, 12, 0, 0),
) )
resource_19 = Resource( resource_19 = Resource(
id=19, id=19,
name="Financial Justice Project", name="Financial Justice Project",
summary="Advocating for economic justice for survivors of violence", summary="Advocating for economic justice for survivors of violence",
link="https://www.financialjusticeproject.org", link="https://www.financialjusticeproject.org",
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2023, 6, 19, 15, 30, 0), created_at=datetime(2023, 6, 19, 15, 30, 0),
) )
resource_20 = Resource( resource_20 = Resource(
id=20, id=20,
name="National Center on Domestic and Sexual Violence", name="National Center on Domestic and Sexual Violence",
summary="Training and resources to end domestic and sexual violence", summary="Training and resources to end domestic and sexual violence",
link="http://www.ncdsv.org", link="http://www.ncdsv.org",
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 20, 10, 15, 0), created_at=datetime(2023, 6, 20, 10, 15, 0),
) )
resources1 = [ resources1 = [
resource_1, resource_1,
resource_2, resource_2,
resource_3, resource_3,
resource_4, resource_4,
resource_5, resource_5,
resource_6, resource_6,
resource_7, resource_7,
resource_8, resource_8,
resource_9, resource_9,
resource_10, resource_10,
resource_11, resource_11,
resource_12, resource_12,
resource_13, resource_13,
resource_14, resource_14,
resource_15, resource_15,
resource_16, resource_16,
resource_17, resource_17,
resource_18, resource_18,
resource_19, resource_19,
resource_20, resource_20,
] ]
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
def reset_table_id_seq( def reset_table_id_seq(
session: Session, session: Session,
entity: type[DeclarativeBase], entity: type[DeclarativeBase],
entity_id_column: InstrumentedAttribute[int], entity_id_column: InstrumentedAttribute[int],
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
id_column_name = entity_id_column.name id_column_name = entity_id_column.name
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}") sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
session.execute(sql) session.execute(sql)
def insert_test_data(session: Session): def insert_test_data(session: Session):
"""Inserts fake resource data into the test session.""" """Inserts fake resource data into the test session."""
global resources1 global resources1
# Create entities for test resource data # Create entities for test resource data
entities = [] entities = []
for resource in resources1: for resource in resources1:
entity = ResourceEntity.from_model(resource) entity = ResourceEntity.from_model(resource)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources1) + 1) reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources1) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()
def insert_fake_data(session: Session): def insert_fake_data(session: Session):
"""Inserts fake resource data into the test session.""" """Inserts fake resource data into the test session."""
global resources global resources
# Create entities for test resource data # Create entities for test resource data
entities = [] entities = []
for resource in resources: for resource in resources:
entity = ResourceEntity.from_model(resource) entity = ResourceEntity.from_model(resource)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1) reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()

View File

@ -1,78 +1,78 @@
from backend.models.user_model import User from backend.models.user_model import User
from backend.entities.service_entity import ServiceEntity from backend.entities.service_entity import ServiceEntity
from ...models.enum_for_models import ProgramTypeEnum from ...models.enum_for_models import ProgramTypeEnum
from backend.services.service import ServiceService from backend.services.service import ServiceService
from backend.services.exceptions import ServiceNotFoundException from backend.services.exceptions import ServiceNotFoundException
from . import service_test_data from . import service_test_data
from . import user_test_data from . import user_test_data
from .fixtures import service_svc, user_svc from .fixtures import service_svc, user_svc
from backend.models.service_model import Service from backend.models.service_model import Service
import pytest import pytest
def test_list(service_svc: ServiceService): def test_list(service_svc: ServiceService):
service = service_svc.get_all(user_test_data.admin) service = service_svc.get_all(user_test_data.admin)
assert len(service) == len(service_test_data.services) assert len(service) == len(service_test_data.services)
assert isinstance(service[0], Service) assert isinstance(service[0], Service)
def test_get_by_name(service_svc: ServiceService): def test_get_by_name(service_svc: ServiceService):
service = service_svc.get_service_by_name("service 1") service = service_svc.get_service_by_name("service 1")
assert service.name == service_test_data.service1.name assert service.name == service_test_data.service1.name
assert isinstance(service, Service) assert isinstance(service, Service)
def test_get_by_name_not_found(service_svc: ServiceService): def test_get_by_name_not_found(service_svc: ServiceService):
with pytest.raises(ServiceNotFoundException): with pytest.raises(ServiceNotFoundException):
service = service_svc.get_service_by_name("service 12") service = service_svc.get_service_by_name("service 12")
pytest.fail() pytest.fail()
def test_get_service_by_user_admin(service_svc: ServiceService): def test_get_service_by_user_admin(service_svc: ServiceService):
service = service_svc.get_service_by_user(user_test_data.admin) service = service_svc.get_service_by_user(user_test_data.admin)
assert len(service) == len(service_test_data.services) assert len(service) == len(service_test_data.services)
def test_get_service_by_user_volun(service_svc: ServiceService): def test_get_service_by_user_volun(service_svc: ServiceService):
service = service_svc.get_service_by_user(user_test_data.volunteer) service = service_svc.get_service_by_user(user_test_data.volunteer)
assert len(service) == 4 assert len(service) == 4
def test_get_by_program(service_svc: ServiceService): def test_get_by_program(service_svc: ServiceService):
services = service_svc.get_service_by_program(ProgramTypeEnum.COMMUNITY) services = service_svc.get_service_by_program(ProgramTypeEnum.COMMUNITY)
for service in services: for service in services:
assert service.program == ProgramTypeEnum.COMMUNITY assert service.program == ProgramTypeEnum.COMMUNITY
assert isinstance(service, Service) assert isinstance(service, Service)
def test_create(service_svc: ServiceService): def test_create(service_svc: ServiceService):
service = service_svc.create(user_test_data.admin, service_test_data.service7) service = service_svc.create(user_test_data.admin, service_test_data.service7)
assert service.name == service_test_data.service7.name assert service.name == service_test_data.service7.name
assert isinstance(service, Service) assert isinstance(service, Service)
def test_update(service_svc: ServiceService): def test_update(service_svc: ServiceService):
service = service_svc.update(user_test_data.admin, service_test_data.service_6_edit) service = service_svc.update(user_test_data.admin, service_test_data.service_6_edit)
assert service.status == service_test_data.service_6_edit.status assert service.status == service_test_data.service_6_edit.status
assert service.requirements == service_test_data.service_6_edit.requirements assert service.requirements == service_test_data.service_6_edit.requirements
assert isinstance(service, Service) assert isinstance(service, Service)
def test_update_not_found(service_svc: ServiceService): def test_update_not_found(service_svc: ServiceService):
with pytest.raises(ServiceNotFoundException): with pytest.raises(ServiceNotFoundException):
service = service_svc.update( service = service_svc.update(
user_test_data.admin, service_test_data.new_service user_test_data.admin, service_test_data.new_service
) )
pytest.fail() pytest.fail()
def test_delete(service_svc: ServiceService): def test_delete(service_svc: ServiceService):
service_svc.delete(user_test_data.admin, service_test_data.service_6) service_svc.delete(user_test_data.admin, service_test_data.service_6)
services = service_svc.get_all(user_test_data.admin) services = service_svc.get_all(user_test_data.admin)
assert len(services) == len(service_test_data.services) - 1 assert len(services) == len(service_test_data.services) - 1
"""def test_delete_not_found(service_svc: ServiceService): """def test_delete_not_found(service_svc: ServiceService):
with pytest.raises(ServiceNotFoundException): with pytest.raises(ServiceNotFoundException):
service_svc.delete(user_test_data.admin, service_test_data.service_10) service_svc.delete(user_test_data.admin, service_test_data.service_10)
pytest.fail()""" pytest.fail()"""

View File

@ -1,353 +1,353 @@
import pytest import pytest
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...entities import ServiceEntity from ...entities import ServiceEntity
from ...models.enum_for_models import ProgramTypeEnum from ...models.enum_for_models import ProgramTypeEnum
from ...models.service_model import Service from ...models.service_model import Service
service1 = Service( service1 = Service(
id=1, id=1,
name="service 1", name="service 1",
status="open", status="open",
summary="presentation educating community on domestic violence", summary="presentation educating community on domestic violence",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service2 = Service( service2 = Service(
id=2, id=2,
name="service 2", name="service 2",
status="closed", status="closed",
summary="service finding safe places to stay", summary="service finding safe places to stay",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service3 = Service( service3 = Service(
id=3, id=3,
name="service 3", name="service 3",
status="open", status="open",
summary="", summary="",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service4 = Service( service4 = Service(
id=4, id=4,
name="service 4", name="service 4",
status="waitlist", status="waitlist",
summary="community event", summary="community event",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service5 = Service( service5 = Service(
id=5, id=5,
name="service 5", name="service 5",
status="open", status="open",
summary="talk circle for victims of domestic violence", summary="talk circle for victims of domestic violence",
requirements=["18+"], requirements=["18+"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service6 = Service( service6 = Service(
id=6, id=6,
name="service 6", name="service 6",
status="waitlist", status="waitlist",
summary="program offering economic assistance", summary="program offering economic assistance",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_6_edit = Service( service_6_edit = Service(
id=6, id=6,
name="service 6", name="service 6",
status="open", status="open",
summary="program offering economic assistance", summary="program offering economic assistance",
requirements=["18+"], requirements=["18+"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service7 = Service( service7 = Service(
id=7, id=7,
name="service 7", name="service 7",
status="waitlist", status="waitlist",
summary="insert generic description", summary="insert generic description",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
new_service = Service( new_service = Service(
id=8, id=8,
name="new service", name="new service",
status="open", status="open",
summary="insert other generic description", summary="insert other generic description",
requirements=[""], requirements=[""],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
services = [service1, service2, service3, service4, service5, service6] services = [service1, service2, service3, service4, service5, service6]
service_1 = Service( service_1 = Service(
id=1, id=1,
name="Crisis Hotline", name="Crisis Hotline",
status="open", status="open",
summary="24/7 support for individuals in crisis", summary="24/7 support for individuals in crisis",
requirements=["Anonymous", "Confidential"], requirements=["Anonymous", "Confidential"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_2 = Service( service_2 = Service(
id=2, id=2,
name="Shelter Placement", name="Shelter Placement",
status="open", status="open",
summary="Emergency shelter for victims of domestic violence", summary="Emergency shelter for victims of domestic violence",
requirements=["Referral required", "Safety assessment"], requirements=["Referral required", "Safety assessment"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_3 = Service( service_3 = Service(
id=3, id=3,
name="Legal Advocacy", name="Legal Advocacy",
status="waitlist", status="waitlist",
summary="Legal support and representation for survivors", summary="Legal support and representation for survivors",
requirements=["Intake required", "Income eligibility"], requirements=["Intake required", "Income eligibility"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_4 = Service( service_4 = Service(
id=4, id=4,
name="Counseling Services", name="Counseling Services",
status="open", status="open",
summary="Individual and group therapy for survivors", summary="Individual and group therapy for survivors",
requirements=["Initial assessment", "Insurance accepted"], requirements=["Initial assessment", "Insurance accepted"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_5 = Service( service_5 = Service(
id=5, id=5,
name="Financial Assistance", name="Financial Assistance",
status="open", status="open",
summary="Emergency funds for survivors in need", summary="Emergency funds for survivors in need",
requirements=["Application required", "Proof of income"], requirements=["Application required", "Proof of income"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_6 = Service( service_6 = Service(
id=6, id=6,
name="Housing Assistance", name="Housing Assistance",
status="waitlist", status="waitlist",
summary="Support for finding safe and affordable housing", summary="Support for finding safe and affordable housing",
requirements=["Referral required", "Background check"], requirements=["Referral required", "Background check"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_7 = Service( service_7 = Service(
id=7, id=7,
name="Job Training", name="Job Training",
status="open", status="open",
summary="Employment skills training for survivors", summary="Employment skills training for survivors",
requirements=["Enrollment required", "18+"], requirements=["Enrollment required", "18+"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_8 = Service( service_8 = Service(
id=8, id=8,
name="Support Groups", name="Support Groups",
status="open", status="open",
summary="Peer support groups for survivors", summary="Peer support groups for survivors",
requirements=["Registration required", "Confidential"], requirements=["Registration required", "Confidential"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_9 = Service( service_9 = Service(
id=9, id=9,
name="Children's Services", name="Children's Services",
status="open", status="open",
summary="Specialized services for children exposed to domestic violence", summary="Specialized services for children exposed to domestic violence",
requirements=["Parental consent", "Age-appropriate"], requirements=["Parental consent", "Age-appropriate"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_10 = Service( service_10 = Service(
id=10, id=10,
name="Safety Planning", name="Safety Planning",
status="open", status="open",
summary="Personalized safety planning for survivors", summary="Personalized safety planning for survivors",
requirements=["Confidential", "Collaborative"], requirements=["Confidential", "Collaborative"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_11 = Service( service_11 = Service(
id=11, id=11,
name="Community Education", name="Community Education",
status="open", status="open",
summary="Workshops and training on domestic violence prevention", summary="Workshops and training on domestic violence prevention",
requirements=["Open to the public", "Registration preferred"], requirements=["Open to the public", "Registration preferred"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_12 = Service( service_12 = Service(
id=12, id=12,
name="Healthcare Services", name="Healthcare Services",
status="open", status="open",
summary="Medical care and support for survivors", summary="Medical care and support for survivors",
requirements=["Referral required", "Insurance accepted"], requirements=["Referral required", "Insurance accepted"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_13 = Service( service_13 = Service(
id=13, id=13,
name="Transportation Assistance", name="Transportation Assistance",
status="waitlist", status="waitlist",
summary="Help with transportation for survivors", summary="Help with transportation for survivors",
requirements=["Eligibility assessment", "Limited availability"], requirements=["Eligibility assessment", "Limited availability"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_14 = Service( service_14 = Service(
id=14, id=14,
name="Court Accompaniment", name="Court Accompaniment",
status="open", status="open",
summary="Support and advocacy during court proceedings", summary="Support and advocacy during court proceedings",
requirements=["Legal case", "Scheduling required"], requirements=["Legal case", "Scheduling required"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_15 = Service( service_15 = Service(
id=15, id=15,
name="Relocation Assistance", name="Relocation Assistance",
status="waitlist", status="waitlist",
summary="Support for relocating to a safe environment", summary="Support for relocating to a safe environment",
requirements=["Referral required", "Safety assessment"], requirements=["Referral required", "Safety assessment"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_16 = Service( service_16 = Service(
id=16, id=16,
name="Parenting Classes", name="Parenting Classes",
status="open", status="open",
summary="Education and support for parents", summary="Education and support for parents",
requirements=["Open to parents", "Pre-registration required"], requirements=["Open to parents", "Pre-registration required"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_17 = Service( service_17 = Service(
id=17, id=17,
name="Life Skills Training", name="Life Skills Training",
status="open", status="open",
summary="Workshops on various life skills for survivors", summary="Workshops on various life skills for survivors",
requirements=["Enrollment required", "Commitment to attend"], requirements=["Enrollment required", "Commitment to attend"],
program=ProgramTypeEnum.ECONOMIC, program=ProgramTypeEnum.ECONOMIC,
) )
service_18 = Service( service_18 = Service(
id=18, id=18,
name="Advocacy Services", name="Advocacy Services",
status="open", status="open",
summary="Individual advocacy and support for survivors", summary="Individual advocacy and support for survivors",
requirements=["Intake required", "Confidential"], requirements=["Intake required", "Confidential"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
service_19 = Service( service_19 = Service(
id=19, id=19,
name="Volunteer Opportunities", name="Volunteer Opportunities",
status="open", status="open",
summary="Various volunteer roles supporting the organization", summary="Various volunteer roles supporting the organization",
requirements=["Background check", "Training required"], requirements=["Background check", "Training required"],
program=ProgramTypeEnum.COMMUNITY, program=ProgramTypeEnum.COMMUNITY,
) )
service_20 = Service( service_20 = Service(
id=20, id=20,
name="Referral Services", name="Referral Services",
status="open", status="open",
summary="Referrals to community resources and partner agencies", summary="Referrals to community resources and partner agencies",
requirements=["Intake required", "Based on individual needs"], requirements=["Intake required", "Based on individual needs"],
program=ProgramTypeEnum.DOMESTIC, program=ProgramTypeEnum.DOMESTIC,
) )
services1 = [ services1 = [
service_1, service_1,
service_2, service_2,
service_3, service_3,
service_4, service_4,
service_5, service_5,
service_6, service_6,
service_7, service_7,
service_8, service_8,
service_9, service_9,
service_10, service_10,
service_11, service_11,
service_12, service_12,
service_13, service_13,
service_14, service_14,
service_15, service_15,
service_16, service_16,
service_17, service_17,
service_18, service_18,
service_19, service_19,
service_20, service_20,
] ]
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
def reset_table_id_seq( def reset_table_id_seq(
session: Session, session: Session,
entity: type[DeclarativeBase], entity: type[DeclarativeBase],
entity_id_column: InstrumentedAttribute[int], entity_id_column: InstrumentedAttribute[int],
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
id_column_name = entity_id_column.name id_column_name = entity_id_column.name
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}") sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
session.execute(sql) session.execute(sql)
def insert_test_data(session: Session): def insert_test_data(session: Session):
"""Inserts fake service data into the test session.""" """Inserts fake service data into the test session."""
global services1 global services1
# Create entities for test organization data # Create entities for test organization data
entities = [] entities = []
for service in services1: for service in services1:
entity = ServiceEntity.from_model(service) entity = ServiceEntity.from_model(service)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services1) + 1) reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services1) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()
def insert_fake_data(session: Session): def insert_fake_data(session: Session):
"""Inserts fake service data into the test session.""" """Inserts fake service data into the test session."""
global services global services
# Create entities for test organization data # Create entities for test organization data
entities = [] entities = []
for service in services: for service in services:
entity = ServiceEntity.from_model(service) entity = ServiceEntity.from_model(service)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services) + 1) reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()

View File

@ -1,14 +1,14 @@
"""Tests for the TagService class.""" """Tests for the TagService class."""
# PyTest # PyTest
import pytest import pytest
from ...services.tag import TagService from ...services.tag import TagService
from .fixtures import tag_svc from .fixtures import tag_svc
from .tag_test_data import tag1, tag2, tag3 from .tag_test_data import tag1, tag2, tag3
from . import tag_test_data from . import tag_test_data
def test_get_all(tag_svc: TagService): def test_get_all(tag_svc: TagService):
"""Test that all tags can be retrieved.""" """Test that all tags can be retrieved."""
tags = tag_svc.all() tags = tag_svc.all()
assert len(tags) == 3 assert len(tags) == 3

View File

@ -1,72 +1,72 @@
import pytest import pytest
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...models.tag_model import Tag from ...models.tag_model import Tag
from ...entities.tag_entity import TagEntity from ...entities.tag_entity import TagEntity
from datetime import datetime from datetime import datetime
tag1 = Tag(id=1, content="Tag 1", created_at=datetime.now()) tag1 = Tag(id=1, content="Tag 1", created_at=datetime.now())
tag2 = Tag(id=2, content="Tag 2", created_at=datetime.now()) tag2 = Tag(id=2, content="Tag 2", created_at=datetime.now())
tag3 = Tag(id=3, content="Tag 3", created_at=datetime.now()) tag3 = Tag(id=3, content="Tag 3", created_at=datetime.now())
tagToCreate = Tag(id=4, content="Tag 4", created_at=datetime.now()) tagToCreate = Tag(id=4, content="Tag 4", created_at=datetime.now())
tags = [tag1, tag2, tag3] tags = [tag1, tag2, tag3]
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
def reset_table_id_seq( def reset_table_id_seq(
session: Session, session: Session,
entity: type[DeclarativeBase], entity: type[DeclarativeBase],
entity_id_column: InstrumentedAttribute[int], entity_id_column: InstrumentedAttribute[int],
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
id_column_name = entity_id_column.name id_column_name = entity_id_column.name
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}") sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
session.execute(sql) session.execute(sql)
def insert_fake_data(session: Session): def insert_fake_data(session: Session):
"""Inserts fake organization data into the test session.""" """Inserts fake organization data into the test session."""
global tags global tags
# Create entities for test organization data # Create entities for test organization data
entities = [] entities = []
for tag in tags: for tag in tags:
entity = TagEntity.from_model(tag) entity = TagEntity.from_model(tag)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, TagEntity, TagEntity.id, len(tags) + 1) reset_table_id_seq(session, TagEntity, TagEntity.id, len(tags) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fake_data_fixture(session: Session): def fake_data_fixture(session: Session):
"""Insert fake data the session automatically when test is run. """Insert fake data the session automatically when test is run.
Note: Note:
This function runs automatically due to the fixture property `autouse=True`. This function runs automatically due to the fixture property `autouse=True`.
""" """
insert_fake_data(session) insert_fake_data(session)
session.commit() session.commit()
yield yield

View File

@ -1,82 +1,82 @@
"""Tests for the UserService class.""" """Tests for the UserService class."""
# PyTest # PyTest
import pytest import pytest
from ...services import UserService from ...services import UserService
from .fixtures import user_svc from .fixtures import user_svc
from ...models.user_model import User from ...models.user_model import User
from ...models.enum_for_models import ProgramTypeEnum from ...models.enum_for_models import ProgramTypeEnum
from .user_test_data import employee, volunteer, admin, newUser, toDelete from .user_test_data import employee, volunteer, admin, newUser, toDelete
from . import user_test_data from . import user_test_data
def test_create(user_svc: UserService): def test_create(user_svc: UserService):
"""Test creating a user""" """Test creating a user"""
user1 = user_svc.create(admin) user1 = user_svc.create(admin)
print(user1) print(user1)
assert user1 is not None assert user1 is not None
assert user1.id is not None assert user1.id is not None
def test_create_id_exists(user_svc: UserService): def test_create_id_exists(user_svc: UserService):
"""Test creating a user with id conflict""" """Test creating a user with id conflict"""
user1 = user_svc.create(volunteer) user1 = user_svc.create(volunteer)
assert user1 is not None assert user1 is not None
assert user1.id is not None assert user1.id is not None
def test_get_all(user_svc: UserService): def test_get_all(user_svc: UserService):
"""Test that all users can be retrieved.""" """Test that all users can be retrieved."""
users = user_svc.all() users = user_svc.all()
assert len(users) == 4 assert len(users) == 4
def test_get_user_by_id(user_svc: UserService): def test_get_user_by_id(user_svc: UserService):
"""Test getting a user by an id""" """Test getting a user by an id"""
if volunteer.id != None: if volunteer.id != None:
user = user_svc.get_user_by_id(volunteer.id) user = user_svc.get_user_by_id(volunteer.id)
assert user is not None assert user is not None
assert user.id is not None assert user.id is not None
def test_get_user_by_id_nonexistent(user_svc: UserService): def test_get_user_by_id_nonexistent(user_svc: UserService):
"""Test getting a user by id that does not exist""" """Test getting a user by id that does not exist"""
with pytest.raises(Exception): with pytest.raises(Exception):
user_svc.get_by_id(100) user_svc.get_by_id(100)
def test_delete_user(user_svc: UserService): def test_delete_user(user_svc: UserService):
"""Test deleting a user""" """Test deleting a user"""
user_svc.delete(toDelete) user_svc.delete(toDelete)
with pytest.raises(Exception): with pytest.raises(Exception):
user_svc.get_user_by_id(toDelete.id) user_svc.get_user_by_id(toDelete.id)
def test_delete_user_nonexistent(user_svc: UserService): def test_delete_user_nonexistent(user_svc: UserService):
"""Test deleting a user that does not exist""" """Test deleting a user that does not exist"""
with pytest.raises(Exception): with pytest.raises(Exception):
user_svc.delete(newUser) user_svc.delete(newUser)
def test_update_user(user_svc: UserService): def test_update_user(user_svc: UserService):
"""Test updating a user """Test updating a user
Updating volunteer Updating volunteer
""" """
user = user_svc.get_user_by_id(volunteer.id) user = user_svc.get_user_by_id(volunteer.id)
assert user is not None assert user is not None
user.username = "volunteer 1" user.username = "volunteer 1"
user.email = "newemail@compass.com" user.email = "newemail@compass.com"
updated_user = user_svc.update(user) updated_user = user_svc.update(user)
assert updated_user is not None assert updated_user is not None
assert updated_user.id == user.id assert updated_user.id == user.id
assert updated_user.username == "volunteer 1" assert updated_user.username == "volunteer 1"
assert updated_user.email == "newemail@compass.com" assert updated_user.email == "newemail@compass.com"
def test_update_user_nonexistent(user_svc: UserService): def test_update_user_nonexistent(user_svc: UserService):
"""Test updated a user that does not exist""" """Test updated a user that does not exist"""
with pytest.raises(Exception): with pytest.raises(Exception):
user_svc.update(newUser) user_svc.update(newUser)

View File

@ -1,196 +1,196 @@
import pytest import pytest
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...models.user_model import User from ...models.user_model import User
# import model enums instead # import model enums instead
from ...models.enum_for_models import UserTypeEnum, ProgramTypeEnum from ...models.enum_for_models import UserTypeEnum, ProgramTypeEnum
from ...entities.user_entity import UserEntity from ...entities.user_entity import UserEntity
from datetime import datetime from datetime import datetime
programs = ProgramTypeEnum programs = ProgramTypeEnum
roles = UserTypeEnum roles = UserTypeEnum
volunteer = User( volunteer = User(
id=1, id=1,
uuid="test1", uuid="test1",
username="volunteer", username="volunteer",
email="volunteer@compass.com", email="volunteer@compass.com",
experience=1, experience=1,
group="volunteers", group="volunteers",
program=[programs.COMMUNITY, programs.ECONOMIC], program=[programs.COMMUNITY, programs.ECONOMIC],
created_at=datetime.now(), created_at=datetime.now(),
role=UserTypeEnum.VOLUNTEER, role=UserTypeEnum.VOLUNTEER,
) )
employee = User( employee = User(
id=2, id=2,
uuid="test2", uuid="test2",
username="employee", username="employee",
email="employee@compass.com", email="employee@compass.com",
experience=5, experience=5,
group="employees", group="employees",
program=[programs.DOMESTIC, programs.ECONOMIC], program=[programs.DOMESTIC, programs.ECONOMIC],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.EMPLOYEE, role=roles.EMPLOYEE,
) )
admin = User( admin = User(
id=3, id=3,
uuid="test3", uuid="test3",
username="admin", username="admin",
email="admin@compass.com", email="admin@compass.com",
experience=10, experience=10,
group="admin", group="admin",
program=[ program=[
programs.ECONOMIC, programs.ECONOMIC,
programs.DOMESTIC, programs.DOMESTIC,
programs.COMMUNITY, programs.COMMUNITY,
], ],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.ADMIN, role=roles.ADMIN,
) )
newUser = User( newUser = User(
id=4, id=4,
username="new", username="new",
uuid="test4", uuid="test4",
email="new@compass.com", email="new@compass.com",
experience=1, experience=1,
group="volunteer", group="volunteer",
program=[programs.ECONOMIC], program=[programs.ECONOMIC],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.VOLUNTEER, role=roles.VOLUNTEER,
) )
toDelete = User( toDelete = User(
id=5, id=5,
username="delete", username="delete",
email="delete@compass.com", email="delete@compass.com",
experience=0, experience=0,
group="none", group="none",
program=[programs.COMMUNITY], program=[programs.COMMUNITY],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.VOLUNTEER, role=roles.VOLUNTEER,
) )
users = [volunteer, employee, admin, toDelete] users = [volunteer, employee, admin, toDelete]
admin1 = User( admin1 = User(
username="Prajwal Moharana", username="Prajwal Moharana",
uuid="acc6e112-d296-4739-a80c-b89b2933e50b", uuid="acc6e112-d296-4739-a80c-b89b2933e50b",
email="root@compass.com", email="root@compass.com",
experience=10, experience=10,
group="admin", group="admin",
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY], program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.ADMIN, role=roles.ADMIN,
) )
employee1 = User( employee1 = User(
username="Mel Ho", username="Mel Ho",
uuid="c5fcff86-3deb-4d09-9f60-9b529e40161a", uuid="c5fcff86-3deb-4d09-9f60-9b529e40161a",
email="employee@compass.com", email="employee@compass.com",
experience=5, experience=5,
group="employee", group="employee",
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY], program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.EMPLOYEE, role=roles.EMPLOYEE,
) )
volunteer1 = User( volunteer1 = User(
username="Pranav Wagh", username="Pranav Wagh",
uuid="1d2e114f-b286-4464-8528-d177dc226b09", uuid="1d2e114f-b286-4464-8528-d177dc226b09",
email="volunteer1@compass.com", email="volunteer1@compass.com",
experience=2, experience=2,
group="volunteer", group="volunteer",
program=[programs.DOMESTIC], program=[programs.DOMESTIC],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.VOLUNTEER, role=roles.VOLUNTEER,
) )
volunteer2 = User( volunteer2 = User(
username="Yashu Singhai", username="Yashu Singhai",
uuid="13888204-1bae-4be4-8192-1ca46be4fc7d", uuid="13888204-1bae-4be4-8192-1ca46be4fc7d",
email="volunteer2@compass.com", email="volunteer2@compass.com",
experience=1, experience=1,
group="volunteer", group="volunteer",
program=[programs.COMMUNITY, programs.ECONOMIC], program=[programs.COMMUNITY, programs.ECONOMIC],
created_at=datetime.now(), created_at=datetime.now(),
role=roles.VOLUNTEER, role=roles.VOLUNTEER,
) )
users1 = [admin1, employee1, volunteer1, volunteer2] users1 = [admin1, employee1, volunteer1, volunteer2]
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
def reset_table_id_seq( def reset_table_id_seq(
session: Session, session: Session,
entity: type[DeclarativeBase], entity: type[DeclarativeBase],
entity_id_column: InstrumentedAttribute[int], entity_id_column: InstrumentedAttribute[int],
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
id_column_name = entity_id_column.name id_column_name = entity_id_column.name
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}") sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
session.execute(sql) session.execute(sql)
def insert_fake_data(session: Session): def insert_fake_data(session: Session):
"""Inserts fake organization data into the test session.""" """Inserts fake organization data into the test session."""
global users global users
# Create entities for test organization data # Create entities for test organization data
entities = [] entities = []
for user in users: for user in users:
entity = UserEntity.from_model(user) entity = UserEntity.from_model(user)
session.add(entity) session.add(entity)
entities.append(entity) entities.append(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users) + 1) reset_table_id_seq(session, UserEntity, UserEntity.id, len(users) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()
def insert_test_data(session: Session): def insert_test_data(session: Session):
"""Inserts fake organization data into the test session.""" """Inserts fake organization data into the test session."""
global users1 global users1
# Create entities for test organization data # Create entities for test organization data
for user in users1: for user in users1:
entity = UserEntity.from_model(user) entity = UserEntity.from_model(user)
session.add(entity) session.add(entity)
# Reset table IDs to prevent ID conflicts # Reset table IDs to prevent ID conflicts
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users1) + 1) reset_table_id_seq(session, UserEntity, UserEntity.id, len(users1) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fake_data_fixture(session: Session): def fake_data_fixture(session: Session):
"""Insert fake data the session automatically when test is run. """Insert fake data the session automatically when test is run.
Note: Note:
This function runs automatically due to the fixture property `autouse=True`. This function runs automatically due to the fixture property `autouse=True`.
""" """
insert_fake_data(session) insert_fake_data(session)
session.commit() session.commit()
yield yield

View File

@ -1,3 +1,3 @@
{ {
"extends": "next/core-web-vitals" "extends": "next/core-web-vitals"
} }

View File

@ -1,6 +1,6 @@
{ {
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"semi": true, "semi": true,
"singleQuote": false "singleQuote": false
} }

View File

@ -1,36 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started ## Getting Started
First, run the development server: First, run the development server:
```bash ```bash
npm run dev npm run dev
# or # or
yarn dev yarn dev
# or # or
pnpm dev pnpm dev
# or # or
bun dev bun dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More ## Learn More
To learn more about Next.js, take a look at the following resources: 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. - [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. - [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! You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel ## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -1,100 +1,100 @@
"use client"; "use client";
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import User, { Role } from "@/utils/models/User"; import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading"; import Loading from "@/components/auth/Loading";
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
useEffect(() => { useEffect(() => {
async function getUser() { async function getUser() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
console.log(data, error); console.log(data, error);
if (error) { if (error) {
console.log("Accessed admin page but not logged in"); console.log("Accessed admin page but not logged in");
router.push("/auth/login"); router.push("/auth/login");
return; return;
} }
const userData = await fetch( const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
); );
const user: User = await userData.json(); const user: User = await userData.json();
if (user.role !== Role.ADMIN) { if (user.role !== Role.ADMIN) {
console.log( console.log(
`Accessed admin page but incorrect permissions: ${user.username} ${user.role}` `Accessed admin page but incorrect permissions: ${user.username} ${user.role}`
); );
router.push("/home"); router.push("/home");
return; return;
} }
setUser(user); setUser(user);
} }
getUser(); getUser();
}, [router]); }, [router]);
return ( return (
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
{/* button to open sidebar */} {/* button to open sidebar */}
<button <button
onClick={() => setIsSidebarOpen(!isSidebarOpen)} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"} aria-label={"Open sidebar"}
> >
{ {
!isSidebarOpen && ( !isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" /> <ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar ) // Icon for closing the sidebar
} }
</button> </button>
{/* sidebar */} {/* sidebar */}
<div <div
className={`absolute inset-y-0 left-0 transform ${ className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen isSidebarOpen
? "translate-x-0" ? "translate-x-0"
: "-translate-x-full" : "-translate-x-full"
} w-64 transition duration-300 ease-in-out`} } w-64 transition duration-300 ease-in-out`}
> >
<Sidebar <Sidebar
setIsSidebarOpen={setIsSidebarOpen} setIsSidebarOpen={setIsSidebarOpen}
name={user.username} name={user.username}
email={user.email} email={user.email}
isAdmin={user.role === Role.ADMIN} isAdmin={user.role === Role.ADMIN}
/> />
</div> </div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"
}`} }`}
> >
{children} {children}
</div> </div>
</div> </div>
) : ( ) : (
<Loading /> <Loading />
)} )}
</div> </div>
); );
} }

View File

@ -1,45 +1,45 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import { Table } from "@/components/Table/Index"; import { Table } from "@/components/Table/Index";
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[]>([]);
useEffect(() => { useEffect(() => {
async function getUser() { async function getUser() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
if (error) { if (error) {
console.log("Accessed admin page but not logged in"); console.log("Accessed admin page but not logged in");
return; return;
} }
const userListData = await fetch( const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
); );
const users: User[] = await userListData.json(); const users: User[] = await userListData.json();
setUsers(users); setUsers(users);
} }
getUser(); getUser();
}, []); }, []);
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}> <PageLayout title="Users" icon={<UsersIcon />}>
<Table users={users} /> <Table users={users} />
</PageLayout> </PageLayout>
</div> </div>
); );
} }

View File

@ -1,9 +1,9 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET() { export async function GET() {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/health`; const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/health`;
const result = await fetch(apiEndpoint); const result = await fetch(apiEndpoint);
return NextResponse.json(await result.json(), { status: result.status }); return NextResponse.json(await result.json(), { status: result.status });
} }

View File

@ -1,24 +1,24 @@
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(request: Request) { export async function GET(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`; const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
console.log(apiEndpoint); console.log(apiEndpoint);
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const resourceData: Resource[] = await data.json(); const resourceData: Resource[] = await data.json();
// TODO: Remove make every resource visible // TODO: Remove make every resource visible
const resources = resourceData.map((resource: Resource) => { const resources = resourceData.map((resource: Resource) => {
resource.visible = true; resource.visible = true;
return resource; return resource;
}); });
return NextResponse.json(resources, { status: data.status }); return NextResponse.json(resources, { status: data.status });
} }

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET() { export async function GET() {
return NextResponse.json({ message: "Hello World!" }, { status: 200 }); return NextResponse.json({ message: "Hello World!" }, { status: 200 });
} }

View File

@ -1,24 +1,24 @@
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(request: Request) { export async function GET(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`; const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
console.log(apiEndpoint); console.log(apiEndpoint);
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const serviceData: Service[] = await data.json(); const serviceData: Service[] = await data.json();
// TODO: Remove make every service visible // TODO: Remove make every service visible
const services = serviceData.map((service: Service) => { const services = serviceData.map((service: Service) => {
service.visible = true; service.visible = true;
return service; return service;
}); });
return NextResponse.json(services, { status: data.status }); return NextResponse.json(services, { status: data.status });
} }

View File

@ -1,24 +1,24 @@
import User from "@/utils/models/User"; import User from "@/utils/models/User";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(request: Request) { export async function GET(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`; const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`;
console.log(apiEndpoint); console.log(apiEndpoint);
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const userData: User[] = await data.json(); const userData: User[] = await data.json();
// TODO: Remove make every user visible // TODO: Remove make every user visible
const users = userData.map((user: User) => { const users = userData.map((user: User) => {
user.visible = true; user.visible = true;
return user; return user;
}); });
return NextResponse.json(users, { status: data.status }); return NextResponse.json(users, { status: data.status });
} }

View File

@ -1,14 +1,14 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(request: Request) { export async function GET(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`; const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
console.log(apiEndpoint); console.log(apiEndpoint);
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}/${uuid}`); const data = await fetch(`${apiEndpoint}/${uuid}`);
return NextResponse.json(await data.json(), { status: data.status }); return NextResponse.json(await data.json(), { status: data.status });
} }

View File

@ -1,58 +1,58 @@
"use server"; "use server";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
import User, { Role } from "@/utils/models/User"; import User, { Role } from "@/utils/models/User";
export async function login(email: string, password: string) { export async function login(email: string, password: string) {
const supabase = createClient(); const supabase = createClient();
// type-casting here for convenience // type-casting here for convenience
// in practice, you should validate your inputs // in practice, you should validate your inputs
const data = { const data = {
email, email,
password, password,
}; };
const { error } = await supabase.auth.signInWithPassword(data); const { error } = await supabase.auth.signInWithPassword(data);
if (error) { if (error) {
return "Incorrect email/password"; return "Incorrect email/password";
} }
const supabaseUser = await supabase.auth.getUser(); const supabaseUser = await supabase.auth.getUser();
if (!supabaseUser.data.user) { if (!supabaseUser.data.user) {
revalidatePath("/home", "layout"); revalidatePath("/home", "layout");
redirect("/home"); redirect("/home");
} }
const apiData = await fetch( const apiData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${supabaseUser.data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${supabaseUser.data.user.id}`
); );
const user: User = await apiData.json(); const user: User = await apiData.json();
console.log(user); console.log(user);
if (user.role === Role.ADMIN) { if (user.role === Role.ADMIN) {
redirect("/admin"); redirect("/admin");
} }
revalidatePath("/home", "layout"); revalidatePath("/home", "layout");
redirect("/home"); redirect("/home");
} }
export async function signOut() { export async function signOut() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) { if (error || !data?.user) {
redirect("auth/login"); redirect("auth/login");
} }
console.log(`Signed out ${data.user.email}!`); console.log(`Signed out ${data.user.email}!`);
await supabase.auth.signOut(); await supabase.auth.signOut();
revalidatePath("/auth/login", "layout"); revalidatePath("/auth/login", "layout");
redirect("/auth/login"); redirect("/auth/login");
} }

View File

@ -1,3 +1,3 @@
export default function ErrorPage() { export default function ErrorPage() {
return <p>Sorry, something went wrong</p>; return <p>Sorry, something went wrong</p>;
} }

View File

@ -1,54 +1,54 @@
// pages/forgot-password.tsx // pages/forgot-password.tsx
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import Input from "@/components/Input"; import Input from "@/components/Input";
import Button from "@/components/Button"; import Button from "@/components/Button";
import InlineLink from "@/components/InlineLink"; import InlineLink from "@/components/InlineLink";
import ErrorBanner from "@/components/auth/ErrorBanner"; import ErrorBanner from "@/components/auth/ErrorBanner";
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const [confirmEmail, setConfirmEmail] = useState(""); const [confirmEmail, setConfirmEmail] = useState("");
const [emailError, setEmailError] = useState<string | null>(null); const [emailError, setEmailError] = useState<string | null>(null);
function isValidEmail(email: string): boolean { function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email.trim() === "") { if (email.trim() === "") {
setEmailError("Email cannot be empty"); setEmailError("Email cannot be empty");
return false; return false;
} else if (!emailRegex.test(email)) { } else if (!emailRegex.test(email)) {
setEmailError("Invalid email format"); setEmailError("Invalid email format");
return false; return false;
} }
return true; // No error return true; // No error
} }
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
isValidEmail(confirmEmail); isValidEmail(confirmEmail);
event.preventDefault(); event.preventDefault();
}; };
return ( return (
<> <>
<h1 className="font-bold text-xl text-purple-800"> <h1 className="font-bold text-xl text-purple-800">
Forgot Password Forgot Password
</h1> </h1>
<div className="mb-6"> <div className="mb-6">
<Input <Input
type="email" type="email"
valid={emailError == null} valid={emailError == null}
title="Enter your email address" title="Enter your email address"
placeholder="janedoe@gmail.com" placeholder="janedoe@gmail.com"
value={confirmEmail} value={confirmEmail}
onChange={(e) => setConfirmEmail(e.target.value)} onChange={(e) => setConfirmEmail(e.target.value)}
/> />
</div> </div>
{emailError && <ErrorBanner heading={emailError} />} {emailError && <ErrorBanner heading={emailError} />}
<div className="flex flex-col items-left space-y-4"> <div className="flex flex-col items-left space-y-4">
<InlineLink href="/auth/login">Back to Sign In</InlineLink> <InlineLink href="/auth/login">Back to Sign In</InlineLink>
<Button type="submit" onClick={handleClick}> <Button type="submit" onClick={handleClick}>
Send Send
</Button> </Button>
</div> </div>
</> </>
); );
} }

View File

@ -1,20 +1,20 @@
import Paper from "@/components/auth/Paper"; import Paper from "@/components/auth/Paper";
export default function RootLayout({ export default function RootLayout({
// Layouts must accept a children prop. // Layouts must accept a children prop.
// This will be populated with nested layouts or pages // This will be populated with nested layouts or pages
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<Paper> <Paper>
<form className="mb-0 m-auto mt-6 space-y-4 border border-gray-200 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl"> <form className="mb-0 m-auto mt-6 space-y-4 border border-gray-200 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl">
{children} {children}
</form> </form>
<p className="text-center mt-6 text-gray-500 text-xs"> <p className="text-center mt-6 text-gray-500 text-xs">
&copy; 2024 Compass Center &copy; 2024 Compass Center
</p> </p>
</Paper> </Paper>
); );
} }

View File

@ -1,118 +1,122 @@
// pages/index.tsx // pages/index.tsx
"use client"; "use client";
import Button from "@/components/Button"; import Button from "@/components/Button";
import Input from "@/components/Input"; import Input from "@/components/Input";
import InlineLink from "@/components/InlineLink"; import InlineLink from "@/components/InlineLink";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PasswordInput from "@/components/auth/PasswordInput"; import PasswordInput from "@/components/auth/PasswordInput";
import ErrorBanner from "@/components/auth/ErrorBanner"; import ErrorBanner from "@/components/auth/ErrorBanner";
import { login } from "../actions"; import { login } from "../actions";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState(""); const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState(""); const [passwordError, setPasswordError] = useState("");
const [loginError, setLoginError] = useState(""); const [loginError, setLoginError] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const supabase = createClient(); console.log("here");
async function checkUser() { },[])
const { data } = await supabase.auth.getUser();
if (data.user) { useEffect(() => {
router.push("/home"); const supabase = createClient();
} async function checkUser() {
} const { data } = await supabase.auth.getUser();
checkUser(); if (data.user) {
}, [router]); router.push("/home");
}
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => { }
setEmail(event.currentTarget.value); checkUser();
}; }, [router]);
const handlePasswordChange = ( const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event: React.ChangeEvent<HTMLInputElement> setEmail(event.currentTarget.value);
) => { };
setPassword(event.currentTarget.value);
}; const handlePasswordChange = (
event: React.ChangeEvent<HTMLInputElement>
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => { ) => {
event.preventDefault(); setPassword(event.currentTarget.value);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; };
if (email.trim().length === 0) { const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
setEmailError("Please enter your email."); event.preventDefault();
return; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
}
if (!emailRegex.test(email)) { if (email.trim().length === 0) {
setEmailError("Please enter a valid email address."); setEmailError("Please enter your email.");
return; return;
} }
setEmailError(""); if (!emailRegex.test(email)) {
setEmailError("Please enter a valid email address.");
if (password.trim().length === 0) { return;
setPasswordError("Please enter your password."); }
return; setEmailError("");
}
setPasswordError(""); if (password.trim().length === 0) {
setPasswordError("Please enter your password.");
setIsLoading(true); return;
const error = await login(email, password); }
setIsLoading(false); setPasswordError("");
if (error) { setIsLoading(true);
setLoginError(error); const error = await login(email, password);
} setIsLoading(false);
};
if (error) {
return ( setLoginError(error);
<> }
<Image };
src="/logo.png"
alt="Compass Center logo." return (
width={100} <>
height={91} <Image
/> src="/logo.png"
<h1 className="font-bold text-2xl text-purple-800">Login</h1> alt="Compass Center logo."
<div className="mb-6"> width={100}
<Input height={91}
type="email" />
valid={emailError === ""} <h1 className="font-bold text-2xl text-purple-800">Login</h1>
title="Email" <div className="mb-6">
placeholder="Enter Email" <Input
onChange={handleEmailChange} type="email"
required valid={emailError === ""}
/> title="Email"
</div> placeholder="Enter Email"
{emailError && <ErrorBanner heading={emailError} />} onChange={handleEmailChange}
<div className="mb-6"> required
<PasswordInput />
title="Password" </div>
placeholder="Enter Password" {emailError && <ErrorBanner heading={emailError} />}
valid={passwordError === ""} <div className="mb-6">
onChange={handlePasswordChange} <PasswordInput
/> title="Password"
</div> placeholder="Enter Password"
{passwordError && <ErrorBanner heading={passwordError} />} valid={passwordError === ""}
<div className="flex flex-col items-left space-y-4"> onChange={handlePasswordChange}
<InlineLink href="/auth/forgot_password"> />
Forgot password? </div>
</InlineLink> {passwordError && <ErrorBanner heading={passwordError} />}
<Button onClick={handleClick} disabled={isLoading}> <div className="flex flex-col items-left space-y-4">
<div className="flex items-center justify-center"> <InlineLink href="/auth/forgot_password">
{isLoading && ( Forgot password?
<div className="w-4 h-4 border-2 border-white border-t-purple-500 rounded-full animate-spin mr-2"></div> </InlineLink>
)} <Button onClick={handleClick} disabled={isLoading}>
{isLoading ? "Logging in..." : "Login"} <div className="flex items-center justify-center">
</div> {isLoading && (
</Button> <div className="w-4 h-4 border-2 border-white border-t-purple-500 rounded-full animate-spin mr-2"></div>
</div> )}
{loginError && <ErrorBanner heading={loginError} />} {isLoading ? "Logging in..." : "Login"}
</> </div>
); </Button>
} </div>
{loginError && <ErrorBanner heading={loginError} />}
</>
);
}

View File

@ -1,79 +1,79 @@
// pages/index.tsx // pages/index.tsx
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Button from "@/components/Button"; import Button from "@/components/Button";
import PasswordInput from "@/components/auth/PasswordInput"; import PasswordInput from "@/components/auth/PasswordInput";
import ErrorBanner from "@/components/auth/ErrorBanner"; import ErrorBanner from "@/components/auth/ErrorBanner";
function isStrongPassword(password: string): boolean { function isStrongPassword(password: string): boolean {
const strongPasswordRegex = const strongPasswordRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/; /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
return strongPasswordRegex.test(password); return strongPasswordRegex.test(password);
} }
export default function Page() { export default function Page() {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [isButtonDisabled, setIsButtonDisabled] = useState(true); const [isButtonDisabled, setIsButtonDisabled] = useState(true);
useEffect(() => { useEffect(() => {
setIsButtonDisabled( setIsButtonDisabled(
newPassword === "" || newPassword === "" ||
confirmPassword === "" || confirmPassword === "" ||
newPassword !== confirmPassword || newPassword !== confirmPassword ||
!isStrongPassword(newPassword) !isStrongPassword(newPassword)
); );
}, [newPassword, confirmPassword]); }, [newPassword, confirmPassword]);
return ( return (
<> <>
<div className="text-center sm:text-left"> <div className="text-center sm:text-left">
<h1 className="font-bold text-xl text-purple-800"> <h1 className="font-bold text-xl text-purple-800">
New Password New Password
</h1> </h1>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<PasswordInput <PasswordInput
title="Enter New Password" title="Enter New Password"
value={newPassword} value={newPassword}
valid={!isButtonDisabled || isStrongPassword(newPassword)} valid={!isButtonDisabled || isStrongPassword(newPassword)}
onChange={(e) => { onChange={(e) => {
setNewPassword(e.target.value); setNewPassword(e.target.value);
}} }}
/> />
</div> </div>
{isStrongPassword(newPassword) || newPassword === "" ? null : ( {isStrongPassword(newPassword) || newPassword === "" ? null : (
<ErrorBanner <ErrorBanner
heading="Password is not strong enough." heading="Password is not strong enough."
description="Tip: Use a mix of letters, numbers, and symbols for a strong password. Aim for at least 8 characters!" description="Tip: Use a mix of letters, numbers, and symbols for a strong password. Aim for at least 8 characters!"
/> />
)} )}
<div className="mb-6"> <div className="mb-6">
<PasswordInput <PasswordInput
title="Confirm Password" title="Confirm Password"
value={confirmPassword} value={confirmPassword}
valid={ valid={
!isButtonDisabled || !isButtonDisabled ||
(newPassword === confirmPassword && (newPassword === confirmPassword &&
confirmPassword !== "") confirmPassword !== "")
} }
onChange={(e) => { onChange={(e) => {
setConfirmPassword(e.target.value); setConfirmPassword(e.target.value);
}} }}
/> />
</div> </div>
{newPassword === confirmPassword || {newPassword === confirmPassword ||
confirmPassword === "" ? null : ( confirmPassword === "" ? null : (
<ErrorBanner <ErrorBanner
heading="Passwords do not match." heading="Passwords do not match."
description="Please make sure both passwords are the exact same!" description="Please make sure both passwords are the exact same!"
/> />
)} )}
<div className="flex flex-col items-left space-y-4"> <div className="flex flex-col items-left space-y-4">
<Button type="submit" disabled={isButtonDisabled}> <Button type="submit" disabled={isButtonDisabled}>
Send Send
</Button> </Button>
</div> </div>
</> </>
); );
} }

View File

@ -1,89 +1,89 @@
"use client"; "use client";
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import User, { Role } from "@/utils/models/User"; import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading"; import Loading from "@/components/auth/Loading";
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
async function getUser() { async function getUser() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
console.log(data, error); console.log(data, error);
if (error) { if (error) {
console.log("Accessed home page but not logged in"); console.log("Accessed home page but not logged in");
router.push("/auth/login"); router.push("/auth/login");
return; return;
} }
const userData = await fetch( const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
); );
setUser(await userData.json()); setUser(await userData.json());
} }
getUser(); getUser();
}, [router]); }, [router]);
return ( return (
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
{/* button to open sidebar */} {/* button to open sidebar */}
<button <button
onClick={() => setIsSidebarOpen(!isSidebarOpen)} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"} aria-label={"Open sidebar"}
> >
{ {
!isSidebarOpen && ( !isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" /> <ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar ) // Icon for closing the sidebar
} }
</button> </button>
{/* sidebar */} {/* sidebar */}
<div <div
className={`absolute inset-y-0 left-0 transform ${ className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen isSidebarOpen
? "translate-x-0" ? "translate-x-0"
: "-translate-x-full" : "-translate-x-full"
} w-64 transition duration-300 ease-in-out`} } w-64 transition duration-300 ease-in-out`}
> >
<Sidebar <Sidebar
name={user.username} name={user.username}
email={user.email} email={user.email}
setIsSidebarOpen={setIsSidebarOpen} setIsSidebarOpen={setIsSidebarOpen}
isAdmin={user.role === Role.ADMIN} isAdmin={user.role === Role.ADMIN}
/> />
</div> </div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"
}`} }`}
> >
{children} {children}
</div> </div>
</div> </div>
) : ( ) : (
<Loading /> <Loading />
)} )}
</div> </div>
); );
} }

View File

@ -1,61 +1,61 @@
"use client"; "use client";
import Callout from "@/components/resource/Callout"; import Callout from "@/components/resource/Callout";
import Card from "@/components/resource/Card"; import Card from "@/components/resource/Card";
import { LandingSearchBar } from "@/components/resource/LandingSearchBar"; import { LandingSearchBar } from "@/components/resource/LandingSearchBar";
import { import {
BookOpenIcon, BookOpenIcon,
BookmarkIcon, BookmarkIcon,
ClipboardIcon, ClipboardIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
export default function Page() { export default function Page() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<div className="pt-16 px-8 pb-4 flex-row"> <div className="pt-16 px-8 pb-4 flex-row">
<div className="mb-4 flex items-center space-x-4"> <div className="mb-4 flex items-center space-x-4">
<Image <Image
src="/logo.png" src="/logo.png"
alt="Compass Center logo." alt="Compass Center logo."
width={25} width={25}
height={25} height={25}
/> />
<h1 className="font-bold text-2xl text-purple-800"> <h1 className="font-bold text-2xl text-purple-800">
Compass Center Advocate Landing Page Compass Center Advocate Landing Page
</h1> </h1>
</div> </div>
<Callout> <Callout>
Welcome! Below you will find a list of resources for the Welcome! Below you will find a list of resources for the
Compass Center&apos;s trained advocates. These materials Compass Center&apos;s trained advocates. These materials
serve to virtually provide a collection of advocacy, serve to virtually provide a collection of advocacy,
resource, and hotline manuals and information. resource, and hotline manuals and information.
<b> <b>
{" "} {" "}
If you are an advocate looking for the contact If you are an advocate looking for the contact
information of a particular Compass Center employee, information of a particular Compass Center employee,
please directly contact your staff back-up or the person please directly contact your staff back-up or the person
in charge of your training. in charge of your training.
</b> </b>
</Callout> </Callout>
</div> </div>
<div className="p-8 flex-grow border-t border-gray-200 bg-gray-50"> <div className="p-8 flex-grow border-t border-gray-200 bg-gray-50">
{/* link to different pages */} {/* link to different pages */}
<div className="grid grid-cols-3 gap-6 pb-6"> <div className="grid grid-cols-3 gap-6 pb-6">
<Link href="/resource"> <Link href="/resource">
<Card icon={<BookmarkIcon />} text="Resources" /> <Card icon={<BookmarkIcon />} text="Resources" />
</Link> </Link>
<Link href="/service"> <Link href="/service">
<Card icon={<ClipboardIcon />} text="Services" /> <Card icon={<ClipboardIcon />} text="Services" />
</Link> </Link>
<Link href="/training-manual"> <Link href="/training-manual">
<Card icon={<BookOpenIcon />} text="Training Manuals" /> <Card icon={<BookOpenIcon />} text="Training Manuals" />
</Link> </Link>
</div> </div>
{/* search bar */} {/* search bar */}
<LandingSearchBar /> <LandingSearchBar />
</div> </div>
</div> </div>
); );
} }

View File

@ -1,20 +1,20 @@
"use client"; "use client";
import "../styles/globals.css"; import "../styles/globals.css";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
export default function RootLayout({ export default function RootLayout({
// Layouts must accept a children prop. // Layouts must accept a children prop.
// This will be populated with nested layouts or pages // This will be populated with nested layouts or pages
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<body>{children}</body> <body>{children}</body>
</html> </html>
); );
} }

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
router.push("/auth/login"); router.push("/auth/login");
return <h1>GO TO LOGIN PAGE (/auth/login)</h1>; return <h1>GO TO LOGIN PAGE (/auth/login)</h1>;
} }

View File

@ -1,92 +1,92 @@
"use client"; "use client";
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import User, { Role } from "@/utils/models/User"; import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading"; import Loading from "@/components/auth/Loading";
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
useEffect(() => { useEffect(() => {
async function getUser() { async function getUser() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
console.log(data, error); console.log(data, error);
if (error) { if (error) {
console.log("Accessed resource page but not logged in"); console.log("Accessed resource page but not logged in");
router.push("/auth/login"); router.push("/auth/login");
return; return;
} }
const userData = await fetch( const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
); );
const user: User = await userData.json(); const user: User = await userData.json();
setUser(user); setUser(user);
} }
getUser(); getUser();
}, [router]); }, [router]);
return ( return (
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
{/* button to open sidebar */} {/* button to open sidebar */}
<button <button
onClick={() => setIsSidebarOpen(!isSidebarOpen)} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"} aria-label={"Open sidebar"}
> >
{ {
!isSidebarOpen && ( !isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" /> <ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar ) // Icon for closing the sidebar
} }
</button> </button>
{/* sidebar */} {/* sidebar */}
<div <div
className={`absolute inset-y-0 left-0 transform ${ className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen isSidebarOpen
? "translate-x-0" ? "translate-x-0"
: "-translate-x-full" : "-translate-x-full"
} w-64 transition duration-300 ease-in-out`} } w-64 transition duration-300 ease-in-out`}
> >
<Sidebar <Sidebar
setIsSidebarOpen={setIsSidebarOpen} setIsSidebarOpen={setIsSidebarOpen}
name={user.username} name={user.username}
email={user.email} email={user.email}
isAdmin={user.role === Role.ADMIN} isAdmin={user.role === Role.ADMIN}
/> />
</div> </div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"
}`} }`}
> >
{children} {children}
</div> </div>
</div> </div>
) : ( ) : (
<Loading /> <Loading />
)} )}
</div> </div>
); );
} }

View File

@ -1,45 +1,45 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import { ResourceTable } from "@/components/Table/ResourceIndex"; import { ResourceTable } from "@/components/Table/ResourceIndex";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
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";
export default function Page() { export default function Page() {
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
useEffect(() => { useEffect(() => {
async function getResources() { async function getResources() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
if (error) { if (error) {
console.log("Accessed admin page but not logged in"); console.log("Accessed admin page but not logged in");
return; return;
} }
const userListData = await fetch( const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}`
); );
const resourcesAPI: Resource[] = await userListData.json(); const resourcesAPI: Resource[] = await userListData.json();
setResources(resourcesAPI); setResources(resourcesAPI);
} }
getResources(); getResources();
}, []); }, []);
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}> <PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable users={resources} /> <ResourceTable users={resources} />
</PageLayout> </PageLayout>
</div> </div>
); );
} }

View File

@ -0,0 +1,92 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter();
const [user, setUser] = useState<User>();
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
console.log(data, error);
if (error) {
console.log("Accessed resource page but not logged in");
router.push("/auth/login");
return;
}
const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
);
const user: User = await userData.json();
setUser(user);
}
getUser();
}, [router]);
return (
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
>
{children}
</div>
</div>
) : (
<Loading />
)}
</div>
);
}

View File

@ -0,0 +1,49 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { ResourceTable } from "@/components/Table/ResourceIndex";
import Resource from "@/utils/models/Resource";
import { createClient } from "@/utils/supabase/client";
import { BookmarkIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
const [resources, setResources] = useState<Resource[]>([]);
console.log("hello");
useEffect(() => {
async function getResources() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
console.log(data, error);
if (error) {
console.log("Accessed admin page but not logged in");
return;
}
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();
}, []);
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable users={resources} />
</PageLayout>
</div>
);
}

View File

@ -1,92 +1,92 @@
"use client"; "use client";
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import User, { Role } from "@/utils/models/User"; import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading"; import Loading from "@/components/auth/Loading";
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
useEffect(() => { useEffect(() => {
async function getUser() { async function getUser() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
console.log(data, error); console.log(data, error);
if (error) { if (error) {
console.log("Accessed service page but not logged in"); console.log("Accessed service page but not logged in");
router.push("/auth/login"); router.push("/auth/login");
return; return;
} }
const userData = await fetch( const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
); );
const user: User = await userData.json(); const user: User = await userData.json();
setUser(user); setUser(user);
} }
getUser(); getUser();
}, [router]); }, [router]);
return ( return (
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
{/* button to open sidebar */} {/* button to open sidebar */}
<button <button
onClick={() => setIsSidebarOpen(!isSidebarOpen)} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"} aria-label={"Open sidebar"}
> >
{ {
!isSidebarOpen && ( !isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" /> <ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar ) // Icon for closing the sidebar
} }
</button> </button>
{/* sidebar */} {/* sidebar */}
<div <div
className={`absolute inset-y-0 left-0 transform ${ className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen isSidebarOpen
? "translate-x-0" ? "translate-x-0"
: "-translate-x-full" : "-translate-x-full"
} w-64 transition duration-300 ease-in-out`} } w-64 transition duration-300 ease-in-out`}
> >
<Sidebar <Sidebar
setIsSidebarOpen={setIsSidebarOpen} setIsSidebarOpen={setIsSidebarOpen}
name={user.username} name={user.username}
email={user.email} email={user.email}
isAdmin={user.role === Role.ADMIN} isAdmin={user.role === Role.ADMIN}
/> />
</div> </div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"
}`} }`}
> >
{children} {children}
</div> </div>
</div> </div>
) : ( ) : (
<Loading /> <Loading />
)} )}
</div> </div>
); );
} }

View File

@ -1,44 +1,44 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import { ServiceTable } from "@/components/Table/ServiceIndex"; import { ServiceTable } from "@/components/Table/ServiceIndex";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
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, setUsers] = useState<Service[]>([]); const [services, setUsers] = useState<Service[]>([]);
useEffect(() => { useEffect(() => {
async function getServices() { async function getServices() {
const supabase = createClient(); const supabase = createClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
if (error) { if (error) {
console.log("Accessed admin page but not logged in"); console.log("Accessed admin page but not logged in");
return; return;
} }
const serviceListData = await fetch( const serviceListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}` `${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}`
); );
const servicesAPI: Service[] = await serviceListData.json(); const servicesAPI: Service[] = await serviceListData.json();
setUsers(servicesAPI); setUsers(servicesAPI);
} }
getServices(); getServices();
}, []); }, []);
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}> <PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable users={services} /> <ServiceTable users={services} />
</PageLayout> </PageLayout>
</div> </div>
); );
} }

View File

@ -1,20 +1,20 @@
import Paper from "@/components/auth/Paper"; import Paper from "@/components/auth/Paper";
export default function RootLayout({ export default function RootLayout({
// Layouts must accept a children prop. // Layouts must accept a children prop.
// This will be populated with nested layouts or pages // This will be populated with nested layouts or pages
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<Paper> <Paper>
<form className="mb-0 m-auto mt-6 space-y-4 border border-gray-200 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl"> <form className="mb-0 m-auto mt-6 space-y-4 border border-gray-200 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl">
{children} {children}
</form> </form>
<p className="text-center mt-6 text-gray-500 text-xs"> <p className="text-center mt-6 text-gray-500 text-xs">
&copy; 2024 Compass Center &copy; 2024 Compass Center
</p> </p>
</Paper> </Paper>
); );
} }

View File

@ -1,116 +1,116 @@
"use client"; "use client";
import Button from "@/components/Button"; import Button from "@/components/Button";
import Input from "@/components/Input"; import Input from "@/components/Input";
import InlineLink from "@/components/InlineLink"; import InlineLink from "@/components/InlineLink";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PasswordInput from "@/components/auth/PasswordInput"; import PasswordInput from "@/components/auth/PasswordInput";
import ErrorBanner from "@/components/auth/ErrorBanner"; import ErrorBanner from "@/components/auth/ErrorBanner";
import { login } from "../auth/actions"; import { login } from "../auth/actions";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState(""); const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState(""); const [passwordError, setPasswordError] = useState("");
const [loginError, setLoginError] = useState(""); const [loginError, setLoginError] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const supabase = createClient(); const supabase = createClient();
async function checkUser() { async function checkUser() {
const { data } = await supabase.auth.getUser(); const { data } = await supabase.auth.getUser();
if (data.user) { if (data.user) {
router.push("/home"); router.push("/home");
} }
} }
checkUser(); checkUser();
}, [router]); }, [router]);
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.currentTarget.value); setEmail(event.currentTarget.value);
}; };
const handlePasswordChange = ( const handlePasswordChange = (
event: React.ChangeEvent<HTMLInputElement> event: React.ChangeEvent<HTMLInputElement>
) => { ) => {
setPassword(event.currentTarget.value); setPassword(event.currentTarget.value);
}; };
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault(); event.preventDefault();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email.trim().length === 0) { if (email.trim().length === 0) {
setEmailError("Please enter your email."); setEmailError("Please enter your email.");
return; return;
} }
if (!emailRegex.test(email)) { if (!emailRegex.test(email)) {
setEmailError("Please enter a valid email address."); setEmailError("Please enter a valid email address.");
return; return;
} }
setEmailError(""); setEmailError("");
if (password.trim().length === 0) { if (password.trim().length === 0) {
setPasswordError("Please enter your password."); setPasswordError("Please enter your password.");
return; return;
} }
setPasswordError(""); setPasswordError("");
setIsLoading(true); setIsLoading(true);
const error = await login(email, password); const error = await login(email, password);
setIsLoading(false); setIsLoading(false);
if (error) { if (error) {
setLoginError(error); setLoginError(error);
} }
}; };
return ( return (
<> <>
<Image <Image
src="/logo.png" src="/logo.png"
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
/> />
<h1 className="font-bold text-2xl text-purple-800">Login</h1> <h1 className="font-bold text-2xl text-purple-800">Login</h1>
<div className="mb-6"> <div className="mb-6">
<Input <Input
type="email" type="email"
valid={emailError === ""} valid={emailError === ""}
title="Email" title="Email"
placeholder="Enter Email" placeholder="Enter Email"
required required
/> />
</div> </div>
{emailError && <ErrorBanner heading={emailError} />} {emailError && <ErrorBanner heading={emailError} />}
<div className="mb-6"> <div className="mb-6">
<PasswordInput <PasswordInput
title="Password" title="Password"
placeholder="Enter Password" placeholder="Enter Password"
valid={passwordError === ""} valid={passwordError === ""}
onChange={handlePasswordChange} onChange={handlePasswordChange}
/> />
</div> </div>
{passwordError && <ErrorBanner heading={passwordError} />} {passwordError && <ErrorBanner heading={passwordError} />}
<div className="flex flex-col items-left space-y-4"> <div className="flex flex-col items-left space-y-4">
<InlineLink href="/auth/forgot_password"> <InlineLink href="/auth/forgot_password">
Forgot password? Forgot password?
</InlineLink> </InlineLink>
<Button onClick={handleClick} disabled={isLoading}> <Button onClick={handleClick} disabled={isLoading}>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
{isLoading && ( {isLoading && (
<div className="w-4 h-4 border-2 border-white border-t-purple-500 rounded-full animate-spin mr-2"></div> <div className="w-4 h-4 border-2 border-white border-t-purple-500 rounded-full animate-spin mr-2"></div>
)} )}
{isLoading ? "Logging in..." : "Login"} {isLoading ? "Logging in..." : "Login"}
</div> </div>
</Button> </Button>
</div> </div>
{loginError && <ErrorBanner heading={loginError} />} {loginError && <ErrorBanner heading={loginError} />}
</> </>
); );
} }

View File

@ -1,40 +1,40 @@
// page.tsx // page.tsx
import React from "react"; import React from "react";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
const ComingSoonPage: React.FC = () => { const ComingSoonPage: React.FC = () => {
return ( return (
<> <>
<Head> <Head>
<title>Training Manuals - Coming Soon</title> <title>Training Manuals - Coming Soon</title>
<meta <meta
name="description" name="description"
content="Our training manuals page is coming soon. Stay tuned for updates!" content="Our training manuals page is coming soon. Stay tuned for updates!"
/> />
</Head> </Head>
<div className="min-h-screen bg-gradient-to-r from-purple-600 to-blue-500 flex items-center justify-center"> <div className="min-h-screen bg-gradient-to-r from-purple-600 to-blue-500 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h1 className="text-4xl font-bold text-white mb-4"> <h1 className="text-4xl font-bold text-white mb-4">
Training Manuals Training Manuals
</h1> </h1>
<p className="text-xl text-white mb-8"> <p className="text-xl text-white mb-8">
Our training manuals page is under construction. Our training manuals page is under construction.
</p> </p>
<p className="text-lg text-white"> <p className="text-lg text-white">
Stay tuned for updates! Stay tuned for updates!
</p> </p>
<div className="mt-8"> <div className="mt-8">
<Link href="/home"> <Link href="/home">
<button className="bg-white text-purple-600 font-semibold py-2 px-4 rounded-full shadow-md hover:bg-purple-100 transition duration-300"> <button className="bg-white text-purple-600 font-semibold py-2 px-4 rounded-full shadow-md hover:bg-purple-100 transition duration-300">
Notify Me Notify Me
</button> </button>
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
</> </>
); );
}; };
export default ComingSoonPage; export default ComingSoonPage;

View File

@ -1,36 +1,36 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } from "react";
type ButtonProps = { type ButtonProps = {
children: ReactNode; children: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
disabled?: boolean; disabled?: boolean;
}; };
const Button: FunctionComponent<ButtonProps> = ({ const Button: FunctionComponent<ButtonProps> = ({
children, children,
type, type,
disabled, disabled,
onClick, onClick,
}) => { }) => {
const buttonClassName = `inline-flex items-center justify-center rounded border ${ const buttonClassName = `inline-flex items-center justify-center rounded border ${
disabled disabled
? "bg-gray-400 text-gray-600 cursor-not-allowed" ? "bg-gray-400 text-gray-600 cursor-not-allowed"
: "border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500" : "border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500"
} px-4 py-2 text-md font-semibold w-full sm:w-auto`; } px-4 py-2 text-md font-semibold w-full sm:w-auto`;
return ( return (
<button <button
className={buttonClassName} className={buttonClassName}
onClick={onClick} onClick={onClick}
type={type} type={type}
disabled={disabled} disabled={disabled}
> >
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
{children} {children}
</div> </div>
</button> </button>
); );
}; };
export default Button; export default Button;

View File

@ -1,247 +1,247 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } 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, StarIcon as SolidStarIcon,
EnvelopeIcon, EnvelopeIcon,
UserIcon, UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { import {
ArrowsPointingOutIcon, ArrowsPointingOutIcon,
ArrowsPointingInIcon, ArrowsPointingInIcon,
StarIcon as OutlineStarIcon, StarIcon as OutlineStarIcon,
ListBulletIcon, ListBulletIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
type DrawerProps = { type DrawerProps = {
title: string; title: string;
children: ReactNode; children: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset"; // specify possible values for type type?: "button" | "submit" | "reset"; // specify possible values for type
disabled?: boolean; disabled?: boolean;
editableContent?: any; editableContent?: any;
onSave?: (content: any) => void; onSave?: (content: any) => void;
rowContent?: any; rowContent?: any;
onRowUpdate?: (content: any) => void; onRowUpdate?: (content: any) => void;
}; };
interface EditContent { interface EditContent {
content: string; content: string;
isEditing: boolean; isEditing: boolean;
} }
const Drawer: FunctionComponent<DrawerProps> = ({ const Drawer: FunctionComponent<DrawerProps> = ({
title, title,
children, children,
onSave, onSave,
editableContent, editableContent,
rowContent, rowContent,
onRowUpdate, onRowUpdate,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false); const [isFull, setIsFull] = useState(false);
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const [tempRowContent, setTempRowContent] = useState(rowContent); const [tempRowContent, setTempRowContent] = useState(rowContent);
const handleTempRowContentChange = (e) => { const handleTempRowContentChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
console.log(name); console.log(name);
console.log(value); console.log(value);
setTempRowContent((prevContent) => ({ setTempRowContent((prevContent) => ({
...prevContent, ...prevContent,
[name]: value, [name]: value,
})); }));
}; };
const handleEnterPress = (e) => { const handleEnterPress = (e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
// Update the rowContent with the temporaryRowContent // Update the rowContent with the temporaryRowContent
if (onRowUpdate) { if (onRowUpdate) {
onRowUpdate(tempRowContent); onRowUpdate(tempRowContent);
} }
} }
}; };
const toggleDrawer = () => { const toggleDrawer = () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
if (isFull) { if (isFull) {
setIsFull(!isFull); setIsFull(!isFull);
} }
}; };
const toggleDrawerFullScreen = () => setIsFull(!isFull); const toggleDrawerFullScreen = () => setIsFull(!isFull);
const toggleFavorite = () => setIsFavorite(!isFavorite); const toggleFavorite = () => setIsFavorite(!isFavorite);
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${ const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full" isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
} ${isFull ? "w-full" : "w-1/2"}`; } ${isFull ? "w-full" : "w-1/2"}`;
const iconComponent = isFull ? ( const iconComponent = isFull ? (
<ArrowsPointingInIcon className="h-5 w-5" /> <ArrowsPointingInIcon className="h-5 w-5" />
) : ( ) : (
<ArrowsPointingOutIcon className="h-5 w-5" /> <ArrowsPointingOutIcon className="h-5 w-5" />
); );
const favoriteIcon = isFavorite ? ( const favoriteIcon = isFavorite ? (
<SolidStarIcon className="h-5 w-5" /> <SolidStarIcon className="h-5 w-5" />
) : ( ) : (
<OutlineStarIcon className="h-5 w-5" /> <OutlineStarIcon className="h-5 w-5" />
); );
const [presetOptions, setPresetOptions] = useState([ const [presetOptions, setPresetOptions] = useState([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const [rolePresetOptions, setRolePresetOptions] = useState([ const [rolePresetOptions, setRolePresetOptions] = useState([
"domestic", "domestic",
"community", "community",
"economic", "economic",
]); ]);
const [tagColors, setTagColors] = useState(new Map()); const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => { const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) { if (!tagColors.has(tag)) {
const colors = [ const colors = [
"bg-cyan-100", "bg-cyan-100",
"bg-blue-100", "bg-blue-100",
"bg-green-100", "bg-green-100",
"bg-yellow-100", "bg-yellow-100",
"bg-purple-100", "bg-purple-100",
]; ];
const randomColor = const randomColor =
colors[Math.floor(Math.random() * colors.length)]; colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor)); setTagColors(new Map(tagColors).set(tag, randomColor));
} }
return tagColors.get(tag); return tagColors.get(tag);
}; };
return ( return (
<div> <div>
<button <button
className={ className={
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md" "ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
} }
onClick={toggleDrawer} onClick={toggleDrawer}
> >
Open Open
</button> </button>
<div className={drawerClassName}></div> <div className={drawerClassName}></div>
<div className={drawerClassName}> <div className={drawerClassName}>
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<div className="flex flex-row items-center justify-between space-x-2"> <div className="flex flex-row items-center justify-between space-x-2">
<span className="h-5 text-purple-200 w-5"> <span className="h-5 text-purple-200 w-5">
<UserIcon /> <UserIcon />
</span> </span>
<h2 className="text-lg text-gray-800 font-semibold"> <h2 className="text-lg text-gray-800 font-semibold">
{rowContent.username} {rowContent.username}
</h2> </h2>
</div> </div>
<div> <div>
<button <button
onClick={toggleFavorite} onClick={toggleFavorite}
className="py-2 text-gray-500 hover:text-gray-800 mr-2" className="py-2 text-gray-500 hover:text-gray-800 mr-2"
> >
{favoriteIcon} {favoriteIcon}
</button> </button>
<button <button
onClick={toggleDrawerFullScreen} onClick={toggleDrawerFullScreen}
className="py-2 text-gray-500 hover:text-gray-800 mr-2" className="py-2 text-gray-500 hover:text-gray-800 mr-2"
> >
{iconComponent} {iconComponent}
</button> </button>
<button <button
onClick={toggleDrawer} onClick={toggleDrawer}
className="py-2 text-gray-500 hover:text-gray-800" className="py-2 text-gray-500 hover:text-gray-800"
> >
<ChevronDoubleLeftIcon className="h-5 w-5" /> <ChevronDoubleLeftIcon className="h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>
<div className="p-4"> <div className="p-4">
<table className="p-4"> <table className="p-4">
<tbody className="items-center"> <tbody className="items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between"> <tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<td> <td>
<UserIcon className="h-4 w-4" /> <UserIcon className="h-4 w-4" />
</td> </td>
<td className="w-32">Username</td> <td className="w-32">Username</td>
</div> </div>
<td className="w-3/4 w-3/4 p-2 pl-0"> <td className="w-3/4 w-3/4 p-2 pl-0">
<input <input
type="text" type="text"
name="username" name="username"
value={tempRowContent.username} value={tempRowContent.username}
onChange={handleTempRowContentChange} onChange={handleTempRowContentChange}
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50" className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
/> />
</td> </td>
</tr> </tr>
<tr className="w-full text-xs items-center flex flex-row justify-between"> <tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<td> <td>
<ListBulletIcon className="h-4 w-4" /> <ListBulletIcon className="h-4 w-4" />
</td> </td>
<td className="w-32">Role</td> <td className="w-32">Role</td>
</div> </div>
<td className="w-3/4 hover:bg-gray-50"> <td className="w-3/4 hover:bg-gray-50">
<TagsInput <TagsInput
presetValue={tempRowContent.role} presetValue={tempRowContent.role}
presetOptions={presetOptions} presetOptions={presetOptions}
setPresetOptions={setPresetOptions} setPresetOptions={setPresetOptions}
getTagColor={getTagColor} getTagColor={getTagColor}
setTagColors={setTagColors} setTagColors={setTagColors}
/> />
</td> </td>
</tr> </tr>
<tr className="w-full text-xs items-center flex flex-row justify-between"> <tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<td> <td>
<EnvelopeIcon className="h-4 w-4" /> <EnvelopeIcon className="h-4 w-4" />
</td> </td>
<td className="w-32">Email</td> <td className="w-32">Email</td>
</div> </div>
<td className="w-3/4 p-2 pl-0"> <td className="w-3/4 p-2 pl-0">
<input <input
type="text" type="text"
name="email" name="email"
value={tempRowContent.email} value={tempRowContent.email}
onChange={handleTempRowContentChange} onChange={handleTempRowContentChange}
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500" className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
/> />
</td> </td>
</tr> </tr>
<tr className="w-full text-xs items-center flex flex-row justify-between"> <tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<td> <td>
<ListBulletIcon className="h-4 w-4" /> <ListBulletIcon className="h-4 w-4" />
</td> </td>
<td className="w-32">Type of Program</td> <td className="w-32">Type of Program</td>
</div> </div>
<td className="w-3/4 hover:bg-gray-50"> <td className="w-3/4 hover:bg-gray-50">
{/* {rowContent.program} */} {/* {rowContent.program} */}
<TagsInput <TagsInput
presetValue={tempRowContent.program} presetValue={tempRowContent.program}
presetOptions={rolePresetOptions} presetOptions={rolePresetOptions}
setPresetOptions={setRolePresetOptions} setPresetOptions={setRolePresetOptions}
getTagColor={getTagColor} getTagColor={getTagColor}
setTagColors={setTagColors} setTagColors={setTagColors}
/> />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<br /> <br />
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default Drawer; export default Drawer;

View File

@ -1,53 +1,53 @@
import { useState } from "react"; import { useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/24/solid"; import { ChevronDownIcon } from "@heroicons/react/24/solid";
const mockTags = ["food relief", "period poverty", "nutrition education"]; const mockTags = ["food relief", "period poverty", "nutrition education"];
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty"; type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
export const ContainsDropdown = ({ export const ContainsDropdown = ({
isDropdownOpen, isDropdownOpen,
setIsDropdownOpen, setIsDropdownOpen,
filterType, filterType,
setFilterType, setFilterType,
}) => { }) => {
const handleFilterTypeChange = (type: FilterType) => { const handleFilterTypeChange = (type: FilterType) => {
setFilterType(type); setFilterType(type);
setIsDropdownOpen(false); setIsDropdownOpen(false);
}; };
return ( return (
<div className="relative"> <div className="relative">
<div <div
className={`absolute z-10 mt-8 -top-28 bg-white border border-gray-300 rounded-md shadow-md p-2 ${ className={`absolute z-10 mt-8 -top-28 bg-white border border-gray-300 rounded-md shadow-md p-2 ${
isDropdownOpen ? "block" : "hidden" isDropdownOpen ? "block" : "hidden"
}`} }`}
> >
<div <div
className="cursor-pointer hover:bg-gray-100 rounded" className="cursor-pointer hover:bg-gray-100 rounded"
onClick={() => handleFilterTypeChange("contains")} onClick={() => handleFilterTypeChange("contains")}
> >
Contains Contains
</div> </div>
<div <div
className="cursor-pointer hover:bg-gray-100 rounded" className="cursor-pointer hover:bg-gray-100 rounded"
onClick={() => handleFilterTypeChange("does not contain")} onClick={() => handleFilterTypeChange("does not contain")}
> >
Does not contain Does not contain
</div> </div>
<div <div
className="cursor-pointer hover:bg-gray-100 rounded" className="cursor-pointer hover:bg-gray-100 rounded"
onClick={() => handleFilterTypeChange("is empty")} onClick={() => handleFilterTypeChange("is empty")}
> >
Is empty Is empty
</div> </div>
<div <div
className="cursor-pointer hover:bg-gray-100 rounded" className="cursor-pointer hover:bg-gray-100 rounded"
onClick={() => handleFilterTypeChange("is not empty")} onClick={() => handleFilterTypeChange("is not empty")}
> >
Is not empty Is not empty
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View File

@ -1,95 +1,95 @@
// FilterBox.tsx // FilterBox.tsx
import { useState } from "react"; import { useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/24/solid"; import { ChevronDownIcon } from "@heroicons/react/24/solid";
import { ContainsDropdown } from "./ContainsDropdown"; import { ContainsDropdown } from "./ContainsDropdown";
const mockTags = ["food relief", "period poverty", "nutrition education"]; const mockTags = ["food relief", "period poverty", "nutrition education"];
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty"; type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
export const FilterBox = () => { export const FilterBox = () => {
const [selectedTags, setSelectedTags] = useState<string[]>([]); const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [showContainsDropdown, setShowContainsDropdown] = useState(false); const [showContainsDropdown, setShowContainsDropdown] = useState(false);
const [filterType, setFilterType] = useState<FilterType>("contains"); const [filterType, setFilterType] = useState<FilterType>("contains");
const handleTagChange = (tag: string) => { const handleTagChange = (tag: string) => {
setSelectedTags((prevTags) => setSelectedTags((prevTags) =>
prevTags.includes(tag) prevTags.includes(tag)
? prevTags.filter((t) => t !== tag) ? prevTags.filter((t) => t !== tag)
: [...prevTags, tag] : [...prevTags, tag]
); );
}; };
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
}; };
const renderSelectedTags = () => const renderSelectedTags = () =>
selectedTags.map((tag) => ( selectedTags.map((tag) => (
<div <div
key={tag} key={tag}
className="bg-purple-100 text-purple-800 px-2 py-1 rounded-md flex items-center mr-2" className="bg-purple-100 text-purple-800 px-2 py-1 rounded-md flex items-center mr-2"
> >
<span>{tag}</span> <span>{tag}</span>
<span <span
className="ml-2 cursor-pointer" className="ml-2 cursor-pointer"
onClick={() => handleTagChange(tag)} onClick={() => handleTagChange(tag)}
> >
&times; &times;
</span> </span>
</div> </div>
)); ));
return ( return (
<div className="text-xs bg-white border border-gray-300 z-50 rounded-md p-2 shadow absolute right-5 top-[200px]"> <div className="text-xs bg-white border border-gray-300 z-50 rounded-md p-2 shadow absolute right-5 top-[200px]">
<div className="mb-2"> <div className="mb-2">
<span className="font-semibold"> <span className="font-semibold">
Tags{" "} Tags{" "}
<button <button
onClick={() => onClick={() =>
setShowContainsDropdown((prevState) => !prevState) setShowContainsDropdown((prevState) => !prevState)
} }
className="hover:bg-gray-50 text-gray-500 hover:text-gray-700" className="hover:bg-gray-50 text-gray-500 hover:text-gray-700"
> >
{filterType} <ChevronDownIcon className="inline h-3" /> {filterType} <ChevronDownIcon className="inline h-3" />
</button> </button>
</span> </span>
</div> </div>
<div className="flex flex-wrap mb-2 px-2 py-1 border border-gray-300 rounded w-full"> <div className="flex flex-wrap mb-2 px-2 py-1 border border-gray-300 rounded w-full">
{selectedTags.length > 0 && renderSelectedTags()} {selectedTags.length > 0 && renderSelectedTags()}
<input <input
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} onChange={handleSearchChange}
placeholder="Search tags..." placeholder="Search tags..."
/> />
</div> </div>
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{mockTags {mockTags
.filter((tag) => .filter((tag) =>
tag.toLowerCase().includes(searchTerm.toLowerCase()) tag.toLowerCase().includes(searchTerm.toLowerCase())
) )
.map((tag) => ( .map((tag) => (
<div key={tag} className="flex items-center"> <div key={tag} className="flex items-center">
<input <input
type="checkbox" type="checkbox"
checked={selectedTags.includes(tag)} checked={selectedTags.includes(tag)}
onChange={() => handleTagChange(tag)} onChange={() => handleTagChange(tag)}
className="mr-2 accent-purple-500" className="mr-2 accent-purple-500"
/> />
<label>{tag}</label> <label>{tag}</label>
</div> </div>
))} ))}
</div> </div>
{showContainsDropdown && ( {showContainsDropdown && (
<ContainsDropdown <ContainsDropdown
isDropdownOpen={showContainsDropdown} isDropdownOpen={showContainsDropdown}
setIsDropdownOpen={setShowContainsDropdown} setIsDropdownOpen={setShowContainsDropdown}
filterType={filterType} filterType={filterType}
setFilterType={setFilterType} setFilterType={setFilterType}
/> />
)} )}
</div> </div>
); );
}; };

View File

@ -1,21 +1,21 @@
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
interface Link { interface Link {
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void; onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
href?: string; href?: string;
children: ReactNode; children: ReactNode;
} }
const InlineLink: React.FC<Link> = ({ href = "#", children, onClick }) => { const InlineLink: React.FC<Link> = ({ href = "#", children, onClick }) => {
return ( return (
<a <a
onClick={onClick} onClick={onClick}
href={href} href={href}
className="text-sm text-purple-600 hover:underline font-semibold" className="text-sm text-purple-600 hover:underline font-semibold"
> >
{children} {children}
</a> </a>
); );
}; };
export default InlineLink; export default InlineLink;

View File

@ -1,57 +1,57 @@
import React, { import React, {
FunctionComponent, FunctionComponent,
InputHTMLAttributes, InputHTMLAttributes,
ReactNode, ReactNode,
ChangeEvent, ChangeEvent,
} from "react"; } from "react";
type InputProps = InputHTMLAttributes<HTMLInputElement> & { type InputProps = InputHTMLAttributes<HTMLInputElement> & {
icon?: ReactNode; icon?: ReactNode;
title?: ReactNode; title?: ReactNode;
type?: ReactNode; type?: ReactNode;
placeholder?: ReactNode; placeholder?: ReactNode;
valid?: boolean; valid?: boolean;
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}; };
const Input: FunctionComponent<InputProps> = ({ const Input: FunctionComponent<InputProps> = ({
icon, icon,
type, type,
title, title,
placeholder, placeholder,
onChange, onChange,
valid = true, valid = true,
...rest ...rest
}) => { }) => {
return ( return (
<div> <div>
<label <label
htmlFor={title} htmlFor={title}
className={ className={
valid valid
? "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-purple-600 focus-within:ring-1 focus-within:ring-purple-600" ? "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-purple-600 focus-within:ring-1 focus-within:ring-purple-600"
: "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-red-600 focus-within:ring-1 focus-within:ring-red-600" : "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-red-600 focus-within:ring-1 focus-within:ring-red-600"
} }
> >
<span className="text-xs font-semibold text-gray-700"> <span className="text-xs font-semibold text-gray-700">
{" "} {" "}
{title}{" "} {title}{" "}
</span> </span>
<div className="mt-1 flex items-center"> <div className="mt-1 flex items-center">
<input <input
type={type} type={type}
id={title} id={title}
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
className="w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm" className="w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/> />
<span className="inline-flex items-center px-3 text-gray-500"> <span className="inline-flex items-center px-3 text-gray-500">
{icon} {icon}
</span> </span>
</div> </div>
</label> </label>
</div> </div>
); );
}; };
export default Input; export default Input;

View File

@ -1,27 +1,27 @@
interface PageLayoutProps { interface PageLayoutProps {
icon: React.ReactElement; icon: React.ReactElement;
title: string; title: string;
children: React.ReactElement; children: React.ReactElement;
} }
export const PageLayout: React.FC<PageLayoutProps> = ({ export const PageLayout: React.FC<PageLayoutProps> = ({
icon, icon,
title, title,
children, children,
}) => { }) => {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<div className="pt-16 px-8 pb-4 flex-row"> <div className="pt-16 px-8 pb-4 flex-row">
<div className="mb-4 flex items-center space-x-4"> <div className="mb-4 flex items-center space-x-4">
<span className="w-6 h-6 text-purple-200">{icon}</span> <span className="w-6 h-6 text-purple-200">{icon}</span>
<h1 className="font-bold text-2xl text-purple-800"> <h1 className="font-bold text-2xl text-purple-800">
{title} {title}
</h1> </h1>
</div> </div>
</div> </div>
{/* data */} {/* data */}
<div className="px-8 py-8">{children}</div> <div className="px-8 py-8">{children}</div>
</div> </div>
); );
}; };

View File

@ -1,89 +1,89 @@
import React from "react"; import React from "react";
import { import {
HomeIcon, HomeIcon,
ChevronDoubleLeftIcon, ChevronDoubleLeftIcon,
BookmarkIcon, BookmarkIcon,
ClipboardIcon, ClipboardIcon,
BookOpenIcon, BookOpenIcon,
LockClosedIcon, LockClosedIcon,
} 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";
interface SidebarProps { interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
name: string; name: string;
email: string; email: string;
isAdmin: boolean; isAdmin: boolean;
} }
const Sidebar: React.FC<SidebarProps> = ({ const Sidebar: React.FC<SidebarProps> = ({
setIsSidebarOpen, setIsSidebarOpen,
name, name,
email, email,
isAdmin: admin, isAdmin: admin,
}) => { }) => {
return ( return (
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4"> <div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
{/* button to close sidebar */} {/* button to close sidebar */}
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
onClick={() => setIsSidebarOpen(false)} onClick={() => setIsSidebarOpen(false)}
className="py-2 text-gray-500 hover:text-gray-800" className="py-2 text-gray-500 hover:text-gray-800"
aria-label="Close sidebar" aria-label="Close sidebar"
> >
<ChevronDoubleLeftIcon className="h-5 w-5" /> <ChevronDoubleLeftIcon className="h-5 w-5" />
</button> </button>
</div> </div>
<div className="flex flex-col space-y-8"> <div className="flex flex-col space-y-8">
{/* user + logout button */} {/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md "> <div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
<UserProfile name={name} email={email} /> <UserProfile name={name} email={email} />
</div> </div>
{/* navigation menu */} {/* navigation menu */}
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h4 className="text-xs font-semibold text-gray-500"> <h4 className="text-xs font-semibold text-gray-500">
Pages Pages
</h4> </h4>
<nav className="flex flex-col"> <nav className="flex flex-col">
{admin && ( {admin && (
<SidebarItem <SidebarItem
icon={<LockClosedIcon />} icon={<LockClosedIcon />}
text="Admin" text="Admin"
active={true} active={true}
redirect="/admin" redirect="/admin"
/> />
)} )}
<SidebarItem <SidebarItem
icon={<HomeIcon />} icon={<HomeIcon />}
text="Home" text="Home"
active={true} active={true}
redirect="/home" redirect="/home"
/> />
<SidebarItem <SidebarItem
icon={<BookmarkIcon />} icon={<BookmarkIcon />}
text="Resources" text="Resources"
active={true} active={true}
redirect="/resource" redirect="/resource"
/> />
<SidebarItem <SidebarItem
icon={<ClipboardIcon />} icon={<ClipboardIcon />}
text="Services" text="Services"
active={true} active={true}
redirect="/service" redirect="/service"
/> />
<SidebarItem <SidebarItem
icon={<BookOpenIcon />} icon={<BookOpenIcon />}
text="Training Manuals" text="Training Manuals"
active={true} active={true}
redirect="/training-manuals" redirect="/training-manuals"
/> />
</nav> </nav>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default Sidebar; export default Sidebar;

View File

@ -1,31 +1,31 @@
import Link from "next/link"; import Link from "next/link";
interface SidebarItemProps { interface SidebarItemProps {
icon: React.ReactElement; icon: React.ReactElement;
text: string; text: string;
active: boolean; active: boolean;
redirect: string; redirect: string;
} }
export const SidebarItem: React.FC<SidebarItemProps> = ({ export const SidebarItem: React.FC<SidebarItemProps> = ({
icon, icon,
text, text,
active, active,
redirect, redirect,
}) => { }) => {
return ( return (
<Link <Link
href={redirect} href={redirect}
className={ className={
active active
? "flex items-center p-2 my-1 space-x-2 bg-gray-200 rounded-md" ? "flex items-center p-2 my-1 space-x-2 bg-gray-200 rounded-md"
: "flex items-center p-2 my-1 space-x-2 hover:bg-gray-200 rounded-md" : "flex items-center p-2 my-1 space-x-2 hover:bg-gray-200 rounded-md"
} }
> >
<span className="h-5 text-gray-500 w-5">{icon}</span> <span className="h-5 text-gray-500 w-5">{icon}</span>
<span className="flex-grow font-medium text-xs text-gray-500"> <span className="flex-grow font-medium text-xs text-gray-500">
{text} {text}
</span> </span>
</Link> </Link>
); );
}; };

View File

@ -1,306 +1,306 @@
// for showcasing to compass // for showcasing to compass
import users from "./users.json"; import users from "./users.json";
import { import {
Cell, Cell,
ColumnDef, ColumnDef,
Row, Row,
createColumnHelper, createColumnHelper,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
sortingFns, sortingFns,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
FunctionComponent, FunctionComponent,
useRef, useRef,
ChangeEventHandler, ChangeEventHandler,
Key, Key,
} from "react"; } from "react";
import { RowOptionMenu } from "./RowOptionMenu"; import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction"; import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { import {
AtSymbolIcon, AtSymbolIcon,
Bars2Icon, Bars2Icon,
ArrowDownCircleIcon, ArrowDownCircleIcon,
PlusIcon, PlusIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils"; import { rankItem } from "@tanstack/match-sorter-utils";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
// For search // For search
const fuzzyFilter = ( const fuzzyFilter = (
row: Row<any>, row: Row<any>,
columnId: string, columnId: string,
value: any, value: any,
addMeta: (meta: any) => void addMeta: (meta: any) => void
) => { ) => {
// Rank the item // Rank the item
const itemRank = rankItem(row.getValue(columnId), value); const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info // Store the ranking info
addMeta(itemRank); addMeta(itemRank);
// Return if the item should be filtered in/out // Return if the item should be filtered in/out
return itemRank.passed; return itemRank.passed;
}; };
export const Table = ({ users }: { users: User[] }) => { export const Table = ({ users }: { users: User[] }) => {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
useEffect(() => { useEffect(() => {
const sortedUsers = [...users].sort((a, b) => const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
setData(sortedUsers); setData(sortedUsers);
}, [users]); }, [users]);
const deleteUser = (userId: number) => { const deleteUser = (userId: number) => {
console.log(data); console.log(data);
setData((currentData) => setData((currentData) =>
currentData.filter((user) => user.id !== userId) currentData.filter((user) => user.id !== userId)
); );
}; };
const hideUser = (userId: number) => { const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`); console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => { setData((currentData) => {
const newData = currentData const newData = currentData
.map((user) => { .map((user) => {
if (user.id === userId) { if (user.id === userId) {
return { ...user, visible: !user.visible }; return { ...user, visible: !user.visible };
} }
return user; return user;
}) })
.sort((a, b) => .sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
console.log(newData); console.log(newData);
return newData; return newData;
}); });
}; };
const [presetOptions, setPresetOptions] = useState([ const [presetOptions, setPresetOptions] = useState([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const [tagColors, setTagColors] = useState(new Map()); const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => { const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) { if (!tagColors.has(tag)) {
const colors = [ const colors = [
"bg-cyan-100", "bg-cyan-100",
"bg-blue-100", "bg-blue-100",
"bg-green-100", "bg-green-100",
"bg-yellow-100", "bg-yellow-100",
"bg-purple-100", "bg-purple-100",
]; ];
const randomColor = const randomColor =
colors[Math.floor(Math.random() * colors.length)]; colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor)); setTagColors(new Map(tagColors).set(tag, randomColor));
} }
return tagColors.get(tag); return tagColors.get(tag);
}; };
const columns = [ const columns = [
columnHelper.display({ columnHelper.display({
id: "options", id: "options",
cell: (props) => ( cell: (props) => (
<RowOptionMenu <RowOptionMenu
onDelete={() => deleteUser(props.row.original.id)} onDelete={() => deleteUser(props.row.original.id)}
onHide={() => hideUser(props.row.original.id)} onHide={() => hideUser(props.row.original.id)}
/> />
), ),
}), }),
columnHelper.accessor("username", { columnHelper.accessor("username", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Username <Bars2Icon className="inline align-top h-4" /> Username
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
rowData={info.row.original} rowData={info.row.original}
onRowUpdate={handleRowUpdate} onRowUpdate={handleRowUpdate}
/> />
), ),
}), }),
columnHelper.accessor("role", { columnHelper.accessor("role", {
header: () => ( header: () => (
<> <>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "} <ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Role Role
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput
presetValue={info.getValue()} presetValue={info.getValue()}
presetOptions={presetOptions} presetOptions={presetOptions}
setPresetOptions={setPresetOptions} setPresetOptions={setPresetOptions}
getTagColor={getTagColor} getTagColor={getTagColor}
setTagColors={setTagColors} setTagColors={setTagColors}
/> />
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
header: () => ( header: () => (
<> <>
<AtSymbolIcon className="inline align-top h-4" /> Email <AtSymbolIcon className="inline align-top h-4" /> Email
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500 underline hover:text-gray-400"> <span className="ml-2 text-gray-500 underline hover:text-gray-400">
{info.getValue()} {info.getValue()}
</span> </span>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "} <ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Program Program
</> </>
), ),
cell: (info) => <TagsInput presetValue={info.getValue()} />, cell: (info) => <TagsInput presetValue={info.getValue()} />,
}), }),
]; ];
const [data, setData] = useState<User[]>([...users]); const [data, setData] = useState<User[]>([...users]);
const addUser = () => { const addUser = () => {
setData([...data]); setData([...data]);
}; };
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => { const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
setQuery(String(target.value)); setQuery(String(target.value));
}; };
const handleCellChange = (e: ChangeEvent, key: Key) => { const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
console.log(key); console.log(key);
}; };
// TODO: Filtering // TODO: Filtering
// TODO: Sorting // TODO: Sorting
// added this fn for editing rows // added this fn for editing rows
const handleRowUpdate = (updatedRow: User) => { const handleRowUpdate = (updatedRow: User) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id); const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) { if (dataIndex !== -1) {
const updatedData = [...data]; const updatedData = [...data];
updatedData[dataIndex] = updatedRow; updatedData[dataIndex] = updatedRow;
setData(updatedData); setData(updatedData);
} }
}; };
const table = useReactTable({ const table = useReactTable({
columns, columns,
data, data,
filterFns: { filterFns: {
fuzzy: fuzzyFilter, fuzzy: fuzzyFilter,
}, },
state: { state: {
globalFilter: query, globalFilter: query,
}, },
onGlobalFilterChange: setQuery, onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter, globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const handleRowData = (row: any) => { const handleRowData = (row: any) => {
const rowData: any = {}; const rowData: any = {};
row.cells.forEach((cell: any) => { row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value; rowData[cell.column.id] = cell.value;
}); });
// Use rowData object containing data from all columns for the current row // Use rowData object containing data from all columns for the current row
console.log(rowData); console.log(rowData);
return rowData; return rowData;
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} /> <TableAction query={query} handleChange={handleSearchChange} />
</div> </div>
<table className="w-full text-xs text-left rtl:text-right"> <table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize"> <thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => ( {headerGroup.headers.map((header, i) => (
<th <th
scope="col" scope="col"
className={ className={
"p-2 border-gray-200 border-y font-medium " + "p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1 (1 < i && i < columns.length - 1
? "border-x" ? "border-x"
: "") : "")
} }
key={header.id} key={header.id}
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext()
)} )}
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</thead> </thead>
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual row // Individual row
const isUserVisible = row.original.visible; const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : "" !isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`; }`;
return ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => ( {row.getVisibleCells().map((cell, i) => (
<td <td
key={cell.id} key={cell.id}
className={ className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none" "[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
} }
> >
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext()
)} )}
</td> </td>
))} ))}
</tr> </tr>
); );
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td <td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100} colSpan={100}
onClick={addUser} onClick={addUser}
> >
<span className="flex ml-1 text-gray-500"> <span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" /> <PlusIcon className="inline h-4 mr-1" />
New New
</span> </span>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
); );
}; };

View File

@ -1,33 +1,33 @@
/* An extension of TableCell.tsx that includes an "open" button and the drawer. /* An extension of TableCell.tsx that includes an "open" button and the drawer.
For cells in the "primary" (or first) column of the table. */ For cells in the "primary" (or first) column of the table. */
import Drawer from "@/components/Drawer/Drawer"; import Drawer from "@/components/Drawer/Drawer";
import { TableCell } from "./TableCell"; import { TableCell } from "./TableCell";
import { SetStateAction, useState } from "react"; import { SetStateAction, useState } from "react";
export const PrimaryTableCell = ({ getValue, row, column, table }) => { export const PrimaryTableCell = ({ getValue, row, column, table }) => {
const [pageContent, setPageContent] = useState(""); const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent: SetStateAction<string>) => { const handleDrawerContentChange = (newContent: SetStateAction<string>) => {
setPageContent(newContent); setPageContent(newContent);
}; };
return ( return (
<div className="font-semibold group"> <div className="font-semibold group">
<TableCell <TableCell
getValue={getValue} getValue={getValue}
row={row} row={row}
column={column} column={column}
table={table} table={table}
/> />
<span className="absolute right-1 top-1"> <span className="absolute right-1 top-1">
<Drawer <Drawer
title={getValue()} title={getValue()}
editableContent={pageContent} editableContent={pageContent}
onSave={handleDrawerContentChange} onSave={handleDrawerContentChange}
> >
{pageContent} {pageContent}
</Drawer> </Drawer>
</span> </span>
</div> </div>
); );
}; };

View File

@ -1,304 +1,304 @@
// for showcasing to compass // for showcasing to compass
import users from "./users.json"; import users from "./users.json";
import { import {
Cell, Cell,
ColumnDef, ColumnDef,
Row, Row,
createColumnHelper, createColumnHelper,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
sortingFns, sortingFns,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
FunctionComponent, FunctionComponent,
useRef, useRef,
ChangeEventHandler, ChangeEventHandler,
Key, Key,
} from "react"; } from "react";
import { RowOptionMenu } from "./RowOptionMenu"; import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction"; import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { import {
AtSymbolIcon, AtSymbolIcon,
Bars2Icon, Bars2Icon,
ArrowDownCircleIcon, ArrowDownCircleIcon,
PlusIcon, PlusIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils"; import { rankItem } from "@tanstack/match-sorter-utils";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
// For search // For search
const fuzzyFilter = ( const fuzzyFilter = (
row: Row<any>, row: Row<any>,
columnId: string, columnId: string,
value: any, value: any,
addMeta: (meta: any) => void addMeta: (meta: any) => void
) => { ) => {
// Rank the item // Rank the item
const itemRank = rankItem(row.getValue(columnId), value); const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info // Store the ranking info
addMeta(itemRank); addMeta(itemRank);
// Return if the item should be filtered in/out // Return if the item should be filtered in/out
return itemRank.passed; return itemRank.passed;
}; };
// TODO: Rename everything to resources // TODO: Rename everything to resources
export const ResourceTable = ({ users }: { users: Resource[] }) => { export const ResourceTable = ({ users }: { users: Resource[] }) => {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
useEffect(() => { useEffect(() => {
const sortedUsers = [...users].sort((a, b) => const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
setData(sortedUsers); setData(sortedUsers);
}, [users]); }, [users]);
const deleteUser = (userId: number) => { const deleteUser = (userId: number) => {
console.log(data); console.log(data);
setData((currentData) => setData((currentData) =>
currentData.filter((user) => user.id !== userId) currentData.filter((user) => user.id !== userId)
); );
}; };
const hideUser = (userId: number) => { const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`); console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => { setData((currentData) => {
const newData = currentData const newData = currentData
.map((user) => { .map((user) => {
if (user.id === userId) { if (user.id === userId) {
return { ...user, visible: !user.visible }; return { ...user, visible: !user.visible };
} }
return user; return user;
}) })
.sort((a, b) => .sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
console.log(newData); console.log(newData);
return newData; return newData;
}); });
}; };
const [presetOptions, setPresetOptions] = useState([ const [presetOptions, setPresetOptions] = useState([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const [tagColors, setTagColors] = useState(new Map()); const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => { const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) { if (!tagColors.has(tag)) {
const colors = [ const colors = [
"bg-cyan-100", "bg-cyan-100",
"bg-blue-100", "bg-blue-100",
"bg-green-100", "bg-green-100",
"bg-yellow-100", "bg-yellow-100",
"bg-purple-100", "bg-purple-100",
]; ];
const randomColor = const randomColor =
colors[Math.floor(Math.random() * colors.length)]; colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor)); setTagColors(new Map(tagColors).set(tag, randomColor));
} }
return tagColors.get(tag); return tagColors.get(tag);
}; };
const columns = [ const columns = [
columnHelper.display({ columnHelper.display({
id: "options", id: "options",
cell: (props) => ( cell: (props) => (
<RowOptionMenu <RowOptionMenu
onDelete={() => {}} onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)} onHide={() => hideUser(props.row.original.id)}
/> />
), ),
}), }),
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Name <Bars2Icon className="inline align-top h-4" /> Name
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
rowData={info.row.original} rowData={info.row.original}
onRowUpdate={handleRowUpdate} onRowUpdate={handleRowUpdate}
/> />
), ),
}), }),
columnHelper.accessor("link", { columnHelper.accessor("link", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Link <Bars2Icon className="inline align-top h-4" /> Link
</> </>
), ),
cell: (info) => ( cell: (info) => (
<a <a
href={info.getValue()} href={info.getValue()}
target={"_blank"} target={"_blank"}
className="ml-2 text-gray-500 underline hover:text-gray-400" className="ml-2 text-gray-500 underline hover:text-gray-400"
> >
{info.getValue()} {info.getValue()}
</a> </a>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Program <Bars2Icon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => <TagsInput presetValue={info.getValue()} />, cell: (info) => <TagsInput presetValue={info.getValue()} />,
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Summary <Bars2Icon className="inline align-top h-4" /> Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <span className="ml-2 text-gray-500">{info.getValue()}</span>
), ),
}), }),
]; ];
const [data, setData] = useState<Resource[]>([...users]); const [data, setData] = useState<Resource[]>([...users]);
const addUser = () => { const addUser = () => {
setData([...data]); setData([...data]);
}; };
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => { const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
setQuery(String(target.value)); setQuery(String(target.value));
}; };
const handleCellChange = (e: ChangeEvent, key: Key) => { const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
console.log(key); console.log(key);
}; };
// TODO: Filtering // TODO: Filtering
// TODO: Sorting // TODO: Sorting
// added this fn for editing rows // added this fn for editing rows
const handleRowUpdate = (updatedRow: Resource) => { const handleRowUpdate = (updatedRow: Resource) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id); const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) { if (dataIndex !== -1) {
const updatedData = [...data]; const updatedData = [...data];
updatedData[dataIndex] = updatedRow; updatedData[dataIndex] = updatedRow;
setData(updatedData); setData(updatedData);
} }
}; };
const table = useReactTable({ const table = useReactTable({
columns, columns,
data, data,
filterFns: { filterFns: {
fuzzy: fuzzyFilter, fuzzy: fuzzyFilter,
}, },
state: { state: {
globalFilter: query, globalFilter: query,
}, },
onGlobalFilterChange: setQuery, onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter, globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const handleRowData = (row: any) => { const handleRowData = (row: any) => {
const rowData: any = {}; const rowData: any = {};
row.cells.forEach((cell: any) => { row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value; rowData[cell.column.id] = cell.value;
}); });
// Use rowData object containing data from all columns for the current row // Use rowData object containing data from all columns for the current row
console.log(rowData); console.log(rowData);
return rowData; return rowData;
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} /> <TableAction query={query} handleChange={handleSearchChange} />
</div> </div>
<table className="w-full text-xs text-left rtl:text-right"> <table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize"> <thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => ( {headerGroup.headers.map((header, i) => (
<th <th
scope="col" scope="col"
className={ className={
"p-2 border-gray-200 border-y font-medium " + "p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1 (1 < i && i < columns.length - 1
? "border-x" ? "border-x"
: "") : "")
} }
key={header.id} key={header.id}
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext()
)} )}
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</thead> </thead>
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual row // Individual row
const isUserVisible = row.original.visible; const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : "" !isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`; }`;
return ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => ( {row.getVisibleCells().map((cell, i) => (
<td <td
key={cell.id} key={cell.id}
className={ className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none" "[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
} }
> >
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext()
)} )}
</td> </td>
))} ))}
</tr> </tr>
); );
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td <td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100} colSpan={100}
onClick={addUser} onClick={addUser}
> >
<span className="flex ml-1 text-gray-500"> <span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" /> <PlusIcon className="inline h-4 mr-1" />
New New
</span> </span>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
); );
}; };

View File

@ -1,28 +1,28 @@
import Drawer from "@/components/Drawer/Drawer"; import Drawer from "@/components/Drawer/Drawer";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
export const RowOpenAction = ({ title, rowData, onRowUpdate }) => { export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
const [pageContent, setPageContent] = useState(""); const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent) => { const handleDrawerContentChange = (newContent) => {
setPageContent(newContent); setPageContent(newContent);
}; };
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">
{title} {title}
<span> <span>
{/* Added OnRowUpdate to drawer */} {/* Added OnRowUpdate to drawer */}
<Drawer <Drawer
title="My Drawer Title" title="My Drawer Title"
editableContent={pageContent} editableContent={pageContent}
rowContent={rowData} rowContent={rowData}
onSave={handleDrawerContentChange} onSave={handleDrawerContentChange}
onRowUpdate={onRowUpdate} onRowUpdate={onRowUpdate}
> >
{pageContent} {pageContent}
</Drawer> </Drawer>
</span> </span>
</div> </div>
); );
}; };

View File

@ -1,18 +1,18 @@
import React from "react"; import React from "react";
import { import {
TrashIcon, TrashIcon,
DocumentDuplicateIcon, DocumentDuplicateIcon,
ArrowUpRightIcon, ArrowUpRightIcon,
EyeSlashIcon, EyeSlashIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
export const RowOption = ({ icon: Icon, label, onClick }) => { export const RowOption = ({ icon: Icon, label, onClick }) => {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="hover:bg-gray-100 flex items-center gap-2 p-2 w-full" className="hover:bg-gray-100 flex items-center gap-2 p-2 w-full"
> >
<Icon className="inline h-4" /> {label} <Icon className="inline h-4" /> {label}
</button> </button>
); );
}; };

View File

@ -1,46 +1,46 @@
//delete, duplicate, open //delete, duplicate, open
import { import {
TrashIcon, TrashIcon,
DocumentDuplicateIcon, DocumentDuplicateIcon,
ArrowUpRightIcon, ArrowUpRightIcon,
EllipsisVerticalIcon, EllipsisVerticalIcon,
EyeSlashIcon, EyeSlashIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import Button from "../Button"; import Button from "../Button";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { RowOption } from "./RowOption"; import { RowOption } from "./RowOption";
export const RowOptionMenu = ({ onDelete, onHide }) => { export const RowOptionMenu = ({ onDelete, onHide }) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const openMenu = () => setMenuOpen(true); const openMenu = () => setMenuOpen(true);
const closeMenu = () => setMenuOpen(false); const closeMenu = () => setMenuOpen(false);
// TODO: Hide menu if clicked elsewhere // TODO: Hide menu if clicked elsewhere
return ( return (
<> <>
<button <button
className="items-end" className="items-end"
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
> >
<EllipsisVerticalIcon className="h-4" /> <EllipsisVerticalIcon className="h-4" />
</button> </button>
<div <div
className={ className={
"justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" + "justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" +
(!menuOpen ? " invisible" : "") (!menuOpen ? " invisible" : "")
} }
> >
<RowOption icon={TrashIcon} label="Delete" onClick={onDelete} /> <RowOption icon={TrashIcon} label="Delete" onClick={onDelete} />
<RowOption <RowOption
icon={ArrowUpRightIcon} icon={ArrowUpRightIcon}
label="Open" label="Open"
onClick={() => { onClick={() => {
/* handle open */ /* handle open */
}} }}
/> />
<RowOption icon={EyeSlashIcon} label="Hide" onClick={onHide} /> <RowOption icon={EyeSlashIcon} label="Hide" onClick={onHide} />
</div> </div>
</> </>
); );
}; };

View File

@ -1,312 +1,312 @@
// for showcasing to compass // for showcasing to compass
import users from "./users.json"; import users from "./users.json";
import { import {
Cell, Cell,
ColumnDef, ColumnDef,
Row, Row,
createColumnHelper, createColumnHelper,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
sortingFns, sortingFns,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
FunctionComponent, FunctionComponent,
useRef, useRef,
ChangeEventHandler, ChangeEventHandler,
Key, Key,
} from "react"; } from "react";
import { RowOptionMenu } from "./RowOptionMenu"; import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction"; import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { import {
AtSymbolIcon, AtSymbolIcon,
Bars2Icon, Bars2Icon,
ArrowDownCircleIcon, ArrowDownCircleIcon,
PlusIcon, PlusIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils"; import { rankItem } from "@tanstack/match-sorter-utils";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
// For search // For search
const fuzzyFilter = ( const fuzzyFilter = (
row: Row<any>, row: Row<any>,
columnId: string, columnId: string,
value: any, value: any,
addMeta: (meta: any) => void addMeta: (meta: any) => void
) => { ) => {
// Rank the item // Rank the item
const itemRank = rankItem(row.getValue(columnId), value); const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info // Store the ranking info
addMeta(itemRank); addMeta(itemRank);
// Return if the item should be filtered in/out // Return if the item should be filtered in/out
return itemRank.passed; return itemRank.passed;
}; };
// TODO: Rename everything to service // TODO: Rename everything to service
export const ServiceTable = ({ users }: { users: Service[] }) => { export const ServiceTable = ({ users }: { users: Service[] }) => {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
useEffect(() => { useEffect(() => {
const sortedUsers = [...users].sort((a, b) => const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
setData(sortedUsers); setData(sortedUsers);
}, [users]); }, [users]);
const deleteUser = (userId: number) => { const deleteUser = (userId: number) => {
console.log(data); console.log(data);
setData((currentData) => setData((currentData) =>
currentData.filter((user) => user.id !== userId) currentData.filter((user) => user.id !== userId)
); );
}; };
const hideUser = (userId: number) => { const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`); console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => { setData((currentData) => {
const newData = currentData const newData = currentData
.map((user) => { .map((user) => {
if (user.id === userId) { if (user.id === userId) {
return { ...user, visible: !user.visible }; return { ...user, visible: !user.visible };
} }
return user; return user;
}) })
.sort((a, b) => .sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
console.log(newData); console.log(newData);
return newData; return newData;
}); });
}; };
const [presetOptions, setPresetOptions] = useState([ const [presetOptions, setPresetOptions] = useState([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const [tagColors, setTagColors] = useState(new Map()); const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => { const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) { if (!tagColors.has(tag)) {
const colors = [ const colors = [
"bg-cyan-100", "bg-cyan-100",
"bg-blue-100", "bg-blue-100",
"bg-green-100", "bg-green-100",
"bg-yellow-100", "bg-yellow-100",
"bg-purple-100", "bg-purple-100",
]; ];
const randomColor = const randomColor =
colors[Math.floor(Math.random() * colors.length)]; colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor)); setTagColors(new Map(tagColors).set(tag, randomColor));
} }
return tagColors.get(tag); return tagColors.get(tag);
}; };
const columns = [ const columns = [
columnHelper.display({ columnHelper.display({
id: "options", id: "options",
cell: (props) => ( cell: (props) => (
<RowOptionMenu <RowOptionMenu
onDelete={() => {}} onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)} onHide={() => hideUser(props.row.original.id)}
/> />
), ),
}), }),
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Name <Bars2Icon className="inline align-top h-4" /> Name
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
rowData={info.row.original} rowData={info.row.original}
onRowUpdate={handleRowUpdate} onRowUpdate={handleRowUpdate}
/> />
), ),
}), }),
columnHelper.accessor("status", { columnHelper.accessor("status", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Status <Bars2Icon className="inline align-top h-4" /> Status
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <span className="ml-2 text-gray-500">{info.getValue()}</span>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Program <Bars2Icon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => <TagsInput presetValue={info.getValue()} />, cell: (info) => <TagsInput presetValue={info.getValue()} />,
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Requirements <Bars2Icon className="inline align-top h-4" /> Requirements
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput
presetValue={ presetValue={
info.getValue()[0] !== "" ? info.getValue() : ["N/A"] info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
} }
/> />
), ),
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Summary <Bars2Icon className="inline align-top h-4" /> Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <span className="ml-2 text-gray-500">{info.getValue()}</span>
), ),
}), }),
]; ];
const [data, setData] = useState<Service[]>([...users]); const [data, setData] = useState<Service[]>([...users]);
const addUser = () => { const addUser = () => {
setData([...data]); setData([...data]);
}; };
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => { const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
setQuery(String(target.value)); setQuery(String(target.value));
}; };
const handleCellChange = (e: ChangeEvent, key: Key) => { const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
console.log(key); console.log(key);
}; };
// TODO: Filtering // TODO: Filtering
// TODO: Sorting // TODO: Sorting
// added this fn for editing rows // added this fn for editing rows
const handleRowUpdate = (updatedRow: Service) => { const handleRowUpdate = (updatedRow: Service) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id); const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) { if (dataIndex !== -1) {
const updatedData = [...data]; const updatedData = [...data];
updatedData[dataIndex] = updatedRow; updatedData[dataIndex] = updatedRow;
setData(updatedData); setData(updatedData);
} }
}; };
const table = useReactTable({ const table = useReactTable({
columns, columns,
data, data,
filterFns: { filterFns: {
fuzzy: fuzzyFilter, fuzzy: fuzzyFilter,
}, },
state: { state: {
globalFilter: query, globalFilter: query,
}, },
onGlobalFilterChange: setQuery, onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter, globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const handleRowData = (row: any) => { const handleRowData = (row: any) => {
const rowData: any = {}; const rowData: any = {};
row.cells.forEach((cell: any) => { row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value; rowData[cell.column.id] = cell.value;
}); });
// Use rowData object containing data from all columns for the current row // Use rowData object containing data from all columns for the current row
console.log(rowData); console.log(rowData);
return rowData; return rowData;
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} /> <TableAction query={query} handleChange={handleSearchChange} />
</div> </div>
<table className="w-full text-xs text-left rtl:text-right"> <table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize"> <thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => ( {headerGroup.headers.map((header, i) => (
<th <th
scope="col" scope="col"
className={ className={
"p-2 border-gray-200 border-y font-medium " + "p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1 (1 < i && i < columns.length - 1
? "border-x" ? "border-x"
: "") : "")
} }
key={header.id} key={header.id}
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext()
)} )}
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</thead> </thead>
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual row // Individual row
const isUserVisible = row.original.visible; const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : "" !isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`; }`;
return ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => ( {row.getVisibleCells().map((cell, i) => (
<td <td
key={cell.id} key={cell.id}
className={ className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none" "[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
} }
> >
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext()
)} )}
</td> </td>
))} ))}
</tr> </tr>
); );
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td <td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100} colSpan={100}
onClick={addUser} onClick={addUser}
> >
<span className="flex ml-1 text-gray-500"> <span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" /> <PlusIcon className="inline h-4 mr-1" />
New New
</span> </span>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
); );
}; };

View File

@ -1,69 +1,69 @@
// TableAction.tsx // TableAction.tsx
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react"; import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
import { FilterBox } from "../FilterBox"; import { FilterBox } from "../FilterBox";
type TableActionProps = { type TableActionProps = {
query: string; query: string;
handleChange: ChangeEventHandler<HTMLInputElement>; handleChange: ChangeEventHandler<HTMLInputElement>;
}; };
export const TableAction: FunctionComponent<TableActionProps> = ({ export const TableAction: FunctionComponent<TableActionProps> = ({
query, query,
handleChange, handleChange,
}) => { }) => {
const searchInput = useRef<HTMLInputElement>(null); const searchInput = useRef<HTMLInputElement>(null);
const [searchActive, setSearchActive] = useState(false); const [searchActive, setSearchActive] = useState(false);
const [showFilterBox, setShowFilterBox] = useState(false); const [showFilterBox, setShowFilterBox] = useState(false);
const activateSearch = () => { const activateSearch = () => {
setSearchActive(true); setSearchActive(true);
if (searchInput.current === null) { if (searchInput.current === null) {
return; return;
} }
searchInput.current.focus(); searchInput.current.focus();
searchInput.current.addEventListener("focusout", () => { searchInput.current.addEventListener("focusout", () => {
if (searchInput.current?.value.trim() === "") { if (searchInput.current?.value.trim() === "") {
searchInput.current.value = ""; searchInput.current.value = "";
deactivateSearch(); deactivateSearch();
} }
}); });
}; };
const deactivateSearch = () => setSearchActive(false); const deactivateSearch = () => setSearchActive(false);
const toggleFilterBox = () => setShowFilterBox((prev) => !prev); const toggleFilterBox = () => setShowFilterBox((prev) => !prev);
return ( return (
<div className="w-auto flex flex-row gap-x-0.5 items-center justify-between text-xs font-medium text-gray-500 p-2"> <div className="w-auto flex flex-row gap-x-0.5 items-center justify-between text-xs font-medium text-gray-500 p-2">
<span <span
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50" className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50"
onClick={toggleFilterBox} onClick={toggleFilterBox}
> >
Filter Filter
</span> </span>
{showFilterBox && <FilterBox />} {showFilterBox && <FilterBox />}
<span className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100"> <span className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100">
Sort Sort
</span> </span>
<span <span
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100" className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100"
onClick={activateSearch} onClick={activateSearch}
> >
<MagnifyingGlassIcon className="w-4 h-4 inline" /> <MagnifyingGlassIcon className="w-4 h-4 inline" />
</span> </span>
<input <input
ref={searchInput} ref={searchInput}
className={ className={
"outline-none transition-all duration-300 " + "outline-none transition-all duration-300 " +
(searchActive ? "w-48" : "w-0") (searchActive ? "w-48" : "w-0")
} }
type="text" type="text"
name="search" name="search"
placeholder="Type to search..." placeholder="Type to search..."
value={query ?? ""} value={query ?? ""}
onChange={handleChange} onChange={handleChange}
/> />
</div> </div>
); );
}; };

View File

@ -1,29 +1,29 @@
/* A lone table cell. Passed in for "cell" for a TanStack Table. */ /* A lone table cell. Passed in for "cell" for a TanStack Table. */
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
export const TableCell = ({ getValue, row, column, table }) => { export const TableCell = ({ getValue, row, column, table }) => {
const initialValue = getValue(); const initialValue = getValue();
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
useEffect(() => { useEffect(() => {
setValue(initialValue); setValue(initialValue);
}, [initialValue]); }, [initialValue]);
const onBlur = () => { const onBlur = () => {
table.options.meta?.updateData(row.index, column.id, value); table.options.meta?.updateData(row.index, column.id, value);
}; };
// focus:border focus:border-gray-200 // focus:border focus:border-gray-200
const className = const className =
"w-full p-3 bg-inherit rounded-md outline-none border border-transparent relative " + "w-full p-3 bg-inherit rounded-md outline-none border border-transparent relative " +
"focus:shadow-md focus:border-gray-200 focus:bg-white focus:z-20 focus:p-4 focus:-m-1 " + "focus:shadow-md focus:border-gray-200 focus:bg-white focus:z-20 focus:p-4 focus:-m-1 " +
"focus:w-[calc(100%+0.5rem)]"; "focus:w-[calc(100%+0.5rem)]";
return ( return (
<input <input
className={className} className={className}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
onBlur={onBlur} onBlur={onBlur}
/> />
); );
}; };

View File

@ -1,222 +1,222 @@
[ [
{ {
"id": 0, "id": 0,
"created_at": 1711482132230, "created_at": 1711482132230,
"username": "Bo_Pfeffer", "username": "Bo_Pfeffer",
"role": "ADMIN", "role": "ADMIN",
"email": "Bo.Pfeffer@gmail.com", "email": "Bo.Pfeffer@gmail.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 1, "id": 1,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Marianna_Heathcote76", "username": "Marianna_Heathcote76",
"role": "ADMIN", "role": "ADMIN",
"email": "Marianna_Heathcote14@yahoo.com", "email": "Marianna_Heathcote14@yahoo.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 1, "experience": 1,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 2, "id": 2,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Queenie_Schroeder", "username": "Queenie_Schroeder",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Queenie_Schroeder@yahoo.com", "email": "Queenie_Schroeder@yahoo.com",
"program": "COMMUNITY", "program": "COMMUNITY",
"experience": 5, "experience": 5,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 3, "id": 3,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Arne.Bode", "username": "Arne.Bode",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Arne.Bode@hotmail.com", "email": "Arne.Bode@hotmail.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 3, "experience": 3,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 4, "id": 4,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Maia.Zulauf9", "username": "Maia.Zulauf9",
"role": "ADMIN", "role": "ADMIN",
"email": "Maia_Zulauf@gmail.com", "email": "Maia_Zulauf@gmail.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 5, "experience": 5,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 5, "id": 5,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "River_Bauch", "username": "River_Bauch",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "River.Bauch@yahoo.com", "email": "River.Bauch@yahoo.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 6, "id": 6,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Virgil.Hilll", "username": "Virgil.Hilll",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Virgil.Hilll@yahoo.com", "email": "Virgil.Hilll@yahoo.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 3, "experience": 3,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 7, "id": 7,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Bridget_Cartwright", "username": "Bridget_Cartwright",
"role": "ADMIN", "role": "ADMIN",
"email": "Bridget_Cartwright@yahoo.com", "email": "Bridget_Cartwright@yahoo.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 3, "experience": 3,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 8, "id": 8,
"created_at": 1711482132231, "created_at": 1711482132231,
"username": "Glennie_Keebler64", "username": "Glennie_Keebler64",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "Glennie_Keebler60@yahoo.com", "email": "Glennie_Keebler60@yahoo.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 9, "id": 9,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Orin.Jenkins53", "username": "Orin.Jenkins53",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "Orin.Jenkins@gmail.com", "email": "Orin.Jenkins@gmail.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 1, "experience": 1,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 10, "id": 10,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Zachery.Rosenbaum", "username": "Zachery.Rosenbaum",
"role": "ADMIN", "role": "ADMIN",
"email": "Zachery.Rosenbaum@hotmail.com", "email": "Zachery.Rosenbaum@hotmail.com",
"program": "COMMUNITY", "program": "COMMUNITY",
"experience": 3, "experience": 3,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 11, "id": 11,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Phoebe.Ziemann", "username": "Phoebe.Ziemann",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "Phoebe_Ziemann92@gmail.com", "email": "Phoebe_Ziemann92@gmail.com",
"program": "COMMUNITY", "program": "COMMUNITY",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 12, "id": 12,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Bradford_Conroy53", "username": "Bradford_Conroy53",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Bradford_Conroy94@hotmail.com", "email": "Bradford_Conroy94@hotmail.com",
"program": "COMMUNITY", "program": "COMMUNITY",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 13, "id": 13,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Florine_Strosin55", "username": "Florine_Strosin55",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Florine.Strosin29@hotmail.com", "email": "Florine.Strosin29@hotmail.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 1, "experience": 1,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 14, "id": 14,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Constance.Doyle59", "username": "Constance.Doyle59",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "Constance_Doyle@hotmail.com", "email": "Constance_Doyle@hotmail.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 3, "experience": 3,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 15, "id": 15,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Chauncey_Lockman", "username": "Chauncey_Lockman",
"role": "ADMIN", "role": "ADMIN",
"email": "Chauncey_Lockman@yahoo.com", "email": "Chauncey_Lockman@yahoo.com",
"program": "DOMESTIC", "program": "DOMESTIC",
"experience": 5, "experience": 5,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 16, "id": 16,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Esther_Wuckert-Larson26", "username": "Esther_Wuckert-Larson26",
"role": "EMPLOYEE", "role": "EMPLOYEE",
"email": "Esther_Wuckert-Larson@gmail.com", "email": "Esther_Wuckert-Larson@gmail.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 0, "experience": 0,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 17, "id": 17,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Jewel.Kunde", "username": "Jewel.Kunde",
"role": "VOLUNTEER", "role": "VOLUNTEER",
"email": "Jewel_Kunde29@gmail.com", "email": "Jewel_Kunde29@gmail.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 5, "experience": 5,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 18, "id": 18,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Hildegard_Parker92", "username": "Hildegard_Parker92",
"role": "ADMIN", "role": "ADMIN",
"email": "Hildegard_Parker74@yahoo.com", "email": "Hildegard_Parker74@yahoo.com",
"program": "ECONOMIC", "program": "ECONOMIC",
"experience": 2, "experience": 2,
"group": "", "group": "",
"visible": true "visible": true
}, },
{ {
"id": 19, "id": 19,
"created_at": 1711482132232, "created_at": 1711482132232,
"username": "Jordane.Lakin2", "username": "Jordane.Lakin2",
"role": "ADMIN", "role": "ADMIN",
"email": "Jordane_Lakin@hotmail.com", "email": "Jordane_Lakin@hotmail.com",
"program": "COMMUNITY", "program": "COMMUNITY",
"experience": 1, "experience": 1,
"group": "", "group": "",
"visible": true "visible": true
} }
] ]

View File

@ -1,12 +1,12 @@
import { Tag } from "./Tag"; import { Tag } from "./Tag";
export const CreateNewTagAction = ({ input }) => { export const CreateNewTagAction = ({ input }) => {
return ( return (
<div className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center"> <div className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center">
<p className="capitalize">Create</p> <p className="capitalize">Create</p>
<Tag active={false} onDelete={null}> <Tag active={false} onDelete={null}>
{input} {input}
</Tag> </Tag>
</div> </div>
); );
}; };

View File

@ -1,49 +1,49 @@
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid"; import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react"; import { useState } from "react";
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => { export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState(tag); const [inputValue, setInputValue] = useState(tag);
const editTagOption = (e) => { const editTagOption = (e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleEditTag(tag, inputValue); handleEditTag(tag, inputValue);
setVisible(false); setVisible(false);
} }
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value); setInputValue(e.target.value);
}; };
return ( return (
<div> <div>
<EllipsisHorizontalIcon <EllipsisHorizontalIcon
className="w-5 text-gray-500" className="w-5 text-gray-500"
onClick={() => setVisible(!isVisible)} onClick={() => setVisible(!isVisible)}
/> />
{isVisible && ( {isVisible && (
<div className="absolute flex flex-col justify-start z-50 rounded-md bg-white border border-gray-200 shadow p-2 space-y-2"> <div className="absolute flex flex-col justify-start z-50 rounded-md bg-white border border-gray-200 shadow p-2 space-y-2">
<input <input
type="text" type="text"
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={editTagOption} onKeyDown={editTagOption}
autoFocus autoFocus
className="bg-gray-50 text-2xs focus:outline-none rounded-md font-normal text-gray-800 p-1 border-2 focus:border-blue-200" className="bg-gray-50 text-2xs focus:outline-none rounded-md font-normal text-gray-800 p-1 border-2 focus:border-blue-200"
/> />
<button <button
onClick={() => { onClick={() => {
handleDeleteTag(inputValue); handleDeleteTag(inputValue);
setVisible(false); setVisible(false);
}} }}
className="justify-start flex flex-row space-x-4 hover:bg-gray-100 rounded-md items-center p-2 px-2" className="justify-start flex flex-row space-x-4 hover:bg-gray-100 rounded-md items-center p-2 px-2"
> >
<TrashIcon className="w-3 h-3" /> <TrashIcon className="w-3 h-3" />
<p>Delete</p> <p>Delete</p>
</button> </button>
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@ -1,174 +1,174 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import "tailwindcss/tailwind.css"; import "tailwindcss/tailwind.css";
import { TagsArray } from "./TagsArray"; import { TagsArray } from "./TagsArray";
import { TagDropdown } from "./TagDropdown"; import { TagDropdown } from "./TagDropdown";
import { CreateNewTagAction } from "./CreateNewTagAction"; import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps { interface TagsInputProps {
presetOptions: string[]; presetOptions: string[];
presetValue: string | string[]; presetValue: string | string[];
setPresetOptions: () => {}; setPresetOptions: () => {};
getTagColor: () => {}; getTagColor: () => {};
} }
const TagsInput: React.FC<TagsInputProps> = ({ const TagsInput: React.FC<TagsInputProps> = ({
presetValue, presetValue,
presetOptions, presetOptions,
setPresetOptions, setPresetOptions,
getTagColor, getTagColor,
}) => { }) => {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [cellSelected, setCellSelected] = useState(false); const [cellSelected, setCellSelected] = useState(false);
const [tags, setTags] = useState<Set<string>>( const [tags, setTags] = useState<Set<string>>(
typeof presetValue === "string" typeof presetValue === "string"
? new Set([presetValue]) ? new Set([presetValue])
: new Set(presetValue) : new Set(presetValue)
); );
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions)); const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
const dropdown = useRef<HTMLDivElement>(null); const dropdown = useRef<HTMLDivElement>(null);
const handleClick = () => { const handleClick = () => {
if (!cellSelected) { if (!cellSelected) {
setCellSelected(true); setCellSelected(true);
// Add event listener only after setting cellSelected to true // Add event listener only after setting cellSelected to true
setTimeout(() => { setTimeout(() => {
window.addEventListener("click", handleOutsideClick); window.addEventListener("click", handleOutsideClick);
}, 100); }, 100);
} }
}; };
// TODO: Fix MouseEvent type and remove the as Node as that is completely wrong // TODO: Fix MouseEvent type and remove the as Node as that is completely wrong
const handleOutsideClick = (event: MouseEvent) => { const handleOutsideClick = (event: MouseEvent) => {
if ( if (
dropdown.current && dropdown.current &&
!dropdown.current.contains(event.target as Node) !dropdown.current.contains(event.target as Node)
) { ) {
setCellSelected(false); setCellSelected(false);
// Remove event listener after handling outside click // Remove event listener after handling outside click
window.removeEventListener("click", handleOutsideClick); window.removeEventListener("click", handleOutsideClick);
} }
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions(() => { setOptions(() => {
const newOptions = presetOptions.filter((item) => const newOptions = presetOptions.filter((item) =>
item.includes(e.target.value.toLowerCase()) item.includes(e.target.value.toLowerCase())
); );
return new Set(newOptions); return new Set(newOptions);
}); });
setInputValue(e.target.value); // Update input value state setInputValue(e.target.value); // Update input value state
}; };
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && inputValue.trim()) { if (e.key === "Enter" && inputValue.trim()) {
// setPresetOptions((prevPreset) => { // setPresetOptions((prevPreset) => {
// const uniqueSet = new Set(presetOptions); // const uniqueSet = new Set(presetOptions);
// uniqueSet.add(inputValue); // uniqueSet.add(inputValue);
// return Array.from(uniqueSet); // return Array.from(uniqueSet);
// }); // });
setTags((prevTags) => new Set(prevTags).add(inputValue)); setTags((prevTags) => new Set(prevTags).add(inputValue));
setOptions((prevOptions) => new Set(prevOptions).add(inputValue)); setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
setInputValue(""); setInputValue("");
} }
}; };
const handleSelectTag = (tagToAdd: string) => { const handleSelectTag = (tagToAdd: string) => {
if (!tags.has(tagToAdd)) { if (!tags.has(tagToAdd)) {
// Corrected syntax for checking if a Set contains an item // Corrected syntax for checking if a Set contains an item
setTags((prevTags) => new Set(prevTags).add(tagToAdd)); setTags((prevTags) => new Set(prevTags).add(tagToAdd));
} }
}; };
const handleDeleteTag = (tagToDelete: string) => { const handleDeleteTag = (tagToDelete: string) => {
setTags((prevTags) => { setTags((prevTags) => {
const updatedTags = new Set(prevTags); const updatedTags = new Set(prevTags);
updatedTags.delete(tagToDelete); updatedTags.delete(tagToDelete);
return updatedTags; return updatedTags;
}); });
}; };
const handleDeleteTagOption = (tagToDelete: string) => { const handleDeleteTagOption = (tagToDelete: string) => {
// setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete)); // setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete));
setOptions((prevOptions) => { setOptions((prevOptions) => {
const updatedOptions = new Set(prevOptions); const updatedOptions = new Set(prevOptions);
updatedOptions.delete(tagToDelete); updatedOptions.delete(tagToDelete);
return updatedOptions; return updatedOptions;
}); });
if (tags.has(tagToDelete)) { if (tags.has(tagToDelete)) {
handleDeleteTag(tagToDelete); handleDeleteTag(tagToDelete);
} }
}; };
const handleEditTag = (oldTag: string, newTag: string) => { const handleEditTag = (oldTag: string, newTag: string) => {
if (oldTag !== newTag) { if (oldTag !== newTag) {
setTags((prevTags) => { setTags((prevTags) => {
const tagsArray = Array.from(prevTags); const tagsArray = Array.from(prevTags);
const oldTagIndex = tagsArray.indexOf(oldTag); const oldTagIndex = tagsArray.indexOf(oldTag);
if (oldTagIndex !== -1) { if (oldTagIndex !== -1) {
tagsArray.splice(oldTagIndex, 1, newTag); tagsArray.splice(oldTagIndex, 1, newTag);
} }
return new Set(tagsArray); return new Set(tagsArray);
}); });
setOptions((prevOptions) => { setOptions((prevOptions) => {
const optionsArray = Array.from(prevOptions); const optionsArray = Array.from(prevOptions);
const oldTagIndex = optionsArray.indexOf(oldTag); const oldTagIndex = optionsArray.indexOf(oldTag);
if (oldTagIndex !== -1) { if (oldTagIndex !== -1) {
optionsArray.splice(oldTagIndex, 1, newTag); optionsArray.splice(oldTagIndex, 1, newTag);
} }
return new Set(optionsArray); return new Set(optionsArray);
}); });
} }
}; };
return ( return (
<div className="cursor-pointer" onClick={handleClick}> <div className="cursor-pointer" onClick={handleClick}>
{!cellSelected ? ( {!cellSelected ? (
<TagsArray <TagsArray
active={true} active={true}
handleDelete={handleDeleteTag} handleDelete={handleDeleteTag}
tags={tags} tags={tags}
/> />
) : ( ) : (
<div ref={dropdown}> <div ref={dropdown}>
<div className="absolute w-64 z-50 ml-1 mt-5"> <div className="absolute w-64 z-50 ml-1 mt-5">
<div className="rounded-md border border-gray-200 shadow"> <div className="rounded-md border border-gray-200 shadow">
<div className="flex flex-wrap rounded-t-md items-center gap-2 bg-gray-50 p-2"> <div className="flex flex-wrap rounded-t-md items-center gap-2 bg-gray-50 p-2">
<TagsArray <TagsArray
handleDelete={handleDeleteTag} handleDelete={handleDeleteTag}
active active
tags={tags} tags={tags}
/> />
<input <input
type="text" type="text"
value={inputValue} value={inputValue}
placeholder="Search for an option..." placeholder="Search for an option..."
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleAddTag} onKeyDown={handleAddTag}
className="focus:outline-none bg-transparent" className="focus:outline-none bg-transparent"
autoFocus autoFocus
/> />
</div> </div>
<div className="flex rounded-b-md bg-white flex-col border-t border-gray-100 text-2xs font-medium text-gray-500 p-2"> <div className="flex rounded-b-md bg-white flex-col border-t border-gray-100 text-2xs font-medium text-gray-500 p-2">
<p className="capitalize"> <p className="capitalize">
Select an option or create one Select an option or create one
</p> </p>
<TagDropdown <TagDropdown
handleDeleteTag={handleDeleteTagOption} handleDeleteTag={handleDeleteTagOption}
handleEditTag={handleEditTag} handleEditTag={handleEditTag}
handleAdd={handleSelectTag} handleAdd={handleSelectTag}
tags={options} tags={options}
/> />
{inputValue.length > 0 && ( {inputValue.length > 0 && (
<CreateNewTagAction input={inputValue} /> <CreateNewTagAction input={inputValue} />
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
}; };
export default TagsInput; export default TagsInput;

View File

@ -1,17 +1,17 @@
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
export const Tag = ({ children, handleDelete, active = false }) => { export const Tag = ({ children, handleDelete, active = false }) => {
return ( return (
<span <span
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`} className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
> >
{children} {children}
{active && handleDelete && ( {active && handleDelete && (
<button onClick={() => handleDelete(children)}> <button onClick={() => handleDelete(children)}>
<XMarkIcon className={`ml-1 w-3 text-purple-500`} /> <XMarkIcon className={`ml-1 w-3 text-purple-500`} />
</button> </button>
)} )}
</span> </span>
); );
}; };

View File

@ -1,29 +1,29 @@
import { Tag } from "./Tag"; import { Tag } from "./Tag";
import { DropdownAction } from "./DropdownAction"; import { DropdownAction } from "./DropdownAction";
export const TagDropdown = ({ export const TagDropdown = ({
tags, tags,
handleEditTag, handleEditTag,
handleDeleteTag, handleDeleteTag,
handleAdd, handleAdd,
}) => { }) => {
return ( return (
<div className="z-50 flex flex-col space-y-2 mt-2"> <div className="z-50 flex flex-col space-y-2 mt-2">
{Array.from(tags).map((tag, index) => ( {Array.from(tags).map((tag, index) => (
<div <div
key={index} key={index}
className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100" className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100"
> >
<button onClick={() => handleAdd(tag)}> <button onClick={() => handleAdd(tag)}>
<Tag>{tag}</Tag> <Tag>{tag}</Tag>
</button> </button>
<DropdownAction <DropdownAction
handleDeleteTag={handleDeleteTag} handleDeleteTag={handleDeleteTag}
handleEditTag={handleEditTag} handleEditTag={handleEditTag}
tag={tag} tag={tag}
/> />
</div> </div>
))} ))}
</div> </div>
); );
}; };

View File

@ -1,27 +1,27 @@
import { Tag } from "./Tag"; import { Tag } from "./Tag";
export interface Tags { export interface Tags {
tags: Set<string>; tags: Set<string>;
handleDelete: (tag: string) => void; handleDelete: (tag: string) => void;
active: boolean; active: boolean;
} }
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => { export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
console.log(tags); console.log(tags);
return ( return (
<div className="flex ml-2 flex-wrap gap-2 items-center"> <div className="flex ml-2 flex-wrap gap-2 items-center">
{Array.from(tags).map((tag, index) => { {Array.from(tags).map((tag, index) => {
return ( return (
<Tag <Tag
handleDelete={handleDelete} handleDelete={handleDelete}
active={active} active={active}
key={index} key={index}
> >
{tag} {tag}
</Tag> </Tag>
); );
})} })}
</div> </div>
); );
}; };

View File

@ -1,29 +1,29 @@
import React from "react"; import React from "react";
interface ErrorBannerProps { interface ErrorBannerProps {
heading: string; heading: string;
description?: string | null; description?: string | null;
} }
const ErrorBanner: React.FC<ErrorBannerProps> = ({ const ErrorBanner: React.FC<ErrorBannerProps> = ({
heading, heading,
description = null, description = null,
}) => { }) => {
return ( return (
<div <div
role="alert" role="alert"
className="rounded border-s-4 border-red-500 bg-red-50 p-4" className="rounded border-s-4 border-red-500 bg-red-50 p-4"
> >
<strong className="block text-sm font-semibold text-red-800"> <strong className="block text-sm font-semibold text-red-800">
{heading} {heading}
</strong> </strong>
{description && ( {description && (
<p className="mt-2 text-xs font-thin text-red-700"> <p className="mt-2 text-xs font-thin text-red-700">
{description} {description}
</p> </p>
)} )}
</div> </div>
); );
}; };
export default ErrorBanner; export default ErrorBanner;

View File

@ -1,43 +1,43 @@
/* components/Loading.module.css */ /* components/Loading.module.css */
.loadingOverlay { .loadingOverlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.8);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9999; z-index: 9999;
} }
.loadingContent { .loadingContent {
text-align: center; text-align: center;
} }
.loadingTitle { .loadingTitle {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: #5b21b6; color: #5b21b6;
margin-top: 1rem; margin-top: 1rem;
} }
.loadingSpinner { .loadingSpinner {
width: 50px; width: 50px;
height: 50px; height: 50px;
border: 4px solid #5b21b6; border: 4px solid #5b21b6;
border-top: 4px solid #fff; border-top: 4px solid #fff;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 2rem auto; margin: 2rem auto;
} }
@keyframes spin { @keyframes spin {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }

View File

@ -1,22 +1,22 @@
// components/Loading.js // components/Loading.js
import styles from "./Loading.module.css"; import styles from "./Loading.module.css";
import Image from "next/image"; import Image from "next/image";
const Loading = () => { const Loading = () => {
return ( return (
<div className={styles.loadingOverlay}> <div className={styles.loadingOverlay}>
<div className={styles.loadingContent}> <div className={styles.loadingContent}>
<Image <Image
src="/logo.png" src="/logo.png"
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
/> />
<h1 className={styles.loadingTitle}>Loading...</h1> <h1 className={styles.loadingTitle}>Loading...</h1>
<div className={styles.loadingSpinner}></div> <div className={styles.loadingSpinner}></div>
</div> </div>
</div> </div>
); );
}; };
export default Loading; export default Loading;

Some files were not shown because too many files have changed in this diff Show More