mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-09 22:00:18 -04:00
initial commit
This commit is contained in:
parent
f9abc9169f
commit
5c604e2a5a
|
@ -1,4 +1,4 @@
|
|||
cd compass
|
||||
npm run lint
|
||||
npm run prettier
|
||||
cd compass
|
||||
npm run lint
|
||||
npm run prettier
|
||||
git add .
|
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"backend"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"backend"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
|
@ -1 +1 @@
|
|||
"""Expose API routes via FastAPI routers from this package."""
|
||||
"""Expose API routes via FastAPI routers from this package."""
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
"""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.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services.health import HealthService
|
||||
|
||||
openapi_tags = {
|
||||
"name": "System Health",
|
||||
"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.get("", tags=["System Health"])
|
||||
def health_check(health_svc: HealthService = Depends()) -> str:
|
||||
return health_svc.check()
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services.health import HealthService
|
||||
|
||||
openapi_tags = {
|
||||
"name": "System Health",
|
||||
"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.get("", tags=["System Health"])
|
||||
def health_check(health_svc: HealthService = Depends()) -> str:
|
||||
return health_svc.check()
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from ..services import ResourceService, UserService
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/resource")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Resource",
|
||||
"description": "Resource search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Resource], tags=["Resource"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
resource_svc: ResourceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return resource_svc.get_resource_by_user(subject)
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services import ResourceService, UserService
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/resource")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Resource",
|
||||
"description": "Resource search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Resource], tags=["Resource"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
resource_svc: ResourceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return resource_svc.get_resource_by_user(subject)
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from ..services import ServiceService, UserService
|
||||
from ..models.service_model import Service
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/service")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Service",
|
||||
"description": "Service search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Service], tags=["Service"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
service_svc: ServiceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return service_svc.get_service_by_user(subject)
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services import ServiceService, UserService
|
||||
from ..models.service_model import Service
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/service")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Service",
|
||||
"description": "Service search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Service], tags=["Service"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
service_svc: ServiceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return service_svc.get_service_by_user(subject)
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from ..services import UserService
|
||||
from ..models.user_model import User, UserTypeEnum
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/user")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Users",
|
||||
"description": "User profile search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("/all", response_model=List[User], tags=["Users"])
|
||||
def get_all(user_id: str, user_svc: UserService = Depends()):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
||||
|
||||
return user_svc.all()
|
||||
|
||||
|
||||
@api.get("/{user_id}", response_model=User, tags=["Users"])
|
||||
def get_by_uuid(user_id: str, user_svc: UserService = Depends()):
|
||||
return user_svc.get_user_by_uuid(user_id)
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services import UserService
|
||||
from ..models.user_model import User, UserTypeEnum
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/user")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Users",
|
||||
"description": "User profile search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("/all", response_model=List[User], tags=["Users"])
|
||||
def get_all(user_id: str, user_svc: UserService = Depends()):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
||||
|
||||
return user_svc.all()
|
||||
|
||||
|
||||
@api.get("/{user_id}", response_model=User, tags=["Users"])
|
||||
def get_by_uuid(user_id: str, user_svc: UserService = Depends()):
|
||||
return user_svc.get_user_by_uuid(user_id)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Program_Enum(Enum):
|
||||
ECONOMIC = "ECONOMIC"
|
||||
DOMESTIC = "DOMESTIC"
|
||||
COMMUNITY = "COMMUNITY"
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Program_Enum(Enum):
|
||||
ECONOMIC = "ECONOMIC"
|
||||
DOMESTIC = "DOMESTIC"
|
||||
COMMUNITY = "COMMUNITY"
|
||||
|
|
|
@ -1,67 +1,67 @@
|
|||
""" Defines the table for storing resources """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
from backend.entities.program_enum import Program_Enum
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
|
||||
class ResourceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "resource"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
link: Mapped[str] = mapped_column(String, nullable=False)
|
||||
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
|
||||
# relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
|
||||
back_populates="resource", cascade="all,delete"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Resource) -> Self:
|
||||
"""
|
||||
Create a UserEntity from a User model.
|
||||
|
||||
Args:
|
||||
model (User): The model to create the entity from.
|
||||
|
||||
Returns:
|
||||
Self: The entity (not yet persisted).
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
name=model.name,
|
||||
summary=model.summary,
|
||||
link=model.link,
|
||||
program=model.program,
|
||||
)
|
||||
|
||||
def to_model(self) -> Resource:
|
||||
return Resource(
|
||||
id=self.id,
|
||||
created_at=self.created_at,
|
||||
name=self.name,
|
||||
summary=self.summary,
|
||||
link=self.link,
|
||||
program=self.program,
|
||||
)
|
||||
""" Defines the table for storing resources """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
from backend.entities.program_enum import Program_Enum
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
|
||||
class ResourceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "resource"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
link: Mapped[str] = mapped_column(String, nullable=False)
|
||||
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
|
||||
# relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
|
||||
back_populates="resource", cascade="all,delete"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Resource) -> Self:
|
||||
"""
|
||||
Create a UserEntity from a User model.
|
||||
|
||||
Args:
|
||||
model (User): The model to create the entity from.
|
||||
|
||||
Returns:
|
||||
Self: The entity (not yet persisted).
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
name=model.name,
|
||||
summary=model.summary,
|
||||
link=model.link,
|
||||
program=model.program,
|
||||
)
|
||||
|
||||
def to_model(self) -> Resource:
|
||||
return Resource(
|
||||
id=self.id,
|
||||
created_at=self.created_at,
|
||||
name=self.name,
|
||||
summary=self.summary,
|
||||
link=self.link,
|
||||
program=self.program,
|
||||
)
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
""" Defines the table for resource tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
|
||||
|
||||
class ResourceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "resource_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
|
||||
|
||||
# @classmethod
|
||||
# def from_model (cls, model: resource_tag_model) -> Self:
|
||||
# return cls (
|
||||
# id = model.id,
|
||||
# resourceId = model.resourceId,
|
||||
# tagId = model.tagId,
|
||||
# )
|
||||
|
||||
# def to_model (self) -> resource_tag_model:
|
||||
# return user_model(
|
||||
# id = self.id,
|
||||
# resourceId = self.resourceId,
|
||||
# tagId = self.tagId,
|
||||
# )
|
||||
""" Defines the table for resource tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
|
||||
|
||||
class ResourceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "resource_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
|
||||
|
||||
# @classmethod
|
||||
# def from_model (cls, model: resource_tag_model) -> Self:
|
||||
# return cls (
|
||||
# id = model.id,
|
||||
# resourceId = model.resourceId,
|
||||
# tagId = model.tagId,
|
||||
# )
|
||||
|
||||
# def to_model (self) -> resource_tag_model:
|
||||
# return user_model(
|
||||
# id = self.id,
|
||||
# resourceId = self.resourceId,
|
||||
# tagId = self.tagId,
|
||||
# )
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from sqlalchemy import create_engine, Column, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class SampleEntity(EntityBase):
|
||||
__tablename__ = "persons"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
age: Mapped[int] = mapped_column(Integer)
|
||||
from sqlalchemy import create_engine, Column, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class SampleEntity(EntityBase):
|
||||
__tablename__ = "persons"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
age: Mapped[int] = mapped_column(Integer)
|
||||
email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
|
@ -1,47 +1,47 @@
|
|||
""" Defines the table for storing services """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Program
|
||||
import enum
|
||||
from sqlalchemy import Enum
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from typing import Self
|
||||
from backend.models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
class ServiceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "service"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: 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)
|
||||
requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
|
||||
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
|
||||
|
||||
# relationships
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
|
||||
back_populates="service", cascade="all,delete"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model:Service) -> Self:
|
||||
""" Defines the table for storing services """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Program
|
||||
import enum
|
||||
from sqlalchemy import Enum
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from typing import Self
|
||||
from backend.models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
class ServiceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "service"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: 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)
|
||||
requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
|
||||
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
|
||||
|
||||
# relationships
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
|
||||
back_populates="service", cascade="all,delete"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
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)
|
|
@ -1,25 +1,25 @@
|
|||
""" Defines the table for service tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class ServiceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "service_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")
|
||||
""" Defines the table for service tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class ServiceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "service_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
""" Defines the table for storing tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.tag_model import Tag
|
||||
|
||||
from typing import Self
|
||||
|
||||
class TagEntity(EntityBase):
|
||||
|
||||
#set table name
|
||||
__tablename__ = "tag"
|
||||
|
||||
#set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
content: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
#relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Tag) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
content=model.id,
|
||||
)
|
||||
|
||||
def to_model(self) -> Tag:
|
||||
"""
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
"""
|
||||
|
||||
return Tag(
|
||||
id=self.id,
|
||||
content=self.content,
|
||||
)
|
||||
|
||||
|
||||
|
||||
""" Defines the table for storing tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.tag_model import Tag
|
||||
|
||||
from typing import Self
|
||||
|
||||
class TagEntity(EntityBase):
|
||||
|
||||
#set table name
|
||||
__tablename__ = "tag"
|
||||
|
||||
#set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
content: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
#relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Tag) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
content=model.id,
|
||||
)
|
||||
|
||||
def to_model(self) -> Tag:
|
||||
"""
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
"""
|
||||
|
||||
return Tag(
|
||||
id=self.id,
|
||||
content=self.content,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,88 +1,88 @@
|
|||
""" Defines the table for storing users """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Role and Program
|
||||
from backend.models.enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
|
||||
# Import models for User methods
|
||||
from ..models.user_model import User
|
||||
|
||||
from typing import Self
|
||||
|
||||
|
||||
class UserEntity(EntityBase):
|
||||
"""Serves as the database model for User table"""
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "user"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default="", unique=True
|
||||
)
|
||||
role: Mapped[UserTypeEnum] = mapped_column(Enum(UserTypeEnum), nullable=False)
|
||||
email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||
program: Mapped[list[ProgramTypeEnum]] = mapped_column(
|
||||
ARRAY(Enum(ProgramTypeEnum)), nullable=False
|
||||
)
|
||||
experience: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
group: Mapped[str] = mapped_column(String(50))
|
||||
uuid: Mapped[str] = mapped_column(String, nullable=True)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: User) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
username=model.username,
|
||||
role=model.role,
|
||||
email=model.email,
|
||||
program=model.program,
|
||||
experience=model.experience,
|
||||
group=model.group,
|
||||
uuid=model.uuid,
|
||||
)
|
||||
|
||||
def to_model(self) -> User:
|
||||
"""
|
||||
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
|
||||
"""
|
||||
|
||||
return User(
|
||||
id=self.id,
|
||||
username=self.username,
|
||||
email=self.email,
|
||||
experience=self.experience,
|
||||
group=self.group,
|
||||
program=self.program,
|
||||
role=self.role,
|
||||
created_at=self.created_at,
|
||||
uuid=self.uuid,
|
||||
)
|
||||
""" Defines the table for storing users """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Role and Program
|
||||
from backend.models.enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
|
||||
# Import models for User methods
|
||||
from ..models.user_model import User
|
||||
|
||||
from typing import Self
|
||||
|
||||
|
||||
class UserEntity(EntityBase):
|
||||
"""Serves as the database model for User table"""
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "user"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default="", unique=True
|
||||
)
|
||||
role: Mapped[UserTypeEnum] = mapped_column(Enum(UserTypeEnum), nullable=False)
|
||||
email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||
program: Mapped[list[ProgramTypeEnum]] = mapped_column(
|
||||
ARRAY(Enum(ProgramTypeEnum)), nullable=False
|
||||
)
|
||||
experience: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
group: Mapped[str] = mapped_column(String(50))
|
||||
uuid: Mapped[str] = mapped_column(String, nullable=True)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: User) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
username=model.username,
|
||||
role=model.role,
|
||||
email=model.email,
|
||||
program=model.program,
|
||||
experience=model.experience,
|
||||
group=model.group,
|
||||
uuid=model.uuid,
|
||||
)
|
||||
|
||||
def to_model(self) -> User:
|
||||
"""
|
||||
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
|
||||
"""
|
||||
|
||||
return User(
|
||||
id=self.id,
|
||||
username=self.username,
|
||||
email=self.email,
|
||||
experience=self.experience,
|
||||
group=self.group,
|
||||
program=self.program,
|
||||
role=self.role,
|
||||
created_at=self.created_at,
|
||||
uuid=self.uuid,
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Role_Enum(Enum):
|
||||
"""Determine role for User"""
|
||||
|
||||
ADMIN = "ADMIN"
|
||||
EMPLOYEE = "EMPLOYEE"
|
||||
VOLUNTEER = "VOLUNTEER"
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Role_Enum(Enum):
|
||||
"""Determine role for User"""
|
||||
|
||||
ADMIN = "ADMIN"
|
||||
EMPLOYEE = "EMPLOYEE"
|
||||
VOLUNTEER = "VOLUNTEER"
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from .api import user, health, service, resource
|
||||
|
||||
description = """
|
||||
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
||||
"""
|
||||
|
||||
app = FastAPI(
|
||||
title="Compass API",
|
||||
version="0.0.1",
|
||||
description=description,
|
||||
openapi_tags=[
|
||||
user.openapi_tags,
|
||||
health.openapi_tags,
|
||||
service.openapi_tags,
|
||||
resource.openapi_tags,
|
||||
],
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware)
|
||||
|
||||
feature_apis = [user, health, service, resource]
|
||||
|
||||
for feature_api in feature_apis:
|
||||
app.include_router(feature_api.api)
|
||||
|
||||
|
||||
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
||||
@app.exception_handler(Exception)
|
||||
def permission_exception_handler(request: Request, e: Exception):
|
||||
return JSONResponse(status_code=403, content={"message": str(e)})
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from .api import user, health, service, resource
|
||||
|
||||
description = """
|
||||
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
||||
"""
|
||||
|
||||
app = FastAPI(
|
||||
title="Compass API",
|
||||
version="0.0.1",
|
||||
description=description,
|
||||
openapi_tags=[
|
||||
user.openapi_tags,
|
||||
health.openapi_tags,
|
||||
service.openapi_tags,
|
||||
resource.openapi_tags,
|
||||
],
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware)
|
||||
|
||||
feature_apis = [user, health, service, resource]
|
||||
|
||||
for feature_api in feature_apis:
|
||||
app.include_router(feature_api.api)
|
||||
|
||||
|
||||
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
||||
@app.exception_handler(Exception)
|
||||
def permission_exception_handler(request: Request, e: Exception):
|
||||
return JSONResponse(status_code=403, content={"message": str(e)})
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class ProgramTypeEnum(str, Enum):
|
||||
DOMESTIC = "DOMESTIC"
|
||||
ECONOMIC = "ECONOMIC"
|
||||
COMMUNITY = "COMMUNITY"
|
||||
|
||||
|
||||
class UserTypeEnum(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
EMPLOYEE = "EMPLOYEE"
|
||||
VOLUNTEER = "VOLUNTEER"
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ProgramTypeEnum(str, Enum):
|
||||
DOMESTIC = "DOMESTIC"
|
||||
ECONOMIC = "ECONOMIC"
|
||||
COMMUNITY = "COMMUNITY"
|
||||
|
||||
|
||||
class UserTypeEnum(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
EMPLOYEE = "EMPLOYEE"
|
||||
VOLUNTEER = "VOLUNTEER"
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
id: int | None = None
|
||||
name: str = Field(..., max_length=150, description="The name 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")
|
||||
program: ProgramTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
id: int | None = None
|
||||
name: str = Field(..., max_length=150, description="The name 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")
|
||||
program: ProgramTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .tag_model import Tag
|
||||
from .resource_model import Resource
|
||||
|
||||
|
||||
class ResourceTag(Resource, BaseModel):
|
||||
id: int | None = None
|
||||
resourceid: int | None = None
|
||||
tagid: List[Tag]
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .tag_model import Tag
|
||||
from .resource_model import Resource
|
||||
|
||||
|
||||
class ResourceTag(Resource, BaseModel):
|
||||
id: int | None = None
|
||||
resourceid: int | None = None
|
||||
tagid: List[Tag]
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
|
||||
|
||||
class Service(BaseModel):
|
||||
id: int | None = None
|
||||
created_at: datetime | None = None
|
||||
name: str
|
||||
status: str
|
||||
summary: str
|
||||
requirements: List[str]
|
||||
program: ProgramTypeEnum
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
|
||||
|
||||
class Service(BaseModel):
|
||||
id: int | None = None
|
||||
created_at: datetime | None = None
|
||||
name: str
|
||||
status: str
|
||||
summary: str
|
||||
requirements: List[str]
|
||||
program: ProgramTypeEnum
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
from .enum_for_models import UserTypeEnum
|
||||
from .service_model import Service
|
||||
|
||||
from .tag_model import Tag
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ServiceTag(Service, BaseModel):
|
||||
id: int | None = None
|
||||
serviceid: int | None = None
|
||||
tagId: List[Tag]
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .enum_for_models import ProgramTypeEnum
|
||||
from .enum_for_models import UserTypeEnum
|
||||
from .service_model import Service
|
||||
|
||||
from .tag_model import Tag
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ServiceTag(Service, BaseModel):
|
||||
id: int | None = None
|
||||
serviceid: int | None = None
|
||||
tagId: List[Tag]
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
id: int | None = None
|
||||
content: str = Field(
|
||||
..., max_length=600, description="content associated with the tag"
|
||||
)
|
||||
created_at: datetime | None = None
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
id: int | None = None
|
||||
content: str = Field(
|
||||
..., max_length=600, description="content associated with the tag"
|
||||
)
|
||||
created_at: datetime | None = None
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: int | None = None
|
||||
username: str = Field(..., description="The username of the user")
|
||||
email: str = Field(..., description="The e-mail of the user")
|
||||
experience: int = Field(..., description="Years of Experience of the User")
|
||||
group: str
|
||||
program: List[ProgramTypeEnum]
|
||||
role: UserTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
uuid: str | None = None
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from .enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: int | None = None
|
||||
username: str = Field(..., description="The username of the user")
|
||||
email: str = Field(..., description="The e-mail of the user")
|
||||
experience: int = Field(..., description="Years of Experience of the User")
|
||||
group: str
|
||||
program: List[ProgramTypeEnum]
|
||||
role: UserTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
uuid: str | None = None
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .user import UserService
|
||||
from .resource import ResourceService
|
||||
from .tag import TagService
|
||||
from .user import UserService
|
||||
from .resource import ResourceService
|
||||
from .tag import TagService
|
||||
from .service import ServiceService
|
|
@ -1,25 +1,25 @@
|
|||
"""
|
||||
This file contains exceptions found in the service layer.
|
||||
|
||||
These custom exceptions can then be handled peoperly
|
||||
at the API level.
|
||||
"""
|
||||
|
||||
|
||||
class ResourceNotFoundException(Exception):
|
||||
"""ResourceNotFoundException is raised when a user attempts to access a resource that does not exist."""
|
||||
|
||||
|
||||
class UserPermissionException(Exception):
|
||||
"""UserPermissionException is raised when a user attempts to perform an action they are not authorized to perform."""
|
||||
|
||||
def __init__(self, action: str, resource: str):
|
||||
super().__init__(f"Not authorized to perform `{action}` on `{resource}`")
|
||||
|
||||
|
||||
class ServiceNotFoundException(Exception):
|
||||
"""Exception for when the service being requested is not in the table."""
|
||||
|
||||
|
||||
class ProgramNotAssignedException(Exception):
|
||||
"""Exception for when the user does not have correct access for requested services."""
|
||||
"""
|
||||
This file contains exceptions found in the service layer.
|
||||
|
||||
These custom exceptions can then be handled peoperly
|
||||
at the API level.
|
||||
"""
|
||||
|
||||
|
||||
class ResourceNotFoundException(Exception):
|
||||
"""ResourceNotFoundException is raised when a user attempts to access a resource that does not exist."""
|
||||
|
||||
|
||||
class UserPermissionException(Exception):
|
||||
"""UserPermissionException is raised when a user attempts to perform an action they are not authorized to perform."""
|
||||
|
||||
def __init__(self, action: str, resource: str):
|
||||
super().__init__(f"Not authorized to perform `{action}` on `{resource}`")
|
||||
|
||||
|
||||
class ServiceNotFoundException(Exception):
|
||||
"""Exception for when the service being requested is not in the table."""
|
||||
|
||||
|
||||
class ProgramNotAssignedException(Exception):
|
||||
"""Exception for when the user does not have correct access for requested services."""
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
"""
|
||||
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 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
|
||||
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.
|
||||
"""
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import text
|
||||
from ..database import Session, db_session
|
||||
|
||||
|
||||
class HealthService:
|
||||
_session: Session
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def check(self):
|
||||
stmt = text("SELECT 'OK', NOW()")
|
||||
result = self._session.execute(stmt)
|
||||
row = result.all()[0]
|
||||
return str(f"{row[0]} @ {row[1]}")
|
||||
"""
|
||||
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 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
|
||||
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.
|
||||
"""
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import text
|
||||
from ..database import Session, db_session
|
||||
|
||||
|
||||
class HealthService:
|
||||
_session: Session
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def check(self):
|
||||
stmt = text("SELECT 'OK', NOW()")
|
||||
result = self._session.execute(stmt)
|
||||
row = result.all()[0]
|
||||
return str(f"{row[0]} @ {row[1]}")
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models.user_model import User
|
||||
from ..entities.user_entity import UserEntity
|
||||
from exceptions import ResourceNotFoundException, UserPermissionException
|
||||
from ..models.enum_for_models import UserTypeEnum
|
||||
|
||||
|
||||
class PermissionsService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_role_permissions(self, user: User) -> str:
|
||||
"""
|
||||
Gets a str group based on the user
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
|
||||
# Query the resource table with id
|
||||
obj = (
|
||||
self._session.query(UserEntity)
|
||||
.filter(UserEntity.id == user.id)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
# Check if result is null
|
||||
if obj is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No user permissions found for user with id: {user.id}"
|
||||
)
|
||||
|
||||
return obj.role
|
||||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models.user_model import User
|
||||
from ..entities.user_entity import UserEntity
|
||||
from exceptions import ResourceNotFoundException, UserPermissionException
|
||||
from ..models.enum_for_models import UserTypeEnum
|
||||
|
||||
|
||||
class PermissionsService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_role_permissions(self, user: User) -> str:
|
||||
"""
|
||||
Gets a str group based on the user
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
|
||||
# Query the resource table with id
|
||||
obj = (
|
||||
self._session.query(UserEntity)
|
||||
.filter(UserEntity.id == user.id)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
# Check if result is null
|
||||
if obj is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No user permissions found for user with id: {user.id}"
|
||||
)
|
||||
|
||||
return obj.role
|
||||
|
|
|
@ -1,165 +1,165 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from ..models.resource_model import Resource
|
||||
from ..entities.resource_entity import ResourceEntity
|
||||
from ..models.user_model import User, UserTypeEnum
|
||||
|
||||
from .exceptions import ResourceNotFoundException
|
||||
|
||||
|
||||
class ResourceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_resource_by_user(self, subject: User):
|
||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ResourceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [resource.to_model() for resource in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
resources = []
|
||||
for program in programs:
|
||||
query = select(ResourceEntity).filter(ResourceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
for entity in entities:
|
||||
resources.append(entity)
|
||||
|
||||
return [resource.to_model() for resource in resources]
|
||||
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
resource: Resource object to add to table
|
||||
|
||||
Returns:
|
||||
Resource: Object added to table
|
||||
"""
|
||||
if resource.role != user.role or resource.group != user.group:
|
||||
raise PermissionError(
|
||||
"User does not have permission to add resources in this role or group."
|
||||
)
|
||||
|
||||
resource_entity = ResourceEntity.from_model(resource)
|
||||
self._session.add(resource_entity)
|
||||
self._session.commit()
|
||||
|
||||
return resource_entity.to_model()
|
||||
|
||||
def get_by_id(self, user: User, id: int) -> Resource:
|
||||
"""
|
||||
Gets a resource based on the resource id that the user has access to
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
id: int, the id of the resource
|
||||
|
||||
Returns:
|
||||
Resource
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with id
|
||||
"""
|
||||
resource = (
|
||||
self._session.query(ResourceEntity)
|
||||
.filter(
|
||||
ResourceEntity.id == id,
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if resource is None:
|
||||
raise ResourceNotFoundException(f"No resource found with id: {id}")
|
||||
|
||||
return resource.to_model()
|
||||
|
||||
def update(self, user: User, resource: ResourceEntity) -> Resource:
|
||||
"""
|
||||
Update the resource if the user has access
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
resource (ResourceEntity): Resource to update
|
||||
|
||||
Returns:
|
||||
Resource: Updated resource object
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding ID
|
||||
"""
|
||||
if resource.role != user.role or resource.group != user.group:
|
||||
raise PermissionError(
|
||||
"User does not have permission to update this resource."
|
||||
)
|
||||
|
||||
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
|
||||
|
||||
if obj is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No resource found with matching id: {resource.id}"
|
||||
)
|
||||
|
||||
obj.update_from_model(resource) # Assuming an update method exists
|
||||
self._session.commit()
|
||||
|
||||
return obj.to_model()
|
||||
|
||||
def delete(self, user: User, id: int) -> None:
|
||||
"""
|
||||
Delete resource based on id that the user has access to
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
id: int, a unique resource id
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding id
|
||||
"""
|
||||
resource = (
|
||||
self._session.query(ResourceEntity)
|
||||
.filter(
|
||||
ResourceEntity.id == id,
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if resource is None:
|
||||
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
|
||||
|
||||
self._session.delete(resource)
|
||||
self._session.commit()
|
||||
|
||||
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
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
search_string: a string to search resources by
|
||||
|
||||
Returns:
|
||||
list[Resource]: list of resources relating to the string
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException if no resource is found with the corresponding slug
|
||||
"""
|
||||
query = select(ResourceEntity).where(
|
||||
ResourceEntity.title.ilike(f"%{search_string}%"),
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from ..models.resource_model import Resource
|
||||
from ..entities.resource_entity import ResourceEntity
|
||||
from ..models.user_model import User, UserTypeEnum
|
||||
|
||||
from .exceptions import ResourceNotFoundException
|
||||
|
||||
|
||||
class ResourceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_resource_by_user(self, subject: User):
|
||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ResourceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [resource.to_model() for resource in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
resources = []
|
||||
for program in programs:
|
||||
query = select(ResourceEntity).filter(ResourceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
for entity in entities:
|
||||
resources.append(entity)
|
||||
|
||||
return [resource.to_model() for resource in resources]
|
||||
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
resource: Resource object to add to table
|
||||
|
||||
Returns:
|
||||
Resource: Object added to table
|
||||
"""
|
||||
if resource.role != user.role or resource.group != user.group:
|
||||
raise PermissionError(
|
||||
"User does not have permission to add resources in this role or group."
|
||||
)
|
||||
|
||||
resource_entity = ResourceEntity.from_model(resource)
|
||||
self._session.add(resource_entity)
|
||||
self._session.commit()
|
||||
|
||||
return resource_entity.to_model()
|
||||
|
||||
def get_by_id(self, user: User, id: int) -> Resource:
|
||||
"""
|
||||
Gets a resource based on the resource id that the user has access to
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
id: int, the id of the resource
|
||||
|
||||
Returns:
|
||||
Resource
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with id
|
||||
"""
|
||||
resource = (
|
||||
self._session.query(ResourceEntity)
|
||||
.filter(
|
||||
ResourceEntity.id == id,
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if resource is None:
|
||||
raise ResourceNotFoundException(f"No resource found with id: {id}")
|
||||
|
||||
return resource.to_model()
|
||||
|
||||
def update(self, user: User, resource: ResourceEntity) -> Resource:
|
||||
"""
|
||||
Update the resource if the user has access
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
resource (ResourceEntity): Resource to update
|
||||
|
||||
Returns:
|
||||
Resource: Updated resource object
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding ID
|
||||
"""
|
||||
if resource.role != user.role or resource.group != user.group:
|
||||
raise PermissionError(
|
||||
"User does not have permission to update this resource."
|
||||
)
|
||||
|
||||
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
|
||||
|
||||
if obj is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No resource found with matching id: {resource.id}"
|
||||
)
|
||||
|
||||
obj.update_from_model(resource) # Assuming an update method exists
|
||||
self._session.commit()
|
||||
|
||||
return obj.to_model()
|
||||
|
||||
def delete(self, user: User, id: int) -> None:
|
||||
"""
|
||||
Delete resource based on id that the user has access to
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
id: int, a unique resource id
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding id
|
||||
"""
|
||||
resource = (
|
||||
self._session.query(ResourceEntity)
|
||||
.filter(
|
||||
ResourceEntity.id == id,
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if resource is None:
|
||||
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
|
||||
|
||||
self._session.delete(resource)
|
||||
self._session.commit()
|
||||
|
||||
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
|
||||
|
||||
Parameters:
|
||||
user: a valid User model representing the currently logged in User
|
||||
search_string: a string to search resources by
|
||||
|
||||
Returns:
|
||||
list[Resource]: list of resources relating to the string
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException if no resource is found with the corresponding slug
|
||||
"""
|
||||
query = select(ResourceEntity).where(
|
||||
ResourceEntity.title.ilike(f"%{search_string}%"),
|
||||
ResourceEntity.role == user.role,
|
||||
ResourceEntity.group == user.group,
|
||||
)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
|
|
@ -1,127 +1,127 @@
|
|||
from fastapi import Depends
|
||||
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select, and_, func, or_, exists, or_
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
|
||||
from backend.services.exceptions import (
|
||||
ServiceNotFoundException,
|
||||
ProgramNotAssignedException,
|
||||
)
|
||||
|
||||
|
||||
class ServiceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
|
||||
"""Service method getting services belonging to a particular program."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||
entities = self._session.scalars(query)
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
||||
def get_service_by_id(self, id: int) -> Service:
|
||||
"""Service method getting services by id."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.id == id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(f"Service with id: {id} does not exist")
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def get_service_by_name(self, name: str) -> Service:
|
||||
"""Service method getting services by id."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.name == name)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(f"Service with name: {name} does not exist")
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def get_service_by_user(self, subject: User):
|
||||
"""Service method getting all of the services that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
services = []
|
||||
for program in programs:
|
||||
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
for entity in entities:
|
||||
services.append(entity)
|
||||
|
||||
return [service.to_model() for service in services]
|
||||
|
||||
def get_all(self, subject: User) -> list[Service]:
|
||||
"""Service method retrieving all of the services in the table."""
|
||||
if subject.role == UserTypeEnum.VOLUNTEER:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
|
||||
)
|
||||
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
|
||||
def create(self, subject: User, service: Service) -> Service:
|
||||
"""Creates/adds a service to the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot create service"
|
||||
)
|
||||
|
||||
service_entity = ServiceEntity.from_model(service)
|
||||
self._session.add(service_entity)
|
||||
self._session.commit()
|
||||
return service_entity.to_model()
|
||||
|
||||
def update(self, subject: User, service: Service) -> Service:
|
||||
"""Updates a service if in the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
service_entity.name = service.name
|
||||
service_entity.status = service.status
|
||||
service_entity.summary = service.summary
|
||||
service_entity.requirements = service.requirements
|
||||
service_entity.program = service.program
|
||||
|
||||
self._session.commit()
|
||||
|
||||
return service_entity.to_model()
|
||||
|
||||
def delete(self, subject: User, service: Service) -> None:
|
||||
"""Deletes a service from the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
self._session.delete(service_entity)
|
||||
self._session.commit()
|
||||
from fastapi import Depends
|
||||
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select, and_, func, or_, exists, or_
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
|
||||
from backend.services.exceptions import (
|
||||
ServiceNotFoundException,
|
||||
ProgramNotAssignedException,
|
||||
)
|
||||
|
||||
|
||||
class ServiceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
|
||||
"""Service method getting services belonging to a particular program."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||
entities = self._session.scalars(query)
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
||||
def get_service_by_id(self, id: int) -> Service:
|
||||
"""Service method getting services by id."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.id == id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(f"Service with id: {id} does not exist")
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def get_service_by_name(self, name: str) -> Service:
|
||||
"""Service method getting services by id."""
|
||||
query = select(ServiceEntity).filter(ServiceEntity.name == name)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(f"Service with name: {name} does not exist")
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def get_service_by_user(self, subject: User):
|
||||
"""Service method getting all of the services that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
services = []
|
||||
for program in programs:
|
||||
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
for entity in entities:
|
||||
services.append(entity)
|
||||
|
||||
return [service.to_model() for service in services]
|
||||
|
||||
def get_all(self, subject: User) -> list[Service]:
|
||||
"""Service method retrieving all of the services in the table."""
|
||||
if subject.role == UserTypeEnum.VOLUNTEER:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
|
||||
)
|
||||
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
|
||||
def create(self, subject: User, service: Service) -> Service:
|
||||
"""Creates/adds a service to the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot create service"
|
||||
)
|
||||
|
||||
service_entity = ServiceEntity.from_model(service)
|
||||
self._session.add(service_entity)
|
||||
self._session.commit()
|
||||
return service_entity.to_model()
|
||||
|
||||
def update(self, subject: User, service: Service) -> Service:
|
||||
"""Updates a service if in the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
service_entity.name = service.name
|
||||
service_entity.status = service.status
|
||||
service_entity.summary = service.summary
|
||||
service_entity.requirements = service.requirements
|
||||
service_entity.program = service.program
|
||||
|
||||
self._session.commit()
|
||||
|
||||
return service_entity.to_model()
|
||||
|
||||
def delete(self, subject: User, service: Service) -> None:
|
||||
"""Deletes a service from the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
self._session.delete(service_entity)
|
||||
self._session.commit()
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.tag_model import Tag
|
||||
from ..entities.tag_entity import TagEntity
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class TagService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def all(self) -> list[Tag]:
|
||||
"""Returns a list of all Tags"""
|
||||
|
||||
query = select(TagEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.tag_model import Tag
|
||||
from ..entities.tag_entity import TagEntity
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class TagService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def all(self) -> list[Tag]:
|
||||
"""Returns a list of all Tags"""
|
||||
|
||||
query = select(TagEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
|
|
@ -1,119 +1,119 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from ..entities.user_entity import UserEntity
|
||||
from ..models.user_model import User
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class UserService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_user_by_id(self, id: int) -> User:
|
||||
"""
|
||||
Gets a user by id from the database
|
||||
|
||||
Returns: A User Pydantic model
|
||||
|
||||
"""
|
||||
query = select(UserEntity).where(UserEntity.id == id)
|
||||
user_entity: UserEntity | None = self._session.scalar(query)
|
||||
|
||||
if user_entity is None:
|
||||
raise Exception(f"No user found with matching id: {id}")
|
||||
|
||||
return user_entity.to_model()
|
||||
|
||||
def get_user_by_uuid(self, uuid: str) -> User:
|
||||
"""
|
||||
Gets a user by uuid from the database
|
||||
|
||||
Returns: A User Pydantic model
|
||||
|
||||
"""
|
||||
query = select(UserEntity).where(UserEntity.uuid == uuid)
|
||||
user_entity: UserEntity | None = self._session.scalar(query)
|
||||
|
||||
if user_entity is None:
|
||||
raise Exception(f"No user found with matching uuid: {uuid}")
|
||||
|
||||
return user_entity.to_model()
|
||||
|
||||
def all(self) -> list[User]:
|
||||
"""
|
||||
Returns a list of all Users
|
||||
|
||||
"""
|
||||
query = select(UserEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
||||
def create(self, user: User) -> User:
|
||||
"""
|
||||
Creates a new User Entity and adds to database
|
||||
|
||||
Args: User model
|
||||
|
||||
Returns: User model
|
||||
|
||||
"""
|
||||
try:
|
||||
if (user.id != None):
|
||||
user = self.get_user_by_id(user.id)
|
||||
except:
|
||||
# if does not exist, create new object
|
||||
user_entity = UserEntity.from_model(user)
|
||||
|
||||
# add new user to table
|
||||
self._session.add(user_entity)
|
||||
self._session.commit()
|
||||
finally:
|
||||
# return added object
|
||||
return user
|
||||
|
||||
def delete(self, user: User) -> None:
|
||||
"""
|
||||
Delete a user
|
||||
|
||||
Args: the user to delete
|
||||
|
||||
Returns: none
|
||||
"""
|
||||
obj = self._session.get(UserEntity, user.id)
|
||||
|
||||
if obj is None:
|
||||
raise Exception(f"No matching user found")
|
||||
|
||||
self._session.delete(obj)
|
||||
self._session.commit()
|
||||
|
||||
|
||||
|
||||
def update(self, user: User) -> User:
|
||||
"""
|
||||
Updates a user
|
||||
|
||||
Args: User to be updated
|
||||
|
||||
Returns: The updated User
|
||||
"""
|
||||
obj = self._session.get(UserEntity, user.id)
|
||||
|
||||
if obj is None:
|
||||
raise Exception(f"No matching user found")
|
||||
|
||||
obj.username = user.username
|
||||
obj.role = user.role
|
||||
obj.email = user.email
|
||||
obj.program = user.program
|
||||
obj.experience = user.experience
|
||||
obj.group = user.group
|
||||
|
||||
self._session.commit()
|
||||
|
||||
return obj.to_model()
|
||||
|
||||
|
||||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from ..entities.user_entity import UserEntity
|
||||
from ..models.user_model import User
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class UserService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_user_by_id(self, id: int) -> User:
|
||||
"""
|
||||
Gets a user by id from the database
|
||||
|
||||
Returns: A User Pydantic model
|
||||
|
||||
"""
|
||||
query = select(UserEntity).where(UserEntity.id == id)
|
||||
user_entity: UserEntity | None = self._session.scalar(query)
|
||||
|
||||
if user_entity is None:
|
||||
raise Exception(f"No user found with matching id: {id}")
|
||||
|
||||
return user_entity.to_model()
|
||||
|
||||
def get_user_by_uuid(self, uuid: str) -> User:
|
||||
"""
|
||||
Gets a user by uuid from the database
|
||||
|
||||
Returns: A User Pydantic model
|
||||
|
||||
"""
|
||||
query = select(UserEntity).where(UserEntity.uuid == uuid)
|
||||
user_entity: UserEntity | None = self._session.scalar(query)
|
||||
|
||||
if user_entity is None:
|
||||
raise Exception(f"No user found with matching uuid: {uuid}")
|
||||
|
||||
return user_entity.to_model()
|
||||
|
||||
def all(self) -> list[User]:
|
||||
"""
|
||||
Returns a list of all Users
|
||||
|
||||
"""
|
||||
query = select(UserEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
||||
def create(self, user: User) -> User:
|
||||
"""
|
||||
Creates a new User Entity and adds to database
|
||||
|
||||
Args: User model
|
||||
|
||||
Returns: User model
|
||||
|
||||
"""
|
||||
try:
|
||||
if (user.id != None):
|
||||
user = self.get_user_by_id(user.id)
|
||||
except:
|
||||
# if does not exist, create new object
|
||||
user_entity = UserEntity.from_model(user)
|
||||
|
||||
# add new user to table
|
||||
self._session.add(user_entity)
|
||||
self._session.commit()
|
||||
finally:
|
||||
# return added object
|
||||
return user
|
||||
|
||||
def delete(self, user: User) -> None:
|
||||
"""
|
||||
Delete a user
|
||||
|
||||
Args: the user to delete
|
||||
|
||||
Returns: none
|
||||
"""
|
||||
obj = self._session.get(UserEntity, user.id)
|
||||
|
||||
if obj is None:
|
||||
raise Exception(f"No matching user found")
|
||||
|
||||
self._session.delete(obj)
|
||||
self._session.commit()
|
||||
|
||||
|
||||
|
||||
def update(self, user: User) -> User:
|
||||
"""
|
||||
Updates a user
|
||||
|
||||
Args: User to be updated
|
||||
|
||||
Returns: The updated User
|
||||
"""
|
||||
obj = self._session.get(UserEntity, user.id)
|
||||
|
||||
if obj is None:
|
||||
raise Exception(f"No matching user found")
|
||||
|
||||
obj.username = user.username
|
||||
obj.role = user.role
|
||||
obj.email = user.email
|
||||
obj.program = user.program
|
||||
obj.experience = user.experience
|
||||
obj.group = user.group
|
||||
|
||||
self._session.commit()
|
||||
|
||||
return obj.to_model()
|
||||
|
||||
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
# Testing
|
||||
|
||||
## Backend
|
||||
|
||||
### Organization
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Pytest CLI
|
||||
|
||||
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 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`
|
||||
|
||||
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 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.
|
||||
|
||||
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).
|
||||
|
||||
### Code Coverage
|
||||
|
||||
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:
|
||||
|
||||
`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.
|
||||
|
||||
## 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
|
||||
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)
|
||||
4. Run specific functions by running this command while in the /workspace directory
|
||||
- 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
|
||||
# Testing
|
||||
|
||||
## Backend
|
||||
|
||||
### Organization
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Pytest CLI
|
||||
|
||||
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 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`
|
||||
|
||||
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 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.
|
||||
|
||||
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).
|
||||
|
||||
### Code Coverage
|
||||
|
||||
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:
|
||||
|
||||
`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.
|
||||
|
||||
## 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
|
||||
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)
|
||||
4. Run specific functions by running this command while in the /workspace directory
|
||||
- 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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
"""Fixtures used for testing the core services."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import create_autospec
|
||||
from sqlalchemy.orm import Session
|
||||
from ...services import UserService
|
||||
from ...services import TagService
|
||||
from ...services import ServiceService
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_svc(session: Session):
|
||||
"""This fixture is used to test the UserService class"""
|
||||
return UserService(session)
|
||||
|
||||
@pytest.fixture()
|
||||
def tag_svc(session: Session):
|
||||
"""This fixture is used to test the TagService class"""
|
||||
return TagService(session)
|
||||
|
||||
@pytest.fixture()
|
||||
def service_svc(session: Session):
|
||||
"""This fixture is used to test the ServiceService class"""
|
||||
"""Fixtures used for testing the core services."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import create_autospec
|
||||
from sqlalchemy.orm import Session
|
||||
from ...services import UserService
|
||||
from ...services import TagService
|
||||
from ...services import ServiceService
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_svc(session: Session):
|
||||
"""This fixture is used to test the UserService class"""
|
||||
return UserService(session)
|
||||
|
||||
@pytest.fixture()
|
||||
def tag_svc(session: Session):
|
||||
"""This fixture is used to test the TagService class"""
|
||||
return TagService(session)
|
||||
|
||||
@pytest.fixture()
|
||||
def service_svc(session: Session):
|
||||
"""This fixture is used to test the ServiceService class"""
|
||||
return ServiceService(session)
|
|
@ -1,315 +1,315 @@
|
|||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from ...entities import ResourceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from ...models.resource_model import Resource
|
||||
|
||||
resource1 = Resource(
|
||||
id=1,
|
||||
name="Resource 1",
|
||||
summary="Helpful information for victims of domestic violence",
|
||||
link="https://example.com/resource1",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 1, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource2 = Resource(
|
||||
id=2,
|
||||
name="Resource 2",
|
||||
summary="Legal assistance resources",
|
||||
link="https://example.com/resource2",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 2, 12, 30, 0),
|
||||
)
|
||||
|
||||
resource3 = Resource(
|
||||
id=3,
|
||||
name="Resource 3",
|
||||
summary="Financial aid resources",
|
||||
link="https://example.com/resource3",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 3, 15, 45, 0),
|
||||
)
|
||||
|
||||
resource4 = Resource(
|
||||
id=4,
|
||||
name="Resource 4",
|
||||
summary="Counseling and support groups",
|
||||
link="https://example.com/resource4",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 4, 9, 15, 0),
|
||||
)
|
||||
|
||||
resource5 = Resource(
|
||||
id=5,
|
||||
name="Resource 5",
|
||||
summary="Shelter and housing resources",
|
||||
link="https://example.com/resource5",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||
)
|
||||
|
||||
resources = [resource1, resource2, resource3, resource4, resource5]
|
||||
|
||||
resource_1 = Resource(
|
||||
id=1,
|
||||
name="National Domestic Violence Hotline",
|
||||
summary="24/7 confidential support for victims of domestic violence",
|
||||
link="https://www.thehotline.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 1, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource_2 = Resource(
|
||||
id=2,
|
||||
name="Legal Aid Society",
|
||||
summary="Free legal assistance for low-income individuals",
|
||||
link="https://www.legalaidnyc.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 2, 12, 30, 0),
|
||||
)
|
||||
|
||||
resource_3 = Resource(
|
||||
id=3,
|
||||
name="Financial Empowerment Center",
|
||||
summary="Free financial counseling and education services",
|
||||
link="https://www1.nyc.gov/site/dca/consumers/get-free-financial-counseling.page",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 3, 15, 45, 0),
|
||||
)
|
||||
|
||||
resource_4 = Resource(
|
||||
id=4,
|
||||
name="National Coalition Against Domestic Violence",
|
||||
summary="Resources and support for victims of domestic violence",
|
||||
link="https://ncadv.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 4, 9, 15, 0),
|
||||
)
|
||||
|
||||
resource_5 = Resource(
|
||||
id=5,
|
||||
name="Safe Horizon",
|
||||
summary="Shelter and support services for victims of violence",
|
||||
link="https://www.safehorizon.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||
)
|
||||
|
||||
resource_6 = Resource(
|
||||
id=6,
|
||||
name="National Sexual Assault Hotline",
|
||||
summary="24/7 confidential support for survivors of sexual assault",
|
||||
link="https://www.rainn.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 6, 14, 0, 0),
|
||||
)
|
||||
|
||||
resource_7 = Resource(
|
||||
id=7,
|
||||
name="Victim Compensation Fund",
|
||||
summary="Financial assistance for victims of crime",
|
||||
link="https://ovc.ojp.gov/program/victim-compensation",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 7, 16, 45, 0),
|
||||
)
|
||||
|
||||
resource_8 = Resource(
|
||||
id=8,
|
||||
name="Battered Women's Justice Project",
|
||||
summary="Legal and technical assistance for victims of domestic violence",
|
||||
link="https://www.bwjp.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 8, 10, 30, 0),
|
||||
)
|
||||
|
||||
resource_9 = Resource(
|
||||
id=9,
|
||||
name="National Network to End Domestic Violence",
|
||||
summary="Advocacy and resources for ending domestic violence",
|
||||
link="https://nnedv.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 9, 13, 0, 0),
|
||||
)
|
||||
|
||||
resource_10 = Resource(
|
||||
id=10,
|
||||
name="Economic Justice Project",
|
||||
summary="Promoting economic security for survivors of domestic violence",
|
||||
link="https://www.njep.org",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 10, 15, 15, 0),
|
||||
)
|
||||
|
||||
resource_11 = Resource(
|
||||
id=11,
|
||||
name="Domestic Violence Legal Hotline",
|
||||
summary="Free legal advice for victims of domestic violence",
|
||||
link="https://www.womenslaw.org/find-help/national/hotlines",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 11, 9, 0, 0),
|
||||
)
|
||||
|
||||
resource_12 = Resource(
|
||||
id=12,
|
||||
name="National Resource Center on Domestic Violence",
|
||||
summary="Comprehensive information and resources on domestic violence",
|
||||
link="https://nrcdv.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 12, 11, 30, 0),
|
||||
)
|
||||
|
||||
resource_13 = Resource(
|
||||
id=13,
|
||||
name="Financial Assistance for Victims of Crime",
|
||||
summary="Funding for expenses related to victimization",
|
||||
link="https://ovc.ojp.gov/program/victim-assistance-funding",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 13, 14, 45, 0),
|
||||
)
|
||||
|
||||
resource_14 = Resource(
|
||||
id=14,
|
||||
name="National Clearinghouse for the Defense of Battered Women",
|
||||
summary="Legal resources and support for battered women",
|
||||
link="https://www.ncdbw.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 14, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource_15 = Resource(
|
||||
id=15,
|
||||
name="Victim Connect Resource Center",
|
||||
summary="Referral helpline for crime victims",
|
||||
link="https://victimconnect.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 15, 13, 15, 0),
|
||||
)
|
||||
|
||||
resource_16 = Resource(
|
||||
id=16,
|
||||
name="Economic Empowerment Program",
|
||||
summary="Financial literacy and job readiness training for survivors",
|
||||
link="https://www.purplepurse.com",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 16, 16, 30, 0),
|
||||
)
|
||||
|
||||
resource_17 = Resource(
|
||||
id=17,
|
||||
name="National Domestic Violence Law Project",
|
||||
summary="Legal information and resources for domestic violence survivors",
|
||||
link="https://www.womenslaw.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 17, 9, 45, 0),
|
||||
)
|
||||
|
||||
resource_18 = Resource(
|
||||
id=18,
|
||||
name="Victim Rights Law Center",
|
||||
summary="Free legal services for victims of sexual assault",
|
||||
link="https://victimrights.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 18, 12, 0, 0),
|
||||
)
|
||||
|
||||
resource_19 = Resource(
|
||||
id=19,
|
||||
name="Financial Justice Project",
|
||||
summary="Advocating for economic justice for survivors of violence",
|
||||
link="https://www.financialjusticeproject.org",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 19, 15, 30, 0),
|
||||
)
|
||||
|
||||
resource_20 = Resource(
|
||||
id=20,
|
||||
name="National Center on Domestic and Sexual Violence",
|
||||
summary="Training and resources to end domestic and sexual violence",
|
||||
link="http://www.ncdsv.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 20, 10, 15, 0),
|
||||
)
|
||||
|
||||
resources1 = [
|
||||
resource_1,
|
||||
resource_2,
|
||||
resource_3,
|
||||
resource_4,
|
||||
resource_5,
|
||||
resource_6,
|
||||
resource_7,
|
||||
resource_8,
|
||||
resource_9,
|
||||
resource_10,
|
||||
resource_11,
|
||||
resource_12,
|
||||
resource_13,
|
||||
resource_14,
|
||||
resource_15,
|
||||
resource_16,
|
||||
resource_17,
|
||||
resource_18,
|
||||
resource_19,
|
||||
resource_20,
|
||||
]
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake resource data into the test session."""
|
||||
global resources1
|
||||
# Create entities for test resource data
|
||||
entities = []
|
||||
for resource in resources1:
|
||||
entity = ResourceEntity.from_model(resource)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake resource data into the test session."""
|
||||
global resources
|
||||
# Create entities for test resource data
|
||||
entities = []
|
||||
for resource in resources:
|
||||
entity = ResourceEntity.from_model(resource)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from ...entities import ResourceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from ...models.resource_model import Resource
|
||||
|
||||
resource1 = Resource(
|
||||
id=1,
|
||||
name="Resource 1",
|
||||
summary="Helpful information for victims of domestic violence",
|
||||
link="https://example.com/resource1",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 1, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource2 = Resource(
|
||||
id=2,
|
||||
name="Resource 2",
|
||||
summary="Legal assistance resources",
|
||||
link="https://example.com/resource2",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 2, 12, 30, 0),
|
||||
)
|
||||
|
||||
resource3 = Resource(
|
||||
id=3,
|
||||
name="Resource 3",
|
||||
summary="Financial aid resources",
|
||||
link="https://example.com/resource3",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 3, 15, 45, 0),
|
||||
)
|
||||
|
||||
resource4 = Resource(
|
||||
id=4,
|
||||
name="Resource 4",
|
||||
summary="Counseling and support groups",
|
||||
link="https://example.com/resource4",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 4, 9, 15, 0),
|
||||
)
|
||||
|
||||
resource5 = Resource(
|
||||
id=5,
|
||||
name="Resource 5",
|
||||
summary="Shelter and housing resources",
|
||||
link="https://example.com/resource5",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||
)
|
||||
|
||||
resources = [resource1, resource2, resource3, resource4, resource5]
|
||||
|
||||
resource_1 = Resource(
|
||||
id=1,
|
||||
name="National Domestic Violence Hotline",
|
||||
summary="24/7 confidential support for victims of domestic violence",
|
||||
link="https://www.thehotline.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 1, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource_2 = Resource(
|
||||
id=2,
|
||||
name="Legal Aid Society",
|
||||
summary="Free legal assistance for low-income individuals",
|
||||
link="https://www.legalaidnyc.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 2, 12, 30, 0),
|
||||
)
|
||||
|
||||
resource_3 = Resource(
|
||||
id=3,
|
||||
name="Financial Empowerment Center",
|
||||
summary="Free financial counseling and education services",
|
||||
link="https://www1.nyc.gov/site/dca/consumers/get-free-financial-counseling.page",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 3, 15, 45, 0),
|
||||
)
|
||||
|
||||
resource_4 = Resource(
|
||||
id=4,
|
||||
name="National Coalition Against Domestic Violence",
|
||||
summary="Resources and support for victims of domestic violence",
|
||||
link="https://ncadv.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 4, 9, 15, 0),
|
||||
)
|
||||
|
||||
resource_5 = Resource(
|
||||
id=5,
|
||||
name="Safe Horizon",
|
||||
summary="Shelter and support services for victims of violence",
|
||||
link="https://www.safehorizon.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||
)
|
||||
|
||||
resource_6 = Resource(
|
||||
id=6,
|
||||
name="National Sexual Assault Hotline",
|
||||
summary="24/7 confidential support for survivors of sexual assault",
|
||||
link="https://www.rainn.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 6, 14, 0, 0),
|
||||
)
|
||||
|
||||
resource_7 = Resource(
|
||||
id=7,
|
||||
name="Victim Compensation Fund",
|
||||
summary="Financial assistance for victims of crime",
|
||||
link="https://ovc.ojp.gov/program/victim-compensation",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 7, 16, 45, 0),
|
||||
)
|
||||
|
||||
resource_8 = Resource(
|
||||
id=8,
|
||||
name="Battered Women's Justice Project",
|
||||
summary="Legal and technical assistance for victims of domestic violence",
|
||||
link="https://www.bwjp.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 8, 10, 30, 0),
|
||||
)
|
||||
|
||||
resource_9 = Resource(
|
||||
id=9,
|
||||
name="National Network to End Domestic Violence",
|
||||
summary="Advocacy and resources for ending domestic violence",
|
||||
link="https://nnedv.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 9, 13, 0, 0),
|
||||
)
|
||||
|
||||
resource_10 = Resource(
|
||||
id=10,
|
||||
name="Economic Justice Project",
|
||||
summary="Promoting economic security for survivors of domestic violence",
|
||||
link="https://www.njep.org",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 10, 15, 15, 0),
|
||||
)
|
||||
|
||||
resource_11 = Resource(
|
||||
id=11,
|
||||
name="Domestic Violence Legal Hotline",
|
||||
summary="Free legal advice for victims of domestic violence",
|
||||
link="https://www.womenslaw.org/find-help/national/hotlines",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 11, 9, 0, 0),
|
||||
)
|
||||
|
||||
resource_12 = Resource(
|
||||
id=12,
|
||||
name="National Resource Center on Domestic Violence",
|
||||
summary="Comprehensive information and resources on domestic violence",
|
||||
link="https://nrcdv.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 12, 11, 30, 0),
|
||||
)
|
||||
|
||||
resource_13 = Resource(
|
||||
id=13,
|
||||
name="Financial Assistance for Victims of Crime",
|
||||
summary="Funding for expenses related to victimization",
|
||||
link="https://ovc.ojp.gov/program/victim-assistance-funding",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 13, 14, 45, 0),
|
||||
)
|
||||
|
||||
resource_14 = Resource(
|
||||
id=14,
|
||||
name="National Clearinghouse for the Defense of Battered Women",
|
||||
summary="Legal resources and support for battered women",
|
||||
link="https://www.ncdbw.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 14, 10, 0, 0),
|
||||
)
|
||||
|
||||
resource_15 = Resource(
|
||||
id=15,
|
||||
name="Victim Connect Resource Center",
|
||||
summary="Referral helpline for crime victims",
|
||||
link="https://victimconnect.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 15, 13, 15, 0),
|
||||
)
|
||||
|
||||
resource_16 = Resource(
|
||||
id=16,
|
||||
name="Economic Empowerment Program",
|
||||
summary="Financial literacy and job readiness training for survivors",
|
||||
link="https://www.purplepurse.com",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 16, 16, 30, 0),
|
||||
)
|
||||
|
||||
resource_17 = Resource(
|
||||
id=17,
|
||||
name="National Domestic Violence Law Project",
|
||||
summary="Legal information and resources for domestic violence survivors",
|
||||
link="https://www.womenslaw.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 17, 9, 45, 0),
|
||||
)
|
||||
|
||||
resource_18 = Resource(
|
||||
id=18,
|
||||
name="Victim Rights Law Center",
|
||||
summary="Free legal services for victims of sexual assault",
|
||||
link="https://victimrights.org",
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
created_at=datetime(2023, 6, 18, 12, 0, 0),
|
||||
)
|
||||
|
||||
resource_19 = Resource(
|
||||
id=19,
|
||||
name="Financial Justice Project",
|
||||
summary="Advocating for economic justice for survivors of violence",
|
||||
link="https://www.financialjusticeproject.org",
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
created_at=datetime(2023, 6, 19, 15, 30, 0),
|
||||
)
|
||||
|
||||
resource_20 = Resource(
|
||||
id=20,
|
||||
name="National Center on Domestic and Sexual Violence",
|
||||
summary="Training and resources to end domestic and sexual violence",
|
||||
link="http://www.ncdsv.org",
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
created_at=datetime(2023, 6, 20, 10, 15, 0),
|
||||
)
|
||||
|
||||
resources1 = [
|
||||
resource_1,
|
||||
resource_2,
|
||||
resource_3,
|
||||
resource_4,
|
||||
resource_5,
|
||||
resource_6,
|
||||
resource_7,
|
||||
resource_8,
|
||||
resource_9,
|
||||
resource_10,
|
||||
resource_11,
|
||||
resource_12,
|
||||
resource_13,
|
||||
resource_14,
|
||||
resource_15,
|
||||
resource_16,
|
||||
resource_17,
|
||||
resource_18,
|
||||
resource_19,
|
||||
resource_20,
|
||||
]
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake resource data into the test session."""
|
||||
global resources1
|
||||
# Create entities for test resource data
|
||||
entities = []
|
||||
for resource in resources1:
|
||||
entity = ResourceEntity.from_model(resource)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake resource data into the test session."""
|
||||
global resources
|
||||
# Create entities for test resource data
|
||||
entities = []
|
||||
for resource in resources:
|
||||
entity = ResourceEntity.from_model(resource)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from backend.services.service import ServiceService
|
||||
from backend.services.exceptions import ServiceNotFoundException
|
||||
from . import service_test_data
|
||||
from . import user_test_data
|
||||
from .fixtures import service_svc, user_svc
|
||||
from backend.models.service_model import Service
|
||||
import pytest
|
||||
|
||||
|
||||
def test_list(service_svc: ServiceService):
|
||||
service = service_svc.get_all(user_test_data.admin)
|
||||
assert len(service) == len(service_test_data.services)
|
||||
assert isinstance(service[0], Service)
|
||||
|
||||
|
||||
def test_get_by_name(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_name("service 1")
|
||||
assert service.name == service_test_data.service1.name
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_get_by_name_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service = service_svc.get_service_by_name("service 12")
|
||||
pytest.fail()
|
||||
|
||||
|
||||
def test_get_service_by_user_admin(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_user(user_test_data.admin)
|
||||
assert len(service) == len(service_test_data.services)
|
||||
|
||||
|
||||
def test_get_service_by_user_volun(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_user(user_test_data.volunteer)
|
||||
assert len(service) == 4
|
||||
|
||||
|
||||
def test_get_by_program(service_svc: ServiceService):
|
||||
services = service_svc.get_service_by_program(ProgramTypeEnum.COMMUNITY)
|
||||
for service in services:
|
||||
assert service.program == ProgramTypeEnum.COMMUNITY
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_create(service_svc: ServiceService):
|
||||
service = service_svc.create(user_test_data.admin, service_test_data.service7)
|
||||
assert service.name == service_test_data.service7.name
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_update(service_svc: ServiceService):
|
||||
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.requirements == service_test_data.service_6_edit.requirements
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_update_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service = service_svc.update(
|
||||
user_test_data.admin, service_test_data.new_service
|
||||
)
|
||||
pytest.fail()
|
||||
|
||||
|
||||
def test_delete(service_svc: ServiceService):
|
||||
service_svc.delete(user_test_data.admin, service_test_data.service_6)
|
||||
services = service_svc.get_all(user_test_data.admin)
|
||||
assert len(services) == len(service_test_data.services) - 1
|
||||
|
||||
|
||||
"""def test_delete_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service_svc.delete(user_test_data.admin, service_test_data.service_10)
|
||||
pytest.fail()"""
|
||||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from backend.services.service import ServiceService
|
||||
from backend.services.exceptions import ServiceNotFoundException
|
||||
from . import service_test_data
|
||||
from . import user_test_data
|
||||
from .fixtures import service_svc, user_svc
|
||||
from backend.models.service_model import Service
|
||||
import pytest
|
||||
|
||||
|
||||
def test_list(service_svc: ServiceService):
|
||||
service = service_svc.get_all(user_test_data.admin)
|
||||
assert len(service) == len(service_test_data.services)
|
||||
assert isinstance(service[0], Service)
|
||||
|
||||
|
||||
def test_get_by_name(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_name("service 1")
|
||||
assert service.name == service_test_data.service1.name
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_get_by_name_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service = service_svc.get_service_by_name("service 12")
|
||||
pytest.fail()
|
||||
|
||||
|
||||
def test_get_service_by_user_admin(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_user(user_test_data.admin)
|
||||
assert len(service) == len(service_test_data.services)
|
||||
|
||||
|
||||
def test_get_service_by_user_volun(service_svc: ServiceService):
|
||||
service = service_svc.get_service_by_user(user_test_data.volunteer)
|
||||
assert len(service) == 4
|
||||
|
||||
|
||||
def test_get_by_program(service_svc: ServiceService):
|
||||
services = service_svc.get_service_by_program(ProgramTypeEnum.COMMUNITY)
|
||||
for service in services:
|
||||
assert service.program == ProgramTypeEnum.COMMUNITY
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_create(service_svc: ServiceService):
|
||||
service = service_svc.create(user_test_data.admin, service_test_data.service7)
|
||||
assert service.name == service_test_data.service7.name
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_update(service_svc: ServiceService):
|
||||
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.requirements == service_test_data.service_6_edit.requirements
|
||||
assert isinstance(service, Service)
|
||||
|
||||
|
||||
def test_update_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service = service_svc.update(
|
||||
user_test_data.admin, service_test_data.new_service
|
||||
)
|
||||
pytest.fail()
|
||||
|
||||
|
||||
def test_delete(service_svc: ServiceService):
|
||||
service_svc.delete(user_test_data.admin, service_test_data.service_6)
|
||||
services = service_svc.get_all(user_test_data.admin)
|
||||
assert len(services) == len(service_test_data.services) - 1
|
||||
|
||||
|
||||
"""def test_delete_not_found(service_svc: ServiceService):
|
||||
with pytest.raises(ServiceNotFoundException):
|
||||
service_svc.delete(user_test_data.admin, service_test_data.service_10)
|
||||
pytest.fail()"""
|
||||
|
|
|
@ -1,353 +1,353 @@
|
|||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...entities import ServiceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from ...models.service_model import Service
|
||||
|
||||
service1 = Service(
|
||||
id=1,
|
||||
name="service 1",
|
||||
status="open",
|
||||
summary="presentation educating community on domestic violence",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service2 = Service(
|
||||
id=2,
|
||||
name="service 2",
|
||||
status="closed",
|
||||
summary="service finding safe places to stay",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service3 = Service(
|
||||
id=3,
|
||||
name="service 3",
|
||||
status="open",
|
||||
summary="",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service4 = Service(
|
||||
id=4,
|
||||
name="service 4",
|
||||
status="waitlist",
|
||||
summary="community event",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service5 = Service(
|
||||
id=5,
|
||||
name="service 5",
|
||||
status="open",
|
||||
summary="talk circle for victims of domestic violence",
|
||||
requirements=["18+"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service6 = Service(
|
||||
id=6,
|
||||
name="service 6",
|
||||
status="waitlist",
|
||||
summary="program offering economic assistance",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_6_edit = Service(
|
||||
id=6,
|
||||
name="service 6",
|
||||
status="open",
|
||||
summary="program offering economic assistance",
|
||||
requirements=["18+"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service7 = Service(
|
||||
id=7,
|
||||
name="service 7",
|
||||
status="waitlist",
|
||||
summary="insert generic description",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
new_service = Service(
|
||||
id=8,
|
||||
name="new service",
|
||||
status="open",
|
||||
summary="insert other generic description",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
services = [service1, service2, service3, service4, service5, service6]
|
||||
|
||||
service_1 = Service(
|
||||
id=1,
|
||||
name="Crisis Hotline",
|
||||
status="open",
|
||||
summary="24/7 support for individuals in crisis",
|
||||
requirements=["Anonymous", "Confidential"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_2 = Service(
|
||||
id=2,
|
||||
name="Shelter Placement",
|
||||
status="open",
|
||||
summary="Emergency shelter for victims of domestic violence",
|
||||
requirements=["Referral required", "Safety assessment"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_3 = Service(
|
||||
id=3,
|
||||
name="Legal Advocacy",
|
||||
status="waitlist",
|
||||
summary="Legal support and representation for survivors",
|
||||
requirements=["Intake required", "Income eligibility"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_4 = Service(
|
||||
id=4,
|
||||
name="Counseling Services",
|
||||
status="open",
|
||||
summary="Individual and group therapy for survivors",
|
||||
requirements=["Initial assessment", "Insurance accepted"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_5 = Service(
|
||||
id=5,
|
||||
name="Financial Assistance",
|
||||
status="open",
|
||||
summary="Emergency funds for survivors in need",
|
||||
requirements=["Application required", "Proof of income"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_6 = Service(
|
||||
id=6,
|
||||
name="Housing Assistance",
|
||||
status="waitlist",
|
||||
summary="Support for finding safe and affordable housing",
|
||||
requirements=["Referral required", "Background check"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_7 = Service(
|
||||
id=7,
|
||||
name="Job Training",
|
||||
status="open",
|
||||
summary="Employment skills training for survivors",
|
||||
requirements=["Enrollment required", "18+"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_8 = Service(
|
||||
id=8,
|
||||
name="Support Groups",
|
||||
status="open",
|
||||
summary="Peer support groups for survivors",
|
||||
requirements=["Registration required", "Confidential"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_9 = Service(
|
||||
id=9,
|
||||
name="Children's Services",
|
||||
status="open",
|
||||
summary="Specialized services for children exposed to domestic violence",
|
||||
requirements=["Parental consent", "Age-appropriate"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_10 = Service(
|
||||
id=10,
|
||||
name="Safety Planning",
|
||||
status="open",
|
||||
summary="Personalized safety planning for survivors",
|
||||
requirements=["Confidential", "Collaborative"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_11 = Service(
|
||||
id=11,
|
||||
name="Community Education",
|
||||
status="open",
|
||||
summary="Workshops and training on domestic violence prevention",
|
||||
requirements=["Open to the public", "Registration preferred"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_12 = Service(
|
||||
id=12,
|
||||
name="Healthcare Services",
|
||||
status="open",
|
||||
summary="Medical care and support for survivors",
|
||||
requirements=["Referral required", "Insurance accepted"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_13 = Service(
|
||||
id=13,
|
||||
name="Transportation Assistance",
|
||||
status="waitlist",
|
||||
summary="Help with transportation for survivors",
|
||||
requirements=["Eligibility assessment", "Limited availability"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_14 = Service(
|
||||
id=14,
|
||||
name="Court Accompaniment",
|
||||
status="open",
|
||||
summary="Support and advocacy during court proceedings",
|
||||
requirements=["Legal case", "Scheduling required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_15 = Service(
|
||||
id=15,
|
||||
name="Relocation Assistance",
|
||||
status="waitlist",
|
||||
summary="Support for relocating to a safe environment",
|
||||
requirements=["Referral required", "Safety assessment"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_16 = Service(
|
||||
id=16,
|
||||
name="Parenting Classes",
|
||||
status="open",
|
||||
summary="Education and support for parents",
|
||||
requirements=["Open to parents", "Pre-registration required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_17 = Service(
|
||||
id=17,
|
||||
name="Life Skills Training",
|
||||
status="open",
|
||||
summary="Workshops on various life skills for survivors",
|
||||
requirements=["Enrollment required", "Commitment to attend"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_18 = Service(
|
||||
id=18,
|
||||
name="Advocacy Services",
|
||||
status="open",
|
||||
summary="Individual advocacy and support for survivors",
|
||||
requirements=["Intake required", "Confidential"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_19 = Service(
|
||||
id=19,
|
||||
name="Volunteer Opportunities",
|
||||
status="open",
|
||||
summary="Various volunteer roles supporting the organization",
|
||||
requirements=["Background check", "Training required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_20 = Service(
|
||||
id=20,
|
||||
name="Referral Services",
|
||||
status="open",
|
||||
summary="Referrals to community resources and partner agencies",
|
||||
requirements=["Intake required", "Based on individual needs"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
services1 = [
|
||||
service_1,
|
||||
service_2,
|
||||
service_3,
|
||||
service_4,
|
||||
service_5,
|
||||
service_6,
|
||||
service_7,
|
||||
service_8,
|
||||
service_9,
|
||||
service_10,
|
||||
service_11,
|
||||
service_12,
|
||||
service_13,
|
||||
service_14,
|
||||
service_15,
|
||||
service_16,
|
||||
service_17,
|
||||
service_18,
|
||||
service_19,
|
||||
service_20,
|
||||
]
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake service data into the test session."""
|
||||
global services1
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for service in services1:
|
||||
entity = ServiceEntity.from_model(service)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake service data into the test session."""
|
||||
global services
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for service in services:
|
||||
entity = ServiceEntity.from_model(service)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...entities import ServiceEntity
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
from ...models.service_model import Service
|
||||
|
||||
service1 = Service(
|
||||
id=1,
|
||||
name="service 1",
|
||||
status="open",
|
||||
summary="presentation educating community on domestic violence",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service2 = Service(
|
||||
id=2,
|
||||
name="service 2",
|
||||
status="closed",
|
||||
summary="service finding safe places to stay",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service3 = Service(
|
||||
id=3,
|
||||
name="service 3",
|
||||
status="open",
|
||||
summary="",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service4 = Service(
|
||||
id=4,
|
||||
name="service 4",
|
||||
status="waitlist",
|
||||
summary="community event",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service5 = Service(
|
||||
id=5,
|
||||
name="service 5",
|
||||
status="open",
|
||||
summary="talk circle for victims of domestic violence",
|
||||
requirements=["18+"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service6 = Service(
|
||||
id=6,
|
||||
name="service 6",
|
||||
status="waitlist",
|
||||
summary="program offering economic assistance",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_6_edit = Service(
|
||||
id=6,
|
||||
name="service 6",
|
||||
status="open",
|
||||
summary="program offering economic assistance",
|
||||
requirements=["18+"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service7 = Service(
|
||||
id=7,
|
||||
name="service 7",
|
||||
status="waitlist",
|
||||
summary="insert generic description",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
new_service = Service(
|
||||
id=8,
|
||||
name="new service",
|
||||
status="open",
|
||||
summary="insert other generic description",
|
||||
requirements=[""],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
services = [service1, service2, service3, service4, service5, service6]
|
||||
|
||||
service_1 = Service(
|
||||
id=1,
|
||||
name="Crisis Hotline",
|
||||
status="open",
|
||||
summary="24/7 support for individuals in crisis",
|
||||
requirements=["Anonymous", "Confidential"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_2 = Service(
|
||||
id=2,
|
||||
name="Shelter Placement",
|
||||
status="open",
|
||||
summary="Emergency shelter for victims of domestic violence",
|
||||
requirements=["Referral required", "Safety assessment"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_3 = Service(
|
||||
id=3,
|
||||
name="Legal Advocacy",
|
||||
status="waitlist",
|
||||
summary="Legal support and representation for survivors",
|
||||
requirements=["Intake required", "Income eligibility"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_4 = Service(
|
||||
id=4,
|
||||
name="Counseling Services",
|
||||
status="open",
|
||||
summary="Individual and group therapy for survivors",
|
||||
requirements=["Initial assessment", "Insurance accepted"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_5 = Service(
|
||||
id=5,
|
||||
name="Financial Assistance",
|
||||
status="open",
|
||||
summary="Emergency funds for survivors in need",
|
||||
requirements=["Application required", "Proof of income"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_6 = Service(
|
||||
id=6,
|
||||
name="Housing Assistance",
|
||||
status="waitlist",
|
||||
summary="Support for finding safe and affordable housing",
|
||||
requirements=["Referral required", "Background check"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_7 = Service(
|
||||
id=7,
|
||||
name="Job Training",
|
||||
status="open",
|
||||
summary="Employment skills training for survivors",
|
||||
requirements=["Enrollment required", "18+"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_8 = Service(
|
||||
id=8,
|
||||
name="Support Groups",
|
||||
status="open",
|
||||
summary="Peer support groups for survivors",
|
||||
requirements=["Registration required", "Confidential"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_9 = Service(
|
||||
id=9,
|
||||
name="Children's Services",
|
||||
status="open",
|
||||
summary="Specialized services for children exposed to domestic violence",
|
||||
requirements=["Parental consent", "Age-appropriate"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_10 = Service(
|
||||
id=10,
|
||||
name="Safety Planning",
|
||||
status="open",
|
||||
summary="Personalized safety planning for survivors",
|
||||
requirements=["Confidential", "Collaborative"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_11 = Service(
|
||||
id=11,
|
||||
name="Community Education",
|
||||
status="open",
|
||||
summary="Workshops and training on domestic violence prevention",
|
||||
requirements=["Open to the public", "Registration preferred"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_12 = Service(
|
||||
id=12,
|
||||
name="Healthcare Services",
|
||||
status="open",
|
||||
summary="Medical care and support for survivors",
|
||||
requirements=["Referral required", "Insurance accepted"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_13 = Service(
|
||||
id=13,
|
||||
name="Transportation Assistance",
|
||||
status="waitlist",
|
||||
summary="Help with transportation for survivors",
|
||||
requirements=["Eligibility assessment", "Limited availability"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_14 = Service(
|
||||
id=14,
|
||||
name="Court Accompaniment",
|
||||
status="open",
|
||||
summary="Support and advocacy during court proceedings",
|
||||
requirements=["Legal case", "Scheduling required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_15 = Service(
|
||||
id=15,
|
||||
name="Relocation Assistance",
|
||||
status="waitlist",
|
||||
summary="Support for relocating to a safe environment",
|
||||
requirements=["Referral required", "Safety assessment"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_16 = Service(
|
||||
id=16,
|
||||
name="Parenting Classes",
|
||||
status="open",
|
||||
summary="Education and support for parents",
|
||||
requirements=["Open to parents", "Pre-registration required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_17 = Service(
|
||||
id=17,
|
||||
name="Life Skills Training",
|
||||
status="open",
|
||||
summary="Workshops on various life skills for survivors",
|
||||
requirements=["Enrollment required", "Commitment to attend"],
|
||||
program=ProgramTypeEnum.ECONOMIC,
|
||||
)
|
||||
|
||||
service_18 = Service(
|
||||
id=18,
|
||||
name="Advocacy Services",
|
||||
status="open",
|
||||
summary="Individual advocacy and support for survivors",
|
||||
requirements=["Intake required", "Confidential"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
service_19 = Service(
|
||||
id=19,
|
||||
name="Volunteer Opportunities",
|
||||
status="open",
|
||||
summary="Various volunteer roles supporting the organization",
|
||||
requirements=["Background check", "Training required"],
|
||||
program=ProgramTypeEnum.COMMUNITY,
|
||||
)
|
||||
|
||||
service_20 = Service(
|
||||
id=20,
|
||||
name="Referral Services",
|
||||
status="open",
|
||||
summary="Referrals to community resources and partner agencies",
|
||||
requirements=["Intake required", "Based on individual needs"],
|
||||
program=ProgramTypeEnum.DOMESTIC,
|
||||
)
|
||||
|
||||
services1 = [
|
||||
service_1,
|
||||
service_2,
|
||||
service_3,
|
||||
service_4,
|
||||
service_5,
|
||||
service_6,
|
||||
service_7,
|
||||
service_8,
|
||||
service_9,
|
||||
service_10,
|
||||
service_11,
|
||||
service_12,
|
||||
service_13,
|
||||
service_14,
|
||||
service_15,
|
||||
service_16,
|
||||
service_17,
|
||||
service_18,
|
||||
service_19,
|
||||
service_20,
|
||||
]
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake service data into the test session."""
|
||||
global services1
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for service in services1:
|
||||
entity = ServiceEntity.from_model(service)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake service data into the test session."""
|
||||
global services
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for service in services:
|
||||
entity = ServiceEntity.from_model(service)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
"""Tests for the TagService class."""
|
||||
|
||||
# PyTest
|
||||
import pytest
|
||||
from ...services.tag import TagService
|
||||
from .fixtures import tag_svc
|
||||
from .tag_test_data import tag1, tag2, tag3
|
||||
from . import tag_test_data
|
||||
|
||||
|
||||
def test_get_all(tag_svc: TagService):
|
||||
"""Test that all tags can be retrieved."""
|
||||
tags = tag_svc.all()
|
||||
"""Tests for the TagService class."""
|
||||
|
||||
# PyTest
|
||||
import pytest
|
||||
from ...services.tag import TagService
|
||||
from .fixtures import tag_svc
|
||||
from .tag_test_data import tag1, tag2, tag3
|
||||
from . import tag_test_data
|
||||
|
||||
|
||||
def test_get_all(tag_svc: TagService):
|
||||
"""Test that all tags can be retrieved."""
|
||||
tags = tag_svc.all()
|
||||
assert len(tags) == 3
|
|
@ -1,72 +1,72 @@
|
|||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
from ...models.tag_model import Tag
|
||||
|
||||
from ...entities.tag_entity import TagEntity
|
||||
from datetime import datetime
|
||||
|
||||
tag1 = Tag(id=1, content="Tag 1", 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())
|
||||
|
||||
tagToCreate = Tag(id=4, content="Tag 4", created_at=datetime.now())
|
||||
|
||||
tags = [tag1, tag2, tag3]
|
||||
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global tags
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for tag in tags:
|
||||
entity = TagEntity.from_model(tag)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, TagEntity, TagEntity.id, len(tags) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_data_fixture(session: Session):
|
||||
"""Insert fake data the session automatically when test is run.
|
||||
Note:
|
||||
This function runs automatically due to the fixture property `autouse=True`.
|
||||
"""
|
||||
insert_fake_data(session)
|
||||
session.commit()
|
||||
yield
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
from ...models.tag_model import Tag
|
||||
|
||||
from ...entities.tag_entity import TagEntity
|
||||
from datetime import datetime
|
||||
|
||||
tag1 = Tag(id=1, content="Tag 1", 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())
|
||||
|
||||
tagToCreate = Tag(id=4, content="Tag 4", created_at=datetime.now())
|
||||
|
||||
tags = [tag1, tag2, tag3]
|
||||
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global tags
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for tag in tags:
|
||||
entity = TagEntity.from_model(tag)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, TagEntity, TagEntity.id, len(tags) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_data_fixture(session: Session):
|
||||
"""Insert fake data the session automatically when test is run.
|
||||
Note:
|
||||
This function runs automatically due to the fixture property `autouse=True`.
|
||||
"""
|
||||
insert_fake_data(session)
|
||||
session.commit()
|
||||
yield
|
||||
|
|
|
@ -1,82 +1,82 @@
|
|||
"""Tests for the UserService class."""
|
||||
|
||||
# PyTest
|
||||
import pytest
|
||||
|
||||
from ...services import UserService
|
||||
from .fixtures import user_svc
|
||||
from ...models.user_model import User
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
from .user_test_data import employee, volunteer, admin, newUser, toDelete
|
||||
from . import user_test_data
|
||||
|
||||
|
||||
def test_create(user_svc: UserService):
|
||||
"""Test creating a user"""
|
||||
user1 = user_svc.create(admin)
|
||||
|
||||
print(user1)
|
||||
assert user1 is not None
|
||||
assert user1.id is not None
|
||||
|
||||
|
||||
def test_create_id_exists(user_svc: UserService):
|
||||
"""Test creating a user with id conflict"""
|
||||
user1 = user_svc.create(volunteer)
|
||||
assert user1 is not None
|
||||
assert user1.id is not None
|
||||
|
||||
|
||||
def test_get_all(user_svc: UserService):
|
||||
"""Test that all users can be retrieved."""
|
||||
users = user_svc.all()
|
||||
assert len(users) == 4
|
||||
|
||||
|
||||
def test_get_user_by_id(user_svc: UserService):
|
||||
"""Test getting a user by an id"""
|
||||
if volunteer.id != None:
|
||||
user = user_svc.get_user_by_id(volunteer.id)
|
||||
assert user is not None
|
||||
assert user.id is not None
|
||||
|
||||
|
||||
def test_get_user_by_id_nonexistent(user_svc: UserService):
|
||||
"""Test getting a user by id that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.get_by_id(100)
|
||||
|
||||
|
||||
def test_delete_user(user_svc: UserService):
|
||||
"""Test deleting a user"""
|
||||
user_svc.delete(toDelete)
|
||||
with pytest.raises(Exception):
|
||||
user_svc.get_user_by_id(toDelete.id)
|
||||
|
||||
|
||||
def test_delete_user_nonexistent(user_svc: UserService):
|
||||
"""Test deleting a user that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.delete(newUser)
|
||||
|
||||
|
||||
def test_update_user(user_svc: UserService):
|
||||
"""Test updating a user
|
||||
Updating volunteer
|
||||
"""
|
||||
user = user_svc.get_user_by_id(volunteer.id)
|
||||
assert user is not None
|
||||
user.username = "volunteer 1"
|
||||
user.email = "newemail@compass.com"
|
||||
updated_user = user_svc.update(user)
|
||||
assert updated_user is not None
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.username == "volunteer 1"
|
||||
assert updated_user.email == "newemail@compass.com"
|
||||
|
||||
|
||||
def test_update_user_nonexistent(user_svc: UserService):
|
||||
"""Test updated a user that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.update(newUser)
|
||||
"""Tests for the UserService class."""
|
||||
|
||||
# PyTest
|
||||
import pytest
|
||||
|
||||
from ...services import UserService
|
||||
from .fixtures import user_svc
|
||||
from ...models.user_model import User
|
||||
from ...models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
from .user_test_data import employee, volunteer, admin, newUser, toDelete
|
||||
from . import user_test_data
|
||||
|
||||
|
||||
def test_create(user_svc: UserService):
|
||||
"""Test creating a user"""
|
||||
user1 = user_svc.create(admin)
|
||||
|
||||
print(user1)
|
||||
assert user1 is not None
|
||||
assert user1.id is not None
|
||||
|
||||
|
||||
def test_create_id_exists(user_svc: UserService):
|
||||
"""Test creating a user with id conflict"""
|
||||
user1 = user_svc.create(volunteer)
|
||||
assert user1 is not None
|
||||
assert user1.id is not None
|
||||
|
||||
|
||||
def test_get_all(user_svc: UserService):
|
||||
"""Test that all users can be retrieved."""
|
||||
users = user_svc.all()
|
||||
assert len(users) == 4
|
||||
|
||||
|
||||
def test_get_user_by_id(user_svc: UserService):
|
||||
"""Test getting a user by an id"""
|
||||
if volunteer.id != None:
|
||||
user = user_svc.get_user_by_id(volunteer.id)
|
||||
assert user is not None
|
||||
assert user.id is not None
|
||||
|
||||
|
||||
def test_get_user_by_id_nonexistent(user_svc: UserService):
|
||||
"""Test getting a user by id that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.get_by_id(100)
|
||||
|
||||
|
||||
def test_delete_user(user_svc: UserService):
|
||||
"""Test deleting a user"""
|
||||
user_svc.delete(toDelete)
|
||||
with pytest.raises(Exception):
|
||||
user_svc.get_user_by_id(toDelete.id)
|
||||
|
||||
|
||||
def test_delete_user_nonexistent(user_svc: UserService):
|
||||
"""Test deleting a user that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.delete(newUser)
|
||||
|
||||
|
||||
def test_update_user(user_svc: UserService):
|
||||
"""Test updating a user
|
||||
Updating volunteer
|
||||
"""
|
||||
user = user_svc.get_user_by_id(volunteer.id)
|
||||
assert user is not None
|
||||
user.username = "volunteer 1"
|
||||
user.email = "newemail@compass.com"
|
||||
updated_user = user_svc.update(user)
|
||||
assert updated_user is not None
|
||||
assert updated_user.id == user.id
|
||||
assert updated_user.username == "volunteer 1"
|
||||
assert updated_user.email == "newemail@compass.com"
|
||||
|
||||
|
||||
def test_update_user_nonexistent(user_svc: UserService):
|
||||
"""Test updated a user that does not exist"""
|
||||
with pytest.raises(Exception):
|
||||
user_svc.update(newUser)
|
||||
|
|
|
@ -1,196 +1,196 @@
|
|||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
from ...models.user_model import User
|
||||
|
||||
# import model enums instead
|
||||
from ...models.enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
from ...entities.user_entity import UserEntity
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
programs = ProgramTypeEnum
|
||||
roles = UserTypeEnum
|
||||
|
||||
volunteer = User(
|
||||
id=1,
|
||||
uuid="test1",
|
||||
username="volunteer",
|
||||
email="volunteer@compass.com",
|
||||
experience=1,
|
||||
group="volunteers",
|
||||
program=[programs.COMMUNITY, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=UserTypeEnum.VOLUNTEER,
|
||||
)
|
||||
|
||||
employee = User(
|
||||
id=2,
|
||||
uuid="test2",
|
||||
username="employee",
|
||||
email="employee@compass.com",
|
||||
experience=5,
|
||||
group="employees",
|
||||
program=[programs.DOMESTIC, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.EMPLOYEE,
|
||||
)
|
||||
|
||||
admin = User(
|
||||
id=3,
|
||||
uuid="test3",
|
||||
username="admin",
|
||||
email="admin@compass.com",
|
||||
experience=10,
|
||||
group="admin",
|
||||
program=[
|
||||
programs.ECONOMIC,
|
||||
programs.DOMESTIC,
|
||||
programs.COMMUNITY,
|
||||
],
|
||||
created_at=datetime.now(),
|
||||
role=roles.ADMIN,
|
||||
)
|
||||
|
||||
newUser = User(
|
||||
id=4,
|
||||
username="new",
|
||||
uuid="test4",
|
||||
email="new@compass.com",
|
||||
experience=1,
|
||||
group="volunteer",
|
||||
program=[programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
toDelete = User(
|
||||
id=5,
|
||||
username="delete",
|
||||
email="delete@compass.com",
|
||||
experience=0,
|
||||
group="none",
|
||||
program=[programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
users = [volunteer, employee, admin, toDelete]
|
||||
|
||||
admin1 = User(
|
||||
username="Prajwal Moharana",
|
||||
uuid="acc6e112-d296-4739-a80c-b89b2933e50b",
|
||||
email="root@compass.com",
|
||||
experience=10,
|
||||
group="admin",
|
||||
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.ADMIN,
|
||||
)
|
||||
|
||||
employee1 = User(
|
||||
username="Mel Ho",
|
||||
uuid="c5fcff86-3deb-4d09-9f60-9b529e40161a",
|
||||
email="employee@compass.com",
|
||||
experience=5,
|
||||
group="employee",
|
||||
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.EMPLOYEE,
|
||||
)
|
||||
|
||||
volunteer1 = User(
|
||||
username="Pranav Wagh",
|
||||
uuid="1d2e114f-b286-4464-8528-d177dc226b09",
|
||||
email="volunteer1@compass.com",
|
||||
experience=2,
|
||||
group="volunteer",
|
||||
program=[programs.DOMESTIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
volunteer2 = User(
|
||||
username="Yashu Singhai",
|
||||
uuid="13888204-1bae-4be4-8192-1ca46be4fc7d",
|
||||
email="volunteer2@compass.com",
|
||||
experience=1,
|
||||
group="volunteer",
|
||||
program=[programs.COMMUNITY, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
users1 = [admin1, employee1, volunteer1, volunteer2]
|
||||
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global users
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for user in users:
|
||||
entity = UserEntity.from_model(user)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global users1
|
||||
|
||||
# Create entities for test organization data
|
||||
for user in users1:
|
||||
entity = UserEntity.from_model(user)
|
||||
session.add(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_data_fixture(session: Session):
|
||||
"""Insert fake data the session automatically when test is run.
|
||||
Note:
|
||||
This function runs automatically due to the fixture property `autouse=True`.
|
||||
"""
|
||||
insert_fake_data(session)
|
||||
session.commit()
|
||||
yield
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
from ...models.user_model import User
|
||||
|
||||
# import model enums instead
|
||||
from ...models.enum_for_models import UserTypeEnum, ProgramTypeEnum
|
||||
from ...entities.user_entity import UserEntity
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
programs = ProgramTypeEnum
|
||||
roles = UserTypeEnum
|
||||
|
||||
volunteer = User(
|
||||
id=1,
|
||||
uuid="test1",
|
||||
username="volunteer",
|
||||
email="volunteer@compass.com",
|
||||
experience=1,
|
||||
group="volunteers",
|
||||
program=[programs.COMMUNITY, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=UserTypeEnum.VOLUNTEER,
|
||||
)
|
||||
|
||||
employee = User(
|
||||
id=2,
|
||||
uuid="test2",
|
||||
username="employee",
|
||||
email="employee@compass.com",
|
||||
experience=5,
|
||||
group="employees",
|
||||
program=[programs.DOMESTIC, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.EMPLOYEE,
|
||||
)
|
||||
|
||||
admin = User(
|
||||
id=3,
|
||||
uuid="test3",
|
||||
username="admin",
|
||||
email="admin@compass.com",
|
||||
experience=10,
|
||||
group="admin",
|
||||
program=[
|
||||
programs.ECONOMIC,
|
||||
programs.DOMESTIC,
|
||||
programs.COMMUNITY,
|
||||
],
|
||||
created_at=datetime.now(),
|
||||
role=roles.ADMIN,
|
||||
)
|
||||
|
||||
newUser = User(
|
||||
id=4,
|
||||
username="new",
|
||||
uuid="test4",
|
||||
email="new@compass.com",
|
||||
experience=1,
|
||||
group="volunteer",
|
||||
program=[programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
toDelete = User(
|
||||
id=5,
|
||||
username="delete",
|
||||
email="delete@compass.com",
|
||||
experience=0,
|
||||
group="none",
|
||||
program=[programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
users = [volunteer, employee, admin, toDelete]
|
||||
|
||||
admin1 = User(
|
||||
username="Prajwal Moharana",
|
||||
uuid="acc6e112-d296-4739-a80c-b89b2933e50b",
|
||||
email="root@compass.com",
|
||||
experience=10,
|
||||
group="admin",
|
||||
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.ADMIN,
|
||||
)
|
||||
|
||||
employee1 = User(
|
||||
username="Mel Ho",
|
||||
uuid="c5fcff86-3deb-4d09-9f60-9b529e40161a",
|
||||
email="employee@compass.com",
|
||||
experience=5,
|
||||
group="employee",
|
||||
program=[programs.ECONOMIC, programs.DOMESTIC, programs.COMMUNITY],
|
||||
created_at=datetime.now(),
|
||||
role=roles.EMPLOYEE,
|
||||
)
|
||||
|
||||
volunteer1 = User(
|
||||
username="Pranav Wagh",
|
||||
uuid="1d2e114f-b286-4464-8528-d177dc226b09",
|
||||
email="volunteer1@compass.com",
|
||||
experience=2,
|
||||
group="volunteer",
|
||||
program=[programs.DOMESTIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
volunteer2 = User(
|
||||
username="Yashu Singhai",
|
||||
uuid="13888204-1bae-4be4-8192-1ca46be4fc7d",
|
||||
email="volunteer2@compass.com",
|
||||
experience=1,
|
||||
group="volunteer",
|
||||
program=[programs.COMMUNITY, programs.ECONOMIC],
|
||||
created_at=datetime.now(),
|
||||
role=roles.VOLUNTEER,
|
||||
)
|
||||
|
||||
users1 = [admin1, employee1, volunteer1, volunteer2]
|
||||
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute
|
||||
|
||||
|
||||
def reset_table_id_seq(
|
||||
session: Session,
|
||||
entity: type[DeclarativeBase],
|
||||
entity_id_column: InstrumentedAttribute[int],
|
||||
next_id: int,
|
||||
) -> None:
|
||||
"""Reset the ID sequence of an entity table.
|
||||
|
||||
Args:
|
||||
session (Session) - A SQLAlchemy Session
|
||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||
|
||||
Returns:
|
||||
None"""
|
||||
table = entity.__table__
|
||||
id_column_name = entity_id_column.name
|
||||
sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}")
|
||||
session.execute(sql)
|
||||
|
||||
|
||||
def insert_fake_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global users
|
||||
|
||||
# Create entities for test organization data
|
||||
entities = []
|
||||
for user in users:
|
||||
entity = UserEntity.from_model(user)
|
||||
session.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
def insert_test_data(session: Session):
|
||||
"""Inserts fake organization data into the test session."""
|
||||
|
||||
global users1
|
||||
|
||||
# Create entities for test organization data
|
||||
for user in users1:
|
||||
entity = UserEntity.from_model(user)
|
||||
session.add(entity)
|
||||
|
||||
# Reset table IDs to prevent ID conflicts
|
||||
reset_table_id_seq(session, UserEntity, UserEntity.id, len(users1) + 1)
|
||||
|
||||
# Commit all changes
|
||||
session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_data_fixture(session: Session):
|
||||
"""Insert fake data the session automatically when test is run.
|
||||
Note:
|
||||
This function runs automatically due to the fixture property `autouse=True`.
|
||||
"""
|
||||
insert_fake_data(session)
|
||||
session.commit()
|
||||
yield
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
|
@ -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).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## 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.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
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
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## 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.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
|
|
@ -1,100 +1,100 @@
|
|||
"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 admin 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();
|
||||
|
||||
if (user.role !== Role.ADMIN) {
|
||||
console.log(
|
||||
`Accessed admin page but incorrect permissions: ${user.username} ${user.role}`
|
||||
);
|
||||
router.push("/home");
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
"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 admin 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();
|
||||
|
||||
if (user.role !== Role.ADMIN) {
|
||||
console.log(
|
||||
`Accessed admin page but incorrect permissions: ${user.username} ${user.role}`
|
||||
);
|
||||
router.push("/home");
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import { PageLayout } from "@/components/PageLayout";
|
||||
import { Table } from "@/components/Table/Index";
|
||||
import User from "@/utils/models/User";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { UsersIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed admin page but not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
const userListData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
|
||||
);
|
||||
|
||||
const users: User[] = await userListData.json();
|
||||
|
||||
setUsers(users);
|
||||
}
|
||||
|
||||
getUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||
<Table users={users} />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { PageLayout } from "@/components/PageLayout";
|
||||
import { Table } from "@/components/Table/Index";
|
||||
import User from "@/utils/models/User";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { UsersIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed admin page but not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
const userListData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
|
||||
);
|
||||
|
||||
const users: User[] = await userListData.json();
|
||||
|
||||
setUsers(users);
|
||||
}
|
||||
|
||||
getUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||
<Table users={users} />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/health`;
|
||||
|
||||
const result = await fetch(apiEndpoint);
|
||||
|
||||
return NextResponse.json(await result.json(), { status: result.status });
|
||||
}
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/health`;
|
||||
|
||||
const result = await fetch(apiEndpoint);
|
||||
|
||||
return NextResponse.json(await result.json(), { status: result.status });
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import Resource from "@/utils/models/Resource";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const resourceData: Resource[] = await data.json();
|
||||
// TODO: Remove make every resource visible
|
||||
|
||||
const resources = resourceData.map((resource: Resource) => {
|
||||
resource.visible = true;
|
||||
|
||||
return resource;
|
||||
});
|
||||
|
||||
return NextResponse.json(resources, { status: data.status });
|
||||
}
|
||||
import Resource from "@/utils/models/Resource";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const resourceData: Resource[] = await data.json();
|
||||
// TODO: Remove make every resource visible
|
||||
|
||||
const resources = resourceData.map((resource: Resource) => {
|
||||
resource.visible = true;
|
||||
|
||||
return resource;
|
||||
});
|
||||
|
||||
return NextResponse.json(resources, { status: data.status });
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello World!" }, { status: 200 });
|
||||
}
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello World!" }, { status: 200 });
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import Service from "@/utils/models/Service";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const serviceData: Service[] = await data.json();
|
||||
// TODO: Remove make every service visible
|
||||
|
||||
const services = serviceData.map((service: Service) => {
|
||||
service.visible = true;
|
||||
|
||||
return service;
|
||||
});
|
||||
|
||||
return NextResponse.json(services, { status: data.status });
|
||||
}
|
||||
import Service from "@/utils/models/Service";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const serviceData: Service[] = await data.json();
|
||||
// TODO: Remove make every service visible
|
||||
|
||||
const services = serviceData.map((service: Service) => {
|
||||
service.visible = true;
|
||||
|
||||
return service;
|
||||
});
|
||||
|
||||
return NextResponse.json(services, { status: data.status });
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import User from "@/utils/models/User";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const userData: User[] = await data.json();
|
||||
// TODO: Remove make every user visible
|
||||
|
||||
const users = userData.map((user: User) => {
|
||||
user.visible = true;
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
return NextResponse.json(users, { status: data.status });
|
||||
}
|
||||
import User from "@/utils/models/User";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user/all`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||
|
||||
const userData: User[] = await data.json();
|
||||
// TODO: Remove make every user visible
|
||||
|
||||
const users = userData.map((user: User) => {
|
||||
user.visible = true;
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
return NextResponse.json(users, { status: data.status });
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}/${uuid}`);
|
||||
|
||||
return NextResponse.json(await data.json(), { status: data.status });
|
||||
}
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
|
||||
|
||||
console.log(apiEndpoint);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const uuid = searchParams.get("uuid");
|
||||
|
||||
const data = await fetch(`${apiEndpoint}/${uuid}`);
|
||||
|
||||
return NextResponse.json(await data.json(), { status: data.status });
|
||||
}
|
||||
|
|
|
@ -1,58 +1,58 @@
|
|||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import User, { Role } from "@/utils/models/User";
|
||||
|
||||
export async function login(email: string, password: string) {
|
||||
const supabase = createClient();
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
const { error } = await supabase.auth.signInWithPassword(data);
|
||||
if (error) {
|
||||
return "Incorrect email/password";
|
||||
}
|
||||
|
||||
const supabaseUser = await supabase.auth.getUser();
|
||||
|
||||
if (!supabaseUser.data.user) {
|
||||
revalidatePath("/home", "layout");
|
||||
redirect("/home");
|
||||
}
|
||||
|
||||
const apiData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${supabaseUser.data.user.id}`
|
||||
);
|
||||
|
||||
const user: User = await apiData.json();
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (user.role === Role.ADMIN) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
revalidatePath("/home", "layout");
|
||||
redirect("/home");
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error || !data?.user) {
|
||||
redirect("auth/login");
|
||||
}
|
||||
|
||||
console.log(`Signed out ${data.user.email}!`);
|
||||
|
||||
await supabase.auth.signOut();
|
||||
|
||||
revalidatePath("/auth/login", "layout");
|
||||
redirect("/auth/login");
|
||||
}
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import User, { Role } from "@/utils/models/User";
|
||||
|
||||
export async function login(email: string, password: string) {
|
||||
const supabase = createClient();
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
const { error } = await supabase.auth.signInWithPassword(data);
|
||||
if (error) {
|
||||
return "Incorrect email/password";
|
||||
}
|
||||
|
||||
const supabaseUser = await supabase.auth.getUser();
|
||||
|
||||
if (!supabaseUser.data.user) {
|
||||
revalidatePath("/home", "layout");
|
||||
redirect("/home");
|
||||
}
|
||||
|
||||
const apiData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${supabaseUser.data.user.id}`
|
||||
);
|
||||
|
||||
const user: User = await apiData.json();
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (user.role === Role.ADMIN) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
revalidatePath("/home", "layout");
|
||||
redirect("/home");
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error || !data?.user) {
|
||||
redirect("auth/login");
|
||||
}
|
||||
|
||||
console.log(`Signed out ${data.user.email}!`);
|
||||
|
||||
await supabase.auth.signOut();
|
||||
|
||||
revalidatePath("/auth/login", "layout");
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function ErrorPage() {
|
||||
return <p>Sorry, something went wrong</p>;
|
||||
}
|
||||
export default function ErrorPage() {
|
||||
return <p>Sorry, something went wrong</p>;
|
||||
}
|
||||
|
|
|
@ -1,54 +1,54 @@
|
|||
// pages/forgot-password.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Input from "@/components/Input";
|
||||
import Button from "@/components/Button";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [confirmEmail, setConfirmEmail] = useState("");
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
|
||||
function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (email.trim() === "") {
|
||||
setEmailError("Email cannot be empty");
|
||||
return false;
|
||||
} else if (!emailRegex.test(email)) {
|
||||
setEmailError("Invalid email format");
|
||||
return false;
|
||||
}
|
||||
return true; // No error
|
||||
}
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
isValidEmail(confirmEmail);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-bold text-xl text-purple-800">
|
||||
Forgot Password
|
||||
</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError == null}
|
||||
title="Enter your email address"
|
||||
placeholder="janedoe@gmail.com"
|
||||
value={confirmEmail}
|
||||
onChange={(e) => setConfirmEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/login">Back to Sign In</InlineLink>
|
||||
<Button type="submit" onClick={handleClick}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// pages/forgot-password.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Input from "@/components/Input";
|
||||
import Button from "@/components/Button";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [confirmEmail, setConfirmEmail] = useState("");
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
|
||||
function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (email.trim() === "") {
|
||||
setEmailError("Email cannot be empty");
|
||||
return false;
|
||||
} else if (!emailRegex.test(email)) {
|
||||
setEmailError("Invalid email format");
|
||||
return false;
|
||||
}
|
||||
return true; // No error
|
||||
}
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
isValidEmail(confirmEmail);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-bold text-xl text-purple-800">
|
||||
Forgot Password
|
||||
</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError == null}
|
||||
title="Enter your email address"
|
||||
placeholder="janedoe@gmail.com"
|
||||
value={confirmEmail}
|
||||
onChange={(e) => setConfirmEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/login">Back to Sign In</InlineLink>
|
||||
<Button type="submit" onClick={handleClick}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import Paper from "@/components/auth/Paper";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</form>
|
||||
<p className="text-center mt-6 text-gray-500 text-xs">
|
||||
© 2024 Compass Center
|
||||
</p>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
import Paper from "@/components/auth/Paper";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</form>
|
||||
<p className="text-center mt-6 text-gray-500 text-xs">
|
||||
© 2024 Compass Center
|
||||
</p>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,118 +1,122 @@
|
|||
// pages/index.tsx
|
||||
"use client";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
import { login } from "../actions";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
async function checkUser() {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
router.push("/home");
|
||||
}
|
||||
}
|
||||
checkUser();
|
||||
}, [router]);
|
||||
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email.trim().length === 0) {
|
||||
setEmailError("Please enter your email.");
|
||||
return;
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
setEmailError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
setEmailError("");
|
||||
|
||||
if (password.trim().length === 0) {
|
||||
setPasswordError("Please enter your password.");
|
||||
return;
|
||||
}
|
||||
setPasswordError("");
|
||||
|
||||
setIsLoading(true);
|
||||
const error = await login(email, password);
|
||||
setIsLoading(false);
|
||||
|
||||
if (error) {
|
||||
setLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError === ""}
|
||||
title="Email"
|
||||
placeholder="Enter Email"
|
||||
onChange={handleEmailChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Password"
|
||||
placeholder="Enter Password"
|
||||
valid={passwordError === ""}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <ErrorBanner heading={passwordError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/forgot_password">
|
||||
Forgot password?
|
||||
</InlineLink>
|
||||
<Button onClick={handleClick} disabled={isLoading}>
|
||||
<div className="flex items-center justify-center">
|
||||
{isLoading && (
|
||||
<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"}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{loginError && <ErrorBanner heading={loginError} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// pages/index.tsx
|
||||
"use client";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
import { login } from "../actions";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("here");
|
||||
},[])
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
async function checkUser() {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
router.push("/home");
|
||||
}
|
||||
}
|
||||
checkUser();
|
||||
}, [router]);
|
||||
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email.trim().length === 0) {
|
||||
setEmailError("Please enter your email.");
|
||||
return;
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
setEmailError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
setEmailError("");
|
||||
|
||||
if (password.trim().length === 0) {
|
||||
setPasswordError("Please enter your password.");
|
||||
return;
|
||||
}
|
||||
setPasswordError("");
|
||||
|
||||
setIsLoading(true);
|
||||
const error = await login(email, password);
|
||||
setIsLoading(false);
|
||||
|
||||
if (error) {
|
||||
setLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError === ""}
|
||||
title="Email"
|
||||
placeholder="Enter Email"
|
||||
onChange={handleEmailChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Password"
|
||||
placeholder="Enter Password"
|
||||
valid={passwordError === ""}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <ErrorBanner heading={passwordError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/forgot_password">
|
||||
Forgot password?
|
||||
</InlineLink>
|
||||
<Button onClick={handleClick} disabled={isLoading}>
|
||||
<div className="flex items-center justify-center">
|
||||
{isLoading && (
|
||||
<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"}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{loginError && <ErrorBanner heading={loginError} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,79 +1,79 @@
|
|||
// pages/index.tsx
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
|
||||
function isStrongPassword(password: string): boolean {
|
||||
const strongPasswordRegex =
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
|
||||
return strongPasswordRegex.test(password);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsButtonDisabled(
|
||||
newPassword === "" ||
|
||||
confirmPassword === "" ||
|
||||
newPassword !== confirmPassword ||
|
||||
!isStrongPassword(newPassword)
|
||||
);
|
||||
}, [newPassword, confirmPassword]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-center sm:text-left">
|
||||
<h1 className="font-bold text-xl text-purple-800">
|
||||
New Password
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<PasswordInput
|
||||
title="Enter New Password"
|
||||
value={newPassword}
|
||||
valid={!isButtonDisabled || isStrongPassword(newPassword)}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isStrongPassword(newPassword) || newPassword === "" ? null : (
|
||||
<ErrorBanner
|
||||
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!"
|
||||
/>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Confirm Password"
|
||||
value={confirmPassword}
|
||||
valid={
|
||||
!isButtonDisabled ||
|
||||
(newPassword === confirmPassword &&
|
||||
confirmPassword !== "")
|
||||
}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{newPassword === confirmPassword ||
|
||||
confirmPassword === "" ? null : (
|
||||
<ErrorBanner
|
||||
heading="Passwords do not match."
|
||||
description="Please make sure both passwords are the exact same!"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<Button type="submit" disabled={isButtonDisabled}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// pages/index.tsx
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
|
||||
function isStrongPassword(password: string): boolean {
|
||||
const strongPasswordRegex =
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
|
||||
return strongPasswordRegex.test(password);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsButtonDisabled(
|
||||
newPassword === "" ||
|
||||
confirmPassword === "" ||
|
||||
newPassword !== confirmPassword ||
|
||||
!isStrongPassword(newPassword)
|
||||
);
|
||||
}, [newPassword, confirmPassword]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-center sm:text-left">
|
||||
<h1 className="font-bold text-xl text-purple-800">
|
||||
New Password
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<PasswordInput
|
||||
title="Enter New Password"
|
||||
value={newPassword}
|
||||
valid={!isButtonDisabled || isStrongPassword(newPassword)}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isStrongPassword(newPassword) || newPassword === "" ? null : (
|
||||
<ErrorBanner
|
||||
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!"
|
||||
/>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Confirm Password"
|
||||
value={confirmPassword}
|
||||
valid={
|
||||
!isButtonDisabled ||
|
||||
(newPassword === confirmPassword &&
|
||||
confirmPassword !== "")
|
||||
}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{newPassword === confirmPassword ||
|
||||
confirmPassword === "" ? null : (
|
||||
<ErrorBanner
|
||||
heading="Passwords do not match."
|
||||
description="Please make sure both passwords are the exact same!"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<Button type="submit" disabled={isButtonDisabled}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,89 +1,89 @@
|
|||
"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 { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 [user, setUser] = useState<User>();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
console.log(data, error);
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed home 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}`
|
||||
);
|
||||
|
||||
setUser(await userData.json());
|
||||
}
|
||||
|
||||
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
|
||||
name={user.username}
|
||||
email={user.email}
|
||||
setIsSidebarOpen={setIsSidebarOpen}
|
||||
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>
|
||||
);
|
||||
}
|
||||
"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 { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 [user, setUser] = useState<User>();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
console.log(data, error);
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed home 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}`
|
||||
);
|
||||
|
||||
setUser(await userData.json());
|
||||
}
|
||||
|
||||
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
|
||||
name={user.username}
|
||||
email={user.email}
|
||||
setIsSidebarOpen={setIsSidebarOpen}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
"use client";
|
||||
import Callout from "@/components/resource/Callout";
|
||||
import Card from "@/components/resource/Card";
|
||||
import { LandingSearchBar } from "@/components/resource/LandingSearchBar";
|
||||
import {
|
||||
BookOpenIcon,
|
||||
BookmarkIcon,
|
||||
ClipboardIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={25}
|
||||
height={25}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">
|
||||
Compass Center Advocate Landing Page
|
||||
</h1>
|
||||
</div>
|
||||
<Callout>
|
||||
Welcome! Below you will find a list of resources for the
|
||||
Compass Center's trained advocates. These materials
|
||||
serve to virtually provide a collection of advocacy,
|
||||
resource, and hotline manuals and information.
|
||||
<b>
|
||||
{" "}
|
||||
If you are an advocate looking for the contact
|
||||
information of a particular Compass Center employee,
|
||||
please directly contact your staff back-up or the person
|
||||
in charge of your training.
|
||||
</b>
|
||||
</Callout>
|
||||
</div>
|
||||
<div className="p-8 flex-grow border-t border-gray-200 bg-gray-50">
|
||||
{/* link to different pages */}
|
||||
<div className="grid grid-cols-3 gap-6 pb-6">
|
||||
<Link href="/resource">
|
||||
<Card icon={<BookmarkIcon />} text="Resources" />
|
||||
</Link>
|
||||
<Link href="/service">
|
||||
<Card icon={<ClipboardIcon />} text="Services" />
|
||||
</Link>
|
||||
<Link href="/training-manual">
|
||||
<Card icon={<BookOpenIcon />} text="Training Manuals" />
|
||||
</Link>
|
||||
</div>
|
||||
{/* search bar */}
|
||||
<LandingSearchBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
import Callout from "@/components/resource/Callout";
|
||||
import Card from "@/components/resource/Card";
|
||||
import { LandingSearchBar } from "@/components/resource/LandingSearchBar";
|
||||
import {
|
||||
BookOpenIcon,
|
||||
BookmarkIcon,
|
||||
ClipboardIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={25}
|
||||
height={25}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">
|
||||
Compass Center Advocate Landing Page
|
||||
</h1>
|
||||
</div>
|
||||
<Callout>
|
||||
Welcome! Below you will find a list of resources for the
|
||||
Compass Center's trained advocates. These materials
|
||||
serve to virtually provide a collection of advocacy,
|
||||
resource, and hotline manuals and information.
|
||||
<b>
|
||||
{" "}
|
||||
If you are an advocate looking for the contact
|
||||
information of a particular Compass Center employee,
|
||||
please directly contact your staff back-up or the person
|
||||
in charge of your training.
|
||||
</b>
|
||||
</Callout>
|
||||
</div>
|
||||
<div className="p-8 flex-grow border-t border-gray-200 bg-gray-50">
|
||||
{/* link to different pages */}
|
||||
<div className="grid grid-cols-3 gap-6 pb-6">
|
||||
<Link href="/resource">
|
||||
<Card icon={<BookmarkIcon />} text="Resources" />
|
||||
</Link>
|
||||
<Link href="/service">
|
||||
<Card icon={<ClipboardIcon />} text="Services" />
|
||||
</Link>
|
||||
<Link href="/training-manual">
|
||||
<Card icon={<BookOpenIcon />} text="Training Manuals" />
|
||||
</Link>
|
||||
</div>
|
||||
{/* search bar */}
|
||||
<LandingSearchBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
router.push("/auth/login");
|
||||
|
||||
return <h1>GO TO LOGIN PAGE (/auth/login)</h1>;
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
router.push("/auth/login");
|
||||
|
||||
return <h1>GO TO LOGIN PAGE (/auth/login)</h1>;
|
||||
}
|
||||
|
|
|
@ -1,92 +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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
"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[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getResources() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
"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[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getResources() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
92
compass/app/resource_test/layout.tsx
Normal file
92
compass/app/resource_test/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
49
compass/app/resource_test/page.tsx
Normal file
49
compass/app/resource_test/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -1,92 +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 service 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>
|
||||
);
|
||||
}
|
||||
"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 service 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import { PageLayout } from "@/components/PageLayout";
|
||||
import { ServiceTable } from "@/components/Table/ServiceIndex";
|
||||
import Service from "@/utils/models/Service";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { ClipboardIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [services, setUsers] = useState<Service[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getServices() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed admin page but not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceListData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}`
|
||||
);
|
||||
|
||||
const servicesAPI: Service[] = await serviceListData.json();
|
||||
setUsers(servicesAPI);
|
||||
}
|
||||
|
||||
getServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
||||
<ServiceTable users={services} />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { PageLayout } from "@/components/PageLayout";
|
||||
import { ServiceTable } from "@/components/Table/ServiceIndex";
|
||||
import Service from "@/utils/models/Service";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { ClipboardIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [services, setUsers] = useState<Service[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getServices() {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Accessed admin page but not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceListData = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}`
|
||||
);
|
||||
|
||||
const servicesAPI: Service[] = await serviceListData.json();
|
||||
setUsers(servicesAPI);
|
||||
}
|
||||
|
||||
getServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
||||
<ServiceTable users={services} />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import Paper from "@/components/auth/Paper";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</form>
|
||||
<p className="text-center mt-6 text-gray-500 text-xs">
|
||||
© 2024 Compass Center
|
||||
</p>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
import Paper from "@/components/auth/Paper";
|
||||
|
||||
export default function RootLayout({
|
||||
// Layouts must accept a children prop.
|
||||
// This will be populated with nested layouts or pages
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</form>
|
||||
<p className="text-center mt-6 text-gray-500 text-xs">
|
||||
© 2024 Compass Center
|
||||
</p>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,116 +1,116 @@
|
|||
"use client";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
import { login } from "../auth/actions";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
async function checkUser() {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
router.push("/home");
|
||||
}
|
||||
}
|
||||
checkUser();
|
||||
}, [router]);
|
||||
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email.trim().length === 0) {
|
||||
setEmailError("Please enter your email.");
|
||||
return;
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
setEmailError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
setEmailError("");
|
||||
|
||||
if (password.trim().length === 0) {
|
||||
setPasswordError("Please enter your password.");
|
||||
return;
|
||||
}
|
||||
setPasswordError("");
|
||||
|
||||
setIsLoading(true);
|
||||
const error = await login(email, password);
|
||||
setIsLoading(false);
|
||||
|
||||
if (error) {
|
||||
setLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError === ""}
|
||||
title="Email"
|
||||
placeholder="Enter Email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Password"
|
||||
placeholder="Enter Password"
|
||||
valid={passwordError === ""}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <ErrorBanner heading={passwordError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/forgot_password">
|
||||
Forgot password?
|
||||
</InlineLink>
|
||||
<Button onClick={handleClick} disabled={isLoading}>
|
||||
<div className="flex items-center justify-center">
|
||||
{isLoading && (
|
||||
<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"}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{loginError && <ErrorBanner heading={loginError} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import PasswordInput from "@/components/auth/PasswordInput";
|
||||
import ErrorBanner from "@/components/auth/ErrorBanner";
|
||||
import { login } from "../auth/actions";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
async function checkUser() {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
router.push("/home");
|
||||
}
|
||||
}
|
||||
checkUser();
|
||||
}, [router]);
|
||||
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email.trim().length === 0) {
|
||||
setEmailError("Please enter your email.");
|
||||
return;
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
setEmailError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
setEmailError("");
|
||||
|
||||
if (password.trim().length === 0) {
|
||||
setPasswordError("Please enter your password.");
|
||||
return;
|
||||
}
|
||||
setPasswordError("");
|
||||
|
||||
setIsLoading(true);
|
||||
const error = await login(email, password);
|
||||
setIsLoading(false);
|
||||
|
||||
if (error) {
|
||||
setLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="email"
|
||||
valid={emailError === ""}
|
||||
title="Email"
|
||||
placeholder="Enter Email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{emailError && <ErrorBanner heading={emailError} />}
|
||||
<div className="mb-6">
|
||||
<PasswordInput
|
||||
title="Password"
|
||||
placeholder="Enter Password"
|
||||
valid={passwordError === ""}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <ErrorBanner heading={passwordError} />}
|
||||
<div className="flex flex-col items-left space-y-4">
|
||||
<InlineLink href="/auth/forgot_password">
|
||||
Forgot password?
|
||||
</InlineLink>
|
||||
<Button onClick={handleClick} disabled={isLoading}>
|
||||
<div className="flex items-center justify-center">
|
||||
{isLoading && (
|
||||
<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"}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{loginError && <ErrorBanner heading={loginError} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
// page.tsx
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
||||
const ComingSoonPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Training Manuals - Coming Soon</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Our training manuals page is coming soon. Stay tuned for updates!"
|
||||
/>
|
||||
</Head>
|
||||
<div className="min-h-screen bg-gradient-to-r from-purple-600 to-blue-500 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-white mb-4">
|
||||
Training Manuals
|
||||
</h1>
|
||||
<p className="text-xl text-white mb-8">
|
||||
Our training manuals page is under construction.
|
||||
</p>
|
||||
<p className="text-lg text-white">
|
||||
Stay tuned for updates!
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<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">
|
||||
Notify Me
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComingSoonPage;
|
||||
// page.tsx
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
||||
const ComingSoonPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Training Manuals - Coming Soon</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Our training manuals page is coming soon. Stay tuned for updates!"
|
||||
/>
|
||||
</Head>
|
||||
<div className="min-h-screen bg-gradient-to-r from-purple-600 to-blue-500 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-white mb-4">
|
||||
Training Manuals
|
||||
</h1>
|
||||
<p className="text-xl text-white mb-8">
|
||||
Our training manuals page is under construction.
|
||||
</p>
|
||||
<p className="text-lg text-white">
|
||||
Stay tuned for updates!
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<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">
|
||||
Notify Me
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComingSoonPage;
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import { FunctionComponent, ReactNode } from "react";
|
||||
|
||||
type ButtonProps = {
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const Button: FunctionComponent<ButtonProps> = ({
|
||||
children,
|
||||
type,
|
||||
disabled,
|
||||
onClick,
|
||||
}) => {
|
||||
const buttonClassName = `inline-flex items-center justify-center rounded border ${
|
||||
disabled
|
||||
? "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"
|
||||
} px-4 py-2 text-md font-semibold w-full sm:w-auto`;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={buttonClassName}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
|
||||
type ButtonProps = {
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const Button: FunctionComponent<ButtonProps> = ({
|
||||
children,
|
||||
type,
|
||||
disabled,
|
||||
onClick,
|
||||
}) => {
|
||||
const buttonClassName = `inline-flex items-center justify-center rounded border ${
|
||||
disabled
|
||||
? "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"
|
||||
} px-4 py-2 text-md font-semibold w-full sm:w-auto`;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={buttonClassName}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
|
|
@ -1,247 +1,247 @@
|
|||
import { FunctionComponent, ReactNode } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
StarIcon as SolidStarIcon,
|
||||
EnvelopeIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import {
|
||||
ArrowsPointingOutIcon,
|
||||
ArrowsPointingInIcon,
|
||||
StarIcon as OutlineStarIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
type DrawerProps = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
||||
disabled?: boolean;
|
||||
editableContent?: any;
|
||||
onSave?: (content: any) => void;
|
||||
rowContent?: any;
|
||||
onRowUpdate?: (content: any) => void;
|
||||
};
|
||||
|
||||
interface EditContent {
|
||||
content: string;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
||||
title,
|
||||
children,
|
||||
onSave,
|
||||
editableContent,
|
||||
rowContent,
|
||||
onRowUpdate,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isFull, setIsFull] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
||||
|
||||
const handleTempRowContentChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
console.log(name);
|
||||
console.log(value);
|
||||
setTempRowContent((prevContent) => ({
|
||||
...prevContent,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Update the rowContent with the temporaryRowContent
|
||||
if (onRowUpdate) {
|
||||
onRowUpdate(tempRowContent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isFull) {
|
||||
setIsFull(!isFull);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
||||
|
||||
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 ${
|
||||
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
|
||||
} ${isFull ? "w-full" : "w-1/2"}`;
|
||||
|
||||
const iconComponent = isFull ? (
|
||||
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const favoriteIcon = isFavorite ? (
|
||||
<SolidStarIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<OutlineStarIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
||||
"domestic",
|
||||
"community",
|
||||
"economic",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
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"
|
||||
}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
<div className={drawerClassName}></div>
|
||||
<div className={drawerClassName}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex flex-row items-center justify-between space-x-2">
|
||||
<span className="h-5 text-purple-200 w-5">
|
||||
<UserIcon />
|
||||
</span>
|
||||
<h2 className="text-lg text-gray-800 font-semibold">
|
||||
{rowContent.username}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={toggleFavorite}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{favoriteIcon}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawerFullScreen}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawer}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<table className="p-4">
|
||||
<tbody className="items-center">
|
||||
<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">
|
||||
<td>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Username</td>
|
||||
</div>
|
||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={tempRowContent.username}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Role</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.role}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<EnvelopeIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Email</td>
|
||||
</div>
|
||||
<td className="w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
value={tempRowContent.email}
|
||||
onChange={handleTempRowContentChange}
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Type of Program</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
{/* {rowContent.program} */}
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.program}
|
||||
presetOptions={rolePresetOptions}
|
||||
setPresetOptions={setRolePresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
StarIcon as SolidStarIcon,
|
||||
EnvelopeIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import {
|
||||
ArrowsPointingOutIcon,
|
||||
ArrowsPointingInIcon,
|
||||
StarIcon as OutlineStarIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
type DrawerProps = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
||||
disabled?: boolean;
|
||||
editableContent?: any;
|
||||
onSave?: (content: any) => void;
|
||||
rowContent?: any;
|
||||
onRowUpdate?: (content: any) => void;
|
||||
};
|
||||
|
||||
interface EditContent {
|
||||
content: string;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
||||
title,
|
||||
children,
|
||||
onSave,
|
||||
editableContent,
|
||||
rowContent,
|
||||
onRowUpdate,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isFull, setIsFull] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
||||
|
||||
const handleTempRowContentChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
console.log(name);
|
||||
console.log(value);
|
||||
setTempRowContent((prevContent) => ({
|
||||
...prevContent,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Update the rowContent with the temporaryRowContent
|
||||
if (onRowUpdate) {
|
||||
onRowUpdate(tempRowContent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isFull) {
|
||||
setIsFull(!isFull);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
||||
|
||||
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 ${
|
||||
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
|
||||
} ${isFull ? "w-full" : "w-1/2"}`;
|
||||
|
||||
const iconComponent = isFull ? (
|
||||
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const favoriteIcon = isFavorite ? (
|
||||
<SolidStarIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<OutlineStarIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
||||
"domestic",
|
||||
"community",
|
||||
"economic",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
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"
|
||||
}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
<div className={drawerClassName}></div>
|
||||
<div className={drawerClassName}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex flex-row items-center justify-between space-x-2">
|
||||
<span className="h-5 text-purple-200 w-5">
|
||||
<UserIcon />
|
||||
</span>
|
||||
<h2 className="text-lg text-gray-800 font-semibold">
|
||||
{rowContent.username}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={toggleFavorite}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{favoriteIcon}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawerFullScreen}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawer}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<table className="p-4">
|
||||
<tbody className="items-center">
|
||||
<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">
|
||||
<td>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Username</td>
|
||||
</div>
|
||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={tempRowContent.username}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Role</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.role}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<EnvelopeIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Email</td>
|
||||
</div>
|
||||
<td className="w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
value={tempRowContent.email}
|
||||
onChange={handleTempRowContentChange}
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Type of Program</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
{/* {rowContent.program} */}
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.program}
|
||||
presetOptions={rolePresetOptions}
|
||||
setPresetOptions={setRolePresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import { useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
const mockTags = ["food relief", "period poverty", "nutrition education"];
|
||||
|
||||
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
|
||||
|
||||
export const ContainsDropdown = ({
|
||||
isDropdownOpen,
|
||||
setIsDropdownOpen,
|
||||
filterType,
|
||||
setFilterType,
|
||||
}) => {
|
||||
const handleFilterTypeChange = (type: FilterType) => {
|
||||
setFilterType(type);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`absolute z-10 mt-8 -top-28 bg-white border border-gray-300 rounded-md shadow-md p-2 ${
|
||||
isDropdownOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("contains")}
|
||||
>
|
||||
Contains
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("does not contain")}
|
||||
>
|
||||
Does not contain
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("is empty")}
|
||||
>
|
||||
Is empty
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("is not empty")}
|
||||
>
|
||||
Is not empty
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
const mockTags = ["food relief", "period poverty", "nutrition education"];
|
||||
|
||||
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
|
||||
|
||||
export const ContainsDropdown = ({
|
||||
isDropdownOpen,
|
||||
setIsDropdownOpen,
|
||||
filterType,
|
||||
setFilterType,
|
||||
}) => {
|
||||
const handleFilterTypeChange = (type: FilterType) => {
|
||||
setFilterType(type);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`absolute z-10 mt-8 -top-28 bg-white border border-gray-300 rounded-md shadow-md p-2 ${
|
||||
isDropdownOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("contains")}
|
||||
>
|
||||
Contains
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("does not contain")}
|
||||
>
|
||||
Does not contain
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("is empty")}
|
||||
>
|
||||
Is empty
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-100 rounded"
|
||||
onClick={() => handleFilterTypeChange("is not empty")}
|
||||
>
|
||||
Is not empty
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,95 +1,95 @@
|
|||
// FilterBox.tsx
|
||||
import { useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { ContainsDropdown } from "./ContainsDropdown";
|
||||
|
||||
const mockTags = ["food relief", "period poverty", "nutrition education"];
|
||||
|
||||
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
|
||||
|
||||
export const FilterBox = () => {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showContainsDropdown, setShowContainsDropdown] = useState(false);
|
||||
const [filterType, setFilterType] = useState<FilterType>("contains");
|
||||
|
||||
const handleTagChange = (tag: string) => {
|
||||
setSelectedTags((prevTags) =>
|
||||
prevTags.includes(tag)
|
||||
? prevTags.filter((t) => t !== tag)
|
||||
: [...prevTags, tag]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
|
||||
const renderSelectedTags = () =>
|
||||
selectedTags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="bg-purple-100 text-purple-800 px-2 py-1 rounded-md flex items-center mr-2"
|
||||
>
|
||||
<span>{tag}</span>
|
||||
<span
|
||||
className="ml-2 cursor-pointer"
|
||||
onClick={() => handleTagChange(tag)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
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="mb-2">
|
||||
<span className="font-semibold">
|
||||
Tags{" "}
|
||||
<button
|
||||
onClick={() =>
|
||||
setShowContainsDropdown((prevState) => !prevState)
|
||||
}
|
||||
className="hover:bg-gray-50 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{filterType} <ChevronDownIcon className="inline h-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap mb-2 px-2 py-1 border border-gray-300 rounded w-full">
|
||||
{selectedTags.length > 0 && renderSelectedTags()}
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Search tags..."
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{mockTags
|
||||
.filter((tag) =>
|
||||
tag.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
.map((tag) => (
|
||||
<div key={tag} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.includes(tag)}
|
||||
onChange={() => handleTagChange(tag)}
|
||||
className="mr-2 accent-purple-500"
|
||||
/>
|
||||
<label>{tag}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showContainsDropdown && (
|
||||
<ContainsDropdown
|
||||
isDropdownOpen={showContainsDropdown}
|
||||
setIsDropdownOpen={setShowContainsDropdown}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// FilterBox.tsx
|
||||
import { useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { ContainsDropdown } from "./ContainsDropdown";
|
||||
|
||||
const mockTags = ["food relief", "period poverty", "nutrition education"];
|
||||
|
||||
type FilterType = "contains" | "does not contain" | "is empty" | "is not empty";
|
||||
|
||||
export const FilterBox = () => {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showContainsDropdown, setShowContainsDropdown] = useState(false);
|
||||
const [filterType, setFilterType] = useState<FilterType>("contains");
|
||||
|
||||
const handleTagChange = (tag: string) => {
|
||||
setSelectedTags((prevTags) =>
|
||||
prevTags.includes(tag)
|
||||
? prevTags.filter((t) => t !== tag)
|
||||
: [...prevTags, tag]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
|
||||
const renderSelectedTags = () =>
|
||||
selectedTags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="bg-purple-100 text-purple-800 px-2 py-1 rounded-md flex items-center mr-2"
|
||||
>
|
||||
<span>{tag}</span>
|
||||
<span
|
||||
className="ml-2 cursor-pointer"
|
||||
onClick={() => handleTagChange(tag)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
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="mb-2">
|
||||
<span className="font-semibold">
|
||||
Tags{" "}
|
||||
<button
|
||||
onClick={() =>
|
||||
setShowContainsDropdown((prevState) => !prevState)
|
||||
}
|
||||
className="hover:bg-gray-50 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{filterType} <ChevronDownIcon className="inline h-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap mb-2 px-2 py-1 border border-gray-300 rounded w-full">
|
||||
{selectedTags.length > 0 && renderSelectedTags()}
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Search tags..."
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{mockTags
|
||||
.filter((tag) =>
|
||||
tag.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
.map((tag) => (
|
||||
<div key={tag} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.includes(tag)}
|
||||
onChange={() => handleTagChange(tag)}
|
||||
className="mr-2 accent-purple-500"
|
||||
/>
|
||||
<label>{tag}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showContainsDropdown && (
|
||||
<ContainsDropdown
|
||||
isDropdownOpen={showContainsDropdown}
|
||||
setIsDropdownOpen={setShowContainsDropdown}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
interface Link {
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const InlineLink: React.FC<Link> = ({ href = "#", children, onClick }) => {
|
||||
return (
|
||||
<a
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
className="text-sm text-purple-600 hover:underline font-semibold"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineLink;
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface Link {
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const InlineLink: React.FC<Link> = ({ href = "#", children, onClick }) => {
|
||||
return (
|
||||
<a
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
className="text-sm text-purple-600 hover:underline font-semibold"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineLink;
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
import React, {
|
||||
FunctionComponent,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
ChangeEvent,
|
||||
} from "react";
|
||||
|
||||
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
icon?: ReactNode;
|
||||
title?: ReactNode;
|
||||
type?: ReactNode;
|
||||
placeholder?: ReactNode;
|
||||
valid?: boolean;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const Input: FunctionComponent<InputProps> = ({
|
||||
icon,
|
||||
type,
|
||||
title,
|
||||
placeholder,
|
||||
onChange,
|
||||
valid = true,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={title}
|
||||
className={
|
||||
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-red-600 focus-within:ring-1 focus-within:ring-red-600"
|
||||
}
|
||||
>
|
||||
<span className="text-xs font-semibold text-gray-700">
|
||||
{" "}
|
||||
{title}{" "}
|
||||
</span>
|
||||
<div className="mt-1 flex items-center">
|
||||
<input
|
||||
type={type}
|
||||
id={title}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
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">
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
ChangeEvent,
|
||||
} from "react";
|
||||
|
||||
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
icon?: ReactNode;
|
||||
title?: ReactNode;
|
||||
type?: ReactNode;
|
||||
placeholder?: ReactNode;
|
||||
valid?: boolean;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const Input: FunctionComponent<InputProps> = ({
|
||||
icon,
|
||||
type,
|
||||
title,
|
||||
placeholder,
|
||||
onChange,
|
||||
valid = true,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={title}
|
||||
className={
|
||||
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-red-600 focus-within:ring-1 focus-within:ring-red-600"
|
||||
}
|
||||
>
|
||||
<span className="text-xs font-semibold text-gray-700">
|
||||
{" "}
|
||||
{title}{" "}
|
||||
</span>
|
||||
<div className="mt-1 flex items-center">
|
||||
<input
|
||||
type={type}
|
||||
id={title}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
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">
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
interface PageLayoutProps {
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export const PageLayout: React.FC<PageLayoutProps> = ({
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<span className="w-6 h-6 text-purple-200">{icon}</span>
|
||||
<h1 className="font-bold text-2xl text-purple-800">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{/* data */}
|
||||
<div className="px-8 py-8">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
interface PageLayoutProps {
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export const PageLayout: React.FC<PageLayoutProps> = ({
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* icon + title */}
|
||||
<div className="pt-16 px-8 pb-4 flex-row">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<span className="w-6 h-6 text-purple-200">{icon}</span>
|
||||
<h1 className="font-bold text-2xl text-purple-800">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{/* data */}
|
||||
<div className="px-8 py-8">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,89 +1,89 @@
|
|||
import React from "react";
|
||||
import {
|
||||
HomeIcon,
|
||||
ChevronDoubleLeftIcon,
|
||||
BookmarkIcon,
|
||||
ClipboardIcon,
|
||||
BookOpenIcon,
|
||||
LockClosedIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { SidebarItem } from "./SidebarItem";
|
||||
import { UserProfile } from "../resource/UserProfile";
|
||||
|
||||
interface SidebarProps {
|
||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
name: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
setIsSidebarOpen,
|
||||
name,
|
||||
email,
|
||||
isAdmin: admin,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
|
||||
{/* button to close sidebar */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-8">
|
||||
{/* user + logout button */}
|
||||
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
||||
<UserProfile name={name} email={email} />
|
||||
</div>
|
||||
{/* navigation menu */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="text-xs font-semibold text-gray-500">
|
||||
Pages
|
||||
</h4>
|
||||
<nav className="flex flex-col">
|
||||
{admin && (
|
||||
<SidebarItem
|
||||
icon={<LockClosedIcon />}
|
||||
text="Admin"
|
||||
active={true}
|
||||
redirect="/admin"
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarItem
|
||||
icon={<HomeIcon />}
|
||||
text="Home"
|
||||
active={true}
|
||||
redirect="/home"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<BookmarkIcon />}
|
||||
text="Resources"
|
||||
active={true}
|
||||
redirect="/resource"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<ClipboardIcon />}
|
||||
text="Services"
|
||||
active={true}
|
||||
redirect="/service"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<BookOpenIcon />}
|
||||
text="Training Manuals"
|
||||
active={true}
|
||||
redirect="/training-manuals"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
import React from "react";
|
||||
import {
|
||||
HomeIcon,
|
||||
ChevronDoubleLeftIcon,
|
||||
BookmarkIcon,
|
||||
ClipboardIcon,
|
||||
BookOpenIcon,
|
||||
LockClosedIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { SidebarItem } from "./SidebarItem";
|
||||
import { UserProfile } from "../resource/UserProfile";
|
||||
|
||||
interface SidebarProps {
|
||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
name: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
setIsSidebarOpen,
|
||||
name,
|
||||
email,
|
||||
isAdmin: admin,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
|
||||
{/* button to close sidebar */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-8">
|
||||
{/* user + logout button */}
|
||||
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
||||
<UserProfile name={name} email={email} />
|
||||
</div>
|
||||
{/* navigation menu */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="text-xs font-semibold text-gray-500">
|
||||
Pages
|
||||
</h4>
|
||||
<nav className="flex flex-col">
|
||||
{admin && (
|
||||
<SidebarItem
|
||||
icon={<LockClosedIcon />}
|
||||
text="Admin"
|
||||
active={true}
|
||||
redirect="/admin"
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarItem
|
||||
icon={<HomeIcon />}
|
||||
text="Home"
|
||||
active={true}
|
||||
redirect="/home"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<BookmarkIcon />}
|
||||
text="Resources"
|
||||
active={true}
|
||||
redirect="/resource"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<ClipboardIcon />}
|
||||
text="Services"
|
||||
active={true}
|
||||
redirect="/service"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={<BookOpenIcon />}
|
||||
text="Training Manuals"
|
||||
active={true}
|
||||
redirect="/training-manuals"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import Link from "next/link";
|
||||
|
||||
interface SidebarItemProps {
|
||||
icon: React.ReactElement;
|
||||
text: string;
|
||||
active: boolean;
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
icon,
|
||||
text,
|
||||
active,
|
||||
redirect,
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
href={redirect}
|
||||
className={
|
||||
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 hover:bg-gray-200 rounded-md"
|
||||
}
|
||||
>
|
||||
<span className="h-5 text-gray-500 w-5">{icon}</span>
|
||||
<span className="flex-grow font-medium text-xs text-gray-500">
|
||||
{text}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
import Link from "next/link";
|
||||
|
||||
interface SidebarItemProps {
|
||||
icon: React.ReactElement;
|
||||
text: string;
|
||||
active: boolean;
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
icon,
|
||||
text,
|
||||
active,
|
||||
redirect,
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
href={redirect}
|
||||
className={
|
||||
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 hover:bg-gray-200 rounded-md"
|
||||
}
|
||||
>
|
||||
<span className="h-5 text-gray-500 w-5">{icon}</span>
|
||||
<span className="flex-grow font-medium text-xs text-gray-500">
|
||||
{text}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,306 +1,306 @@
|
|||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import User from "@/utils/models/User";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
export const Table = ({ users }: { users: User[] }) => {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => deleteUser(props.row.original.id)}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("username", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Username
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("role", {
|
||||
header: () => (
|
||||
<>
|
||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||
Role
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={info.getValue()}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: () => (
|
||||
<>
|
||||
<AtSymbolIcon className="inline align-top h-4" /> Email
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
|
||||
{info.getValue()}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||
Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<User[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: User) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import User from "@/utils/models/User";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
export const Table = ({ users }: { users: User[] }) => {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => deleteUser(props.row.original.id)}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("username", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Username
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("role", {
|
||||
header: () => (
|
||||
<>
|
||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||
Role
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={info.getValue()}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: () => (
|
||||
<>
|
||||
<AtSymbolIcon className="inline align-top h-4" /> Email
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
|
||||
{info.getValue()}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||
Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<User[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: User) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
/* An extension of TableCell.tsx that includes an "open" button and the drawer.
|
||||
For cells in the "primary" (or first) column of the table. */
|
||||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import { TableCell } from "./TableCell";
|
||||
import { SetStateAction, useState } from "react";
|
||||
|
||||
export const PrimaryTableCell = ({ getValue, row, column, table }) => {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent: SetStateAction<string>) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group">
|
||||
<TableCell
|
||||
getValue={getValue}
|
||||
row={row}
|
||||
column={column}
|
||||
table={table}
|
||||
/>
|
||||
<span className="absolute right-1 top-1">
|
||||
<Drawer
|
||||
title={getValue()}
|
||||
editableContent={pageContent}
|
||||
onSave={handleDrawerContentChange}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/* An extension of TableCell.tsx that includes an "open" button and the drawer.
|
||||
For cells in the "primary" (or first) column of the table. */
|
||||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import { TableCell } from "./TableCell";
|
||||
import { SetStateAction, useState } from "react";
|
||||
|
||||
export const PrimaryTableCell = ({ getValue, row, column, table }) => {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent: SetStateAction<string>) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group">
|
||||
<TableCell
|
||||
getValue={getValue}
|
||||
row={row}
|
||||
column={column}
|
||||
table={table}
|
||||
/>
|
||||
<span className="absolute right-1 top-1">
|
||||
<Drawer
|
||||
title={getValue()}
|
||||
editableContent={pageContent}
|
||||
onSave={handleDrawerContentChange}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,304 +1,304 @@
|
|||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import Resource from "@/utils/models/Resource";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
// TODO: Rename everything to resources
|
||||
export const ResourceTable = ({ users }: { users: Resource[] }) => {
|
||||
const columnHelper = createColumnHelper<Resource>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => {}}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("link", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Link
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<a
|
||||
href={info.getValue()}
|
||||
target={"_blank"}
|
||||
className="ml-2 text-gray-500 underline hover:text-gray-400"
|
||||
>
|
||||
{info.getValue()}
|
||||
</a>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<Resource[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: Resource) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import Resource from "@/utils/models/Resource";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
// TODO: Rename everything to resources
|
||||
export const ResourceTable = ({ users }: { users: Resource[] }) => {
|
||||
const columnHelper = createColumnHelper<Resource>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => {}}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("link", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Link
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<a
|
||||
href={info.getValue()}
|
||||
target={"_blank"}
|
||||
className="ml-2 text-gray-500 underline hover:text-gray-400"
|
||||
>
|
||||
{info.getValue()}
|
||||
</a>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<Resource[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: Resource) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||
{title}
|
||||
<span>
|
||||
{/* Added OnRowUpdate to drawer */}
|
||||
<Drawer
|
||||
title="My Drawer Title"
|
||||
editableContent={pageContent}
|
||||
rowContent={rowData}
|
||||
onSave={handleDrawerContentChange}
|
||||
onRowUpdate={onRowUpdate}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||
{title}
|
||||
<span>
|
||||
{/* Added OnRowUpdate to drawer */}
|
||||
<Drawer
|
||||
title="My Drawer Title"
|
||||
editableContent={pageContent}
|
||||
rowContent={rowData}
|
||||
onSave={handleDrawerContentChange}
|
||||
onRowUpdate={onRowUpdate}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import React from "react";
|
||||
import {
|
||||
TrashIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ArrowUpRightIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
export const RowOption = ({ icon: Icon, label, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="hover:bg-gray-100 flex items-center gap-2 p-2 w-full"
|
||||
>
|
||||
<Icon className="inline h-4" /> {label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
import React from "react";
|
||||
import {
|
||||
TrashIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ArrowUpRightIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
export const RowOption = ({ icon: Icon, label, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="hover:bg-gray-100 flex items-center gap-2 p-2 w-full"
|
||||
>
|
||||
<Icon className="inline h-4" /> {label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
//delete, duplicate, open
|
||||
import {
|
||||
TrashIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ArrowUpRightIcon,
|
||||
EllipsisVerticalIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Button from "../Button";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { RowOption } from "./RowOption";
|
||||
|
||||
export const RowOptionMenu = ({ onDelete, onHide }) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const openMenu = () => setMenuOpen(true);
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
|
||||
// TODO: Hide menu if clicked elsewhere
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="items-end"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
</button>
|
||||
<div
|
||||
className={
|
||||
"justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" +
|
||||
(!menuOpen ? " invisible" : "")
|
||||
}
|
||||
>
|
||||
<RowOption icon={TrashIcon} label="Delete" onClick={onDelete} />
|
||||
<RowOption
|
||||
icon={ArrowUpRightIcon}
|
||||
label="Open"
|
||||
onClick={() => {
|
||||
/* handle open */
|
||||
}}
|
||||
/>
|
||||
<RowOption icon={EyeSlashIcon} label="Hide" onClick={onHide} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
//delete, duplicate, open
|
||||
import {
|
||||
TrashIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ArrowUpRightIcon,
|
||||
EllipsisVerticalIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Button from "../Button";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { RowOption } from "./RowOption";
|
||||
|
||||
export const RowOptionMenu = ({ onDelete, onHide }) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const openMenu = () => setMenuOpen(true);
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
|
||||
// TODO: Hide menu if clicked elsewhere
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="items-end"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
</button>
|
||||
<div
|
||||
className={
|
||||
"justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" +
|
||||
(!menuOpen ? " invisible" : "")
|
||||
}
|
||||
>
|
||||
<RowOption icon={TrashIcon} label="Delete" onClick={onDelete} />
|
||||
<RowOption
|
||||
icon={ArrowUpRightIcon}
|
||||
label="Open"
|
||||
onClick={() => {
|
||||
/* handle open */
|
||||
}}
|
||||
/>
|
||||
<RowOption icon={EyeSlashIcon} label="Hide" onClick={onHide} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,312 +1,312 @@
|
|||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import Service from "@/utils/models/Service";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
// TODO: Rename everything to service
|
||||
export const ServiceTable = ({ users }: { users: Service[] }) => {
|
||||
const columnHelper = createColumnHelper<Service>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => {}}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Status
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("requirements", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={
|
||||
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<Service[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: Service) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// for showcasing to compass
|
||||
|
||||
import users from "./users.json";
|
||||
import {
|
||||
Cell,
|
||||
ColumnDef,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
sortingFns,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
FunctionComponent,
|
||||
useRef,
|
||||
ChangeEventHandler,
|
||||
Key,
|
||||
} from "react";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import { RowOpenAction } from "./RowOpenAction";
|
||||
import { TableAction } from "./TableAction";
|
||||
import {
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
ArrowDownCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import Service from "@/utils/models/Service";
|
||||
|
||||
// For search
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
// TODO: Rename everything to service
|
||||
export const ServiceTable = ({ users }: { users: Service[] }) => {
|
||||
const columnHelper = createColumnHelper<Service>();
|
||||
|
||||
useEffect(() => {
|
||||
const sortedUsers = [...users].sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
setData(sortedUsers);
|
||||
}, [users]);
|
||||
|
||||
const deleteUser = (userId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((user) => user.id !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideUser = (userId: number) => {
|
||||
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return { ...user, visible: !user.visible };
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.sort((a, b) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||
);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => {}}
|
||||
onHide={() => hideUser(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
onRowUpdate={handleRowUpdate}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Status
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("requirements", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={
|
||||
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
const [data, setData] = useState<Service[]>([...users]);
|
||||
|
||||
const addUser = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// added this fn for editing rows
|
||||
const handleRowUpdate = (updatedRow: Service) => {
|
||||
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||
if (dataIndex !== -1) {
|
||||
const updatedData = [...data];
|
||||
updatedData[dataIndex] = updatedRow;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isUserVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addUser}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,69 +1,69 @@
|
|||
// TableAction.tsx
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
||||
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
|
||||
import { FilterBox } from "../FilterBox";
|
||||
|
||||
type TableActionProps = {
|
||||
query: string;
|
||||
handleChange: ChangeEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
export const TableAction: FunctionComponent<TableActionProps> = ({
|
||||
query,
|
||||
handleChange,
|
||||
}) => {
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const [showFilterBox, setShowFilterBox] = useState(false);
|
||||
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
if (searchInput.current === null) {
|
||||
return;
|
||||
}
|
||||
searchInput.current.focus();
|
||||
searchInput.current.addEventListener("focusout", () => {
|
||||
if (searchInput.current?.value.trim() === "") {
|
||||
searchInput.current.value = "";
|
||||
deactivateSearch();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deactivateSearch = () => setSearchActive(false);
|
||||
|
||||
const toggleFilterBox = () => setShowFilterBox((prev) => !prev);
|
||||
|
||||
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">
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50"
|
||||
onClick={toggleFilterBox}
|
||||
>
|
||||
Filter
|
||||
</span>
|
||||
{showFilterBox && <FilterBox />}
|
||||
<span className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100">
|
||||
Sort
|
||||
</span>
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100"
|
||||
onClick={activateSearch}
|
||||
>
|
||||
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
||||
</span>
|
||||
<input
|
||||
ref={searchInput}
|
||||
className={
|
||||
"outline-none transition-all duration-300 " +
|
||||
(searchActive ? "w-48" : "w-0")
|
||||
}
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Type to search..."
|
||||
value={query ?? ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// TableAction.tsx
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
|
||||
import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react";
|
||||
import { FilterBox } from "../FilterBox";
|
||||
|
||||
type TableActionProps = {
|
||||
query: string;
|
||||
handleChange: ChangeEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
export const TableAction: FunctionComponent<TableActionProps> = ({
|
||||
query,
|
||||
handleChange,
|
||||
}) => {
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const [showFilterBox, setShowFilterBox] = useState(false);
|
||||
|
||||
const activateSearch = () => {
|
||||
setSearchActive(true);
|
||||
if (searchInput.current === null) {
|
||||
return;
|
||||
}
|
||||
searchInput.current.focus();
|
||||
searchInput.current.addEventListener("focusout", () => {
|
||||
if (searchInput.current?.value.trim() === "") {
|
||||
searchInput.current.value = "";
|
||||
deactivateSearch();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deactivateSearch = () => setSearchActive(false);
|
||||
|
||||
const toggleFilterBox = () => setShowFilterBox((prev) => !prev);
|
||||
|
||||
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">
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50"
|
||||
onClick={toggleFilterBox}
|
||||
>
|
||||
Filter
|
||||
</span>
|
||||
{showFilterBox && <FilterBox />}
|
||||
<span className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100">
|
||||
Sort
|
||||
</span>
|
||||
<span
|
||||
className="p-1 rounded hover:text-purple-700 focus:bg-purple-50 hover:bg-purple-50 hover:bg-gray-100"
|
||||
onClick={activateSearch}
|
||||
>
|
||||
<MagnifyingGlassIcon className="w-4 h-4 inline" />
|
||||
</span>
|
||||
<input
|
||||
ref={searchInput}
|
||||
className={
|
||||
"outline-none transition-all duration-300 " +
|
||||
(searchActive ? "w-48" : "w-0")
|
||||
}
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Type to search..."
|
||||
value={query ?? ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
/* A lone table cell. Passed in for "cell" for a TanStack Table. */
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const TableCell = ({ getValue, row, column, table }) => {
|
||||
const initialValue = getValue();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const onBlur = () => {
|
||||
table.options.meta?.updateData(row.index, column.id, value);
|
||||
};
|
||||
// focus:border focus:border-gray-200
|
||||
const className =
|
||||
"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:w-[calc(100%+0.5rem)]";
|
||||
|
||||
return (
|
||||
<input
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
};
|
||||
/* A lone table cell. Passed in for "cell" for a TanStack Table. */
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const TableCell = ({ getValue, row, column, table }) => {
|
||||
const initialValue = getValue();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const onBlur = () => {
|
||||
table.options.meta?.updateData(row.index, column.id, value);
|
||||
};
|
||||
// focus:border focus:border-gray-200
|
||||
const className =
|
||||
"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:w-[calc(100%+0.5rem)]";
|
||||
|
||||
return (
|
||||
<input
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,222 +1,222 @@
|
|||
[
|
||||
{
|
||||
"id": 0,
|
||||
"created_at": 1711482132230,
|
||||
"username": "Bo_Pfeffer",
|
||||
"role": "ADMIN",
|
||||
"email": "Bo.Pfeffer@gmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Marianna_Heathcote76",
|
||||
"role": "ADMIN",
|
||||
"email": "Marianna_Heathcote14@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Queenie_Schroeder",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Queenie_Schroeder@yahoo.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Arne.Bode",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Arne.Bode@hotmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Maia.Zulauf9",
|
||||
"role": "ADMIN",
|
||||
"email": "Maia_Zulauf@gmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"created_at": 1711482132231,
|
||||
"username": "River_Bauch",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "River.Bauch@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Virgil.Hilll",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Virgil.Hilll@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Bridget_Cartwright",
|
||||
"role": "ADMIN",
|
||||
"email": "Bridget_Cartwright@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Glennie_Keebler64",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Glennie_Keebler60@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Orin.Jenkins53",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Orin.Jenkins@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Zachery.Rosenbaum",
|
||||
"role": "ADMIN",
|
||||
"email": "Zachery.Rosenbaum@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Phoebe.Ziemann",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Phoebe_Ziemann92@gmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Bradford_Conroy53",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Bradford_Conroy94@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Florine_Strosin55",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Florine.Strosin29@hotmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Constance.Doyle59",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Constance_Doyle@hotmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Chauncey_Lockman",
|
||||
"role": "ADMIN",
|
||||
"email": "Chauncey_Lockman@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Esther_Wuckert-Larson26",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Esther_Wuckert-Larson@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 0,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Jewel.Kunde",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Jewel_Kunde29@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Hildegard_Parker92",
|
||||
"role": "ADMIN",
|
||||
"email": "Hildegard_Parker74@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Jordane.Lakin2",
|
||||
"role": "ADMIN",
|
||||
"email": "Jordane_Lakin@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"created_at": 1711482132230,
|
||||
"username": "Bo_Pfeffer",
|
||||
"role": "ADMIN",
|
||||
"email": "Bo.Pfeffer@gmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Marianna_Heathcote76",
|
||||
"role": "ADMIN",
|
||||
"email": "Marianna_Heathcote14@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Queenie_Schroeder",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Queenie_Schroeder@yahoo.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Arne.Bode",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Arne.Bode@hotmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Maia.Zulauf9",
|
||||
"role": "ADMIN",
|
||||
"email": "Maia_Zulauf@gmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"created_at": 1711482132231,
|
||||
"username": "River_Bauch",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "River.Bauch@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Virgil.Hilll",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Virgil.Hilll@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Bridget_Cartwright",
|
||||
"role": "ADMIN",
|
||||
"email": "Bridget_Cartwright@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"created_at": 1711482132231,
|
||||
"username": "Glennie_Keebler64",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Glennie_Keebler60@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Orin.Jenkins53",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Orin.Jenkins@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Zachery.Rosenbaum",
|
||||
"role": "ADMIN",
|
||||
"email": "Zachery.Rosenbaum@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Phoebe.Ziemann",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Phoebe_Ziemann92@gmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Bradford_Conroy53",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Bradford_Conroy94@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Florine_Strosin55",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Florine.Strosin29@hotmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Constance.Doyle59",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Constance_Doyle@hotmail.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 3,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Chauncey_Lockman",
|
||||
"role": "ADMIN",
|
||||
"email": "Chauncey_Lockman@yahoo.com",
|
||||
"program": "DOMESTIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Esther_Wuckert-Larson26",
|
||||
"role": "EMPLOYEE",
|
||||
"email": "Esther_Wuckert-Larson@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 0,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Jewel.Kunde",
|
||||
"role": "VOLUNTEER",
|
||||
"email": "Jewel_Kunde29@gmail.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 5,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Hildegard_Parker92",
|
||||
"role": "ADMIN",
|
||||
"email": "Hildegard_Parker74@yahoo.com",
|
||||
"program": "ECONOMIC",
|
||||
"experience": 2,
|
||||
"group": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"created_at": 1711482132232,
|
||||
"username": "Jordane.Lakin2",
|
||||
"role": "ADMIN",
|
||||
"email": "Jordane_Lakin@hotmail.com",
|
||||
"program": "COMMUNITY",
|
||||
"experience": 1,
|
||||
"group": "",
|
||||
"visible": true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Tag } from "./Tag";
|
||||
|
||||
export const CreateNewTagAction = ({ input }) => {
|
||||
return (
|
||||
<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>
|
||||
<Tag active={false} onDelete={null}>
|
||||
{input}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Tag } from "./Tag";
|
||||
|
||||
export const CreateNewTagAction = ({ input }) => {
|
||||
return (
|
||||
<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>
|
||||
<Tag active={false} onDelete={null}>
|
||||
{input}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(tag);
|
||||
|
||||
const editTagOption = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleEditTag(tag, inputValue);
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EllipsisHorizontalIcon
|
||||
className="w-5 text-gray-500"
|
||||
onClick={() => setVisible(!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">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={editTagOption}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleDeleteTag(inputValue);
|
||||
setVisible(false);
|
||||
}}
|
||||
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" />
|
||||
<p>Delete</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(tag);
|
||||
|
||||
const editTagOption = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleEditTag(tag, inputValue);
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EllipsisHorizontalIcon
|
||||
className="w-5 text-gray-500"
|
||||
onClick={() => setVisible(!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">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={editTagOption}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleDeleteTag(inputValue);
|
||||
setVisible(false);
|
||||
}}
|
||||
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" />
|
||||
<p>Delete</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,174 +1,174 @@
|
|||
import React, { useState, useRef } from "react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import { TagsArray } from "./TagsArray";
|
||||
import { TagDropdown } from "./TagDropdown";
|
||||
import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||
|
||||
interface TagsInputProps {
|
||||
presetOptions: string[];
|
||||
presetValue: string | string[];
|
||||
setPresetOptions: () => {};
|
||||
getTagColor: () => {};
|
||||
}
|
||||
|
||||
const TagsInput: React.FC<TagsInputProps> = ({
|
||||
presetValue,
|
||||
presetOptions,
|
||||
setPresetOptions,
|
||||
getTagColor,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [cellSelected, setCellSelected] = useState(false);
|
||||
const [tags, setTags] = useState<Set<string>>(
|
||||
typeof presetValue === "string"
|
||||
? new Set([presetValue])
|
||||
: new Set(presetValue)
|
||||
);
|
||||
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
|
||||
const dropdown = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!cellSelected) {
|
||||
setCellSelected(true);
|
||||
// Add event listener only after setting cellSelected to true
|
||||
setTimeout(() => {
|
||||
window.addEventListener("click", handleOutsideClick);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Fix MouseEvent type and remove the as Node as that is completely wrong
|
||||
const handleOutsideClick = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdown.current &&
|
||||
!dropdown.current.contains(event.target as Node)
|
||||
) {
|
||||
setCellSelected(false);
|
||||
// Remove event listener after handling outside click
|
||||
window.removeEventListener("click", handleOutsideClick);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOptions(() => {
|
||||
const newOptions = presetOptions.filter((item) =>
|
||||
item.includes(e.target.value.toLowerCase())
|
||||
);
|
||||
return new Set(newOptions);
|
||||
});
|
||||
setInputValue(e.target.value); // Update input value state
|
||||
};
|
||||
|
||||
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && inputValue.trim()) {
|
||||
// setPresetOptions((prevPreset) => {
|
||||
// const uniqueSet = new Set(presetOptions);
|
||||
// uniqueSet.add(inputValue);
|
||||
// return Array.from(uniqueSet);
|
||||
// });
|
||||
setTags((prevTags) => new Set(prevTags).add(inputValue));
|
||||
setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
|
||||
setInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTag = (tagToAdd: string) => {
|
||||
if (!tags.has(tagToAdd)) {
|
||||
// Corrected syntax for checking if a Set contains an item
|
||||
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = (tagToDelete: string) => {
|
||||
setTags((prevTags) => {
|
||||
const updatedTags = new Set(prevTags);
|
||||
updatedTags.delete(tagToDelete);
|
||||
return updatedTags;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteTagOption = (tagToDelete: string) => {
|
||||
// setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete));
|
||||
setOptions((prevOptions) => {
|
||||
const updatedOptions = new Set(prevOptions);
|
||||
updatedOptions.delete(tagToDelete);
|
||||
return updatedOptions;
|
||||
});
|
||||
if (tags.has(tagToDelete)) {
|
||||
handleDeleteTag(tagToDelete);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTag = (oldTag: string, newTag: string) => {
|
||||
if (oldTag !== newTag) {
|
||||
setTags((prevTags) => {
|
||||
const tagsArray = Array.from(prevTags);
|
||||
const oldTagIndex = tagsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
tagsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(tagsArray);
|
||||
});
|
||||
|
||||
setOptions((prevOptions) => {
|
||||
const optionsArray = Array.from(prevOptions);
|
||||
const oldTagIndex = optionsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
optionsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(optionsArray);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer" onClick={handleClick}>
|
||||
{!cellSelected ? (
|
||||
<TagsArray
|
||||
active={true}
|
||||
handleDelete={handleDeleteTag}
|
||||
tags={tags}
|
||||
/>
|
||||
) : (
|
||||
<div ref={dropdown}>
|
||||
<div className="absolute w-64 z-50 ml-1 mt-5">
|
||||
<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">
|
||||
<TagsArray
|
||||
handleDelete={handleDeleteTag}
|
||||
active
|
||||
tags={tags}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for an option..."
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleAddTag}
|
||||
className="focus:outline-none bg-transparent"
|
||||
autoFocus
|
||||
/>
|
||||
</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">
|
||||
<p className="capitalize">
|
||||
Select an option or create one
|
||||
</p>
|
||||
<TagDropdown
|
||||
handleDeleteTag={handleDeleteTagOption}
|
||||
handleEditTag={handleEditTag}
|
||||
handleAdd={handleSelectTag}
|
||||
tags={options}
|
||||
/>
|
||||
{inputValue.length > 0 && (
|
||||
<CreateNewTagAction input={inputValue} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsInput;
|
||||
import React, { useState, useRef } from "react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import { TagsArray } from "./TagsArray";
|
||||
import { TagDropdown } from "./TagDropdown";
|
||||
import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||
|
||||
interface TagsInputProps {
|
||||
presetOptions: string[];
|
||||
presetValue: string | string[];
|
||||
setPresetOptions: () => {};
|
||||
getTagColor: () => {};
|
||||
}
|
||||
|
||||
const TagsInput: React.FC<TagsInputProps> = ({
|
||||
presetValue,
|
||||
presetOptions,
|
||||
setPresetOptions,
|
||||
getTagColor,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [cellSelected, setCellSelected] = useState(false);
|
||||
const [tags, setTags] = useState<Set<string>>(
|
||||
typeof presetValue === "string"
|
||||
? new Set([presetValue])
|
||||
: new Set(presetValue)
|
||||
);
|
||||
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
|
||||
const dropdown = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!cellSelected) {
|
||||
setCellSelected(true);
|
||||
// Add event listener only after setting cellSelected to true
|
||||
setTimeout(() => {
|
||||
window.addEventListener("click", handleOutsideClick);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Fix MouseEvent type and remove the as Node as that is completely wrong
|
||||
const handleOutsideClick = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdown.current &&
|
||||
!dropdown.current.contains(event.target as Node)
|
||||
) {
|
||||
setCellSelected(false);
|
||||
// Remove event listener after handling outside click
|
||||
window.removeEventListener("click", handleOutsideClick);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOptions(() => {
|
||||
const newOptions = presetOptions.filter((item) =>
|
||||
item.includes(e.target.value.toLowerCase())
|
||||
);
|
||||
return new Set(newOptions);
|
||||
});
|
||||
setInputValue(e.target.value); // Update input value state
|
||||
};
|
||||
|
||||
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && inputValue.trim()) {
|
||||
// setPresetOptions((prevPreset) => {
|
||||
// const uniqueSet = new Set(presetOptions);
|
||||
// uniqueSet.add(inputValue);
|
||||
// return Array.from(uniqueSet);
|
||||
// });
|
||||
setTags((prevTags) => new Set(prevTags).add(inputValue));
|
||||
setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
|
||||
setInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTag = (tagToAdd: string) => {
|
||||
if (!tags.has(tagToAdd)) {
|
||||
// Corrected syntax for checking if a Set contains an item
|
||||
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = (tagToDelete: string) => {
|
||||
setTags((prevTags) => {
|
||||
const updatedTags = new Set(prevTags);
|
||||
updatedTags.delete(tagToDelete);
|
||||
return updatedTags;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteTagOption = (tagToDelete: string) => {
|
||||
// setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete));
|
||||
setOptions((prevOptions) => {
|
||||
const updatedOptions = new Set(prevOptions);
|
||||
updatedOptions.delete(tagToDelete);
|
||||
return updatedOptions;
|
||||
});
|
||||
if (tags.has(tagToDelete)) {
|
||||
handleDeleteTag(tagToDelete);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTag = (oldTag: string, newTag: string) => {
|
||||
if (oldTag !== newTag) {
|
||||
setTags((prevTags) => {
|
||||
const tagsArray = Array.from(prevTags);
|
||||
const oldTagIndex = tagsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
tagsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(tagsArray);
|
||||
});
|
||||
|
||||
setOptions((prevOptions) => {
|
||||
const optionsArray = Array.from(prevOptions);
|
||||
const oldTagIndex = optionsArray.indexOf(oldTag);
|
||||
if (oldTagIndex !== -1) {
|
||||
optionsArray.splice(oldTagIndex, 1, newTag);
|
||||
}
|
||||
return new Set(optionsArray);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer" onClick={handleClick}>
|
||||
{!cellSelected ? (
|
||||
<TagsArray
|
||||
active={true}
|
||||
handleDelete={handleDeleteTag}
|
||||
tags={tags}
|
||||
/>
|
||||
) : (
|
||||
<div ref={dropdown}>
|
||||
<div className="absolute w-64 z-50 ml-1 mt-5">
|
||||
<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">
|
||||
<TagsArray
|
||||
handleDelete={handleDeleteTag}
|
||||
active
|
||||
tags={tags}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for an option..."
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleAddTag}
|
||||
className="focus:outline-none bg-transparent"
|
||||
autoFocus
|
||||
/>
|
||||
</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">
|
||||
<p className="capitalize">
|
||||
Select an option or create one
|
||||
</p>
|
||||
<TagDropdown
|
||||
handleDeleteTag={handleDeleteTagOption}
|
||||
handleEditTag={handleEditTag}
|
||||
handleAdd={handleSelectTag}
|
||||
tags={options}
|
||||
/>
|
||||
{inputValue.length > 0 && (
|
||||
<CreateNewTagAction input={inputValue} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsInput;
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
export const Tag = ({ children, handleDelete, active = false }) => {
|
||||
return (
|
||||
<span
|
||||
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
|
||||
>
|
||||
{children}
|
||||
{active && handleDelete && (
|
||||
<button onClick={() => handleDelete(children)}>
|
||||
<XMarkIcon className={`ml-1 w-3 text-purple-500`} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
export const Tag = ({ children, handleDelete, active = false }) => {
|
||||
return (
|
||||
<span
|
||||
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
|
||||
>
|
||||
{children}
|
||||
{active && handleDelete && (
|
||||
<button onClick={() => handleDelete(children)}>
|
||||
<XMarkIcon className={`ml-1 w-3 text-purple-500`} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import { Tag } from "./Tag";
|
||||
import { DropdownAction } from "./DropdownAction";
|
||||
|
||||
export const TagDropdown = ({
|
||||
tags,
|
||||
handleEditTag,
|
||||
handleDeleteTag,
|
||||
handleAdd,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-50 flex flex-col space-y-2 mt-2">
|
||||
{Array.from(tags).map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100"
|
||||
>
|
||||
<button onClick={() => handleAdd(tag)}>
|
||||
<Tag>{tag}</Tag>
|
||||
</button>
|
||||
<DropdownAction
|
||||
handleDeleteTag={handleDeleteTag}
|
||||
handleEditTag={handleEditTag}
|
||||
tag={tag}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Tag } from "./Tag";
|
||||
import { DropdownAction } from "./DropdownAction";
|
||||
|
||||
export const TagDropdown = ({
|
||||
tags,
|
||||
handleEditTag,
|
||||
handleDeleteTag,
|
||||
handleAdd,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-50 flex flex-col space-y-2 mt-2">
|
||||
{Array.from(tags).map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100"
|
||||
>
|
||||
<button onClick={() => handleAdd(tag)}>
|
||||
<Tag>{tag}</Tag>
|
||||
</button>
|
||||
<DropdownAction
|
||||
handleDeleteTag={handleDeleteTag}
|
||||
handleEditTag={handleEditTag}
|
||||
tag={tag}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { Tag } from "./Tag";
|
||||
|
||||
export interface Tags {
|
||||
tags: Set<string>;
|
||||
handleDelete: (tag: string) => void;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
||||
console.log(tags);
|
||||
|
||||
return (
|
||||
<div className="flex ml-2 flex-wrap gap-2 items-center">
|
||||
{Array.from(tags).map((tag, index) => {
|
||||
return (
|
||||
<Tag
|
||||
handleDelete={handleDelete}
|
||||
active={active}
|
||||
key={index}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Tag } from "./Tag";
|
||||
|
||||
export interface Tags {
|
||||
tags: Set<string>;
|
||||
handleDelete: (tag: string) => void;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
||||
console.log(tags);
|
||||
|
||||
return (
|
||||
<div className="flex ml-2 flex-wrap gap-2 items-center">
|
||||
{Array.from(tags).map((tag, index) => {
|
||||
return (
|
||||
<Tag
|
||||
handleDelete={handleDelete}
|
||||
active={active}
|
||||
key={index}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import React from "react";
|
||||
|
||||
interface ErrorBannerProps {
|
||||
heading: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
const ErrorBanner: React.FC<ErrorBannerProps> = ({
|
||||
heading,
|
||||
description = null,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded border-s-4 border-red-500 bg-red-50 p-4"
|
||||
>
|
||||
<strong className="block text-sm font-semibold text-red-800">
|
||||
{heading}
|
||||
</strong>
|
||||
{description && (
|
||||
<p className="mt-2 text-xs font-thin text-red-700">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorBanner;
|
||||
import React from "react";
|
||||
|
||||
interface ErrorBannerProps {
|
||||
heading: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
const ErrorBanner: React.FC<ErrorBannerProps> = ({
|
||||
heading,
|
||||
description = null,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded border-s-4 border-red-500 bg-red-50 p-4"
|
||||
>
|
||||
<strong className="block text-sm font-semibold text-red-800">
|
||||
{heading}
|
||||
</strong>
|
||||
{description && (
|
||||
<p className="mt-2 text-xs font-thin text-red-700">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorBanner;
|
||||
|
|
|
@ -1,43 +1,43 @@
|
|||
/* components/Loading.module.css */
|
||||
.loadingOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loadingContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingTitle {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #5b21b6;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #5b21b6;
|
||||
border-top: 4px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* components/Loading.module.css */
|
||||
.loadingOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loadingContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingTitle {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #5b21b6;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #5b21b6;
|
||||
border-top: 4px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
// components/Loading.js
|
||||
import styles from "./Loading.module.css";
|
||||
import Image from "next/image";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className={styles.loadingOverlay}>
|
||||
<div className={styles.loadingContent}>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className={styles.loadingTitle}>Loading...</h1>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
// components/Loading.js
|
||||
import styles from "./Loading.module.css";
|
||||
import Image from "next/image";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className={styles.loadingOverlay}>
|
||||
<div className={styles.loadingContent}>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Compass Center logo."
|
||||
width={100}
|
||||
height={91}
|
||||
/>
|
||||
<h1 className={styles.loadingTitle}>Loading...</h1>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user