Merge pull request #24 from cssgunc/erica-admin-GEN-55-layout

Erica admin gen 55 layout
This commit is contained in:
Meliora Ho 2024-03-26 19:35:00 -04:00 committed by GitHub
commit d9c97bae19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 5898 additions and 5077 deletions

View File

@ -1,72 +1,72 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
# Setup workspace directory # Setup workspace directory
RUN mkdir /workspace RUN mkdir /workspace
WORKDIR /workspace WORKDIR /workspace
# Install useful system utilities # Install useful system utilities
ENV TZ=America/New_York ENV TZ=America/New_York
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \ RUN apt-get update \
&& apt-get install --yes \ && apt-get install --yes \
apt-transport-https \ apt-transport-https \
ca-certificates \ ca-certificates \
curl \ curl \
debian-keyring \ debian-keyring \
debian-archive-keyring \ debian-archive-keyring \
git \ git \
gnupg \ gnupg \
locales \ locales \
postgresql-client \ postgresql-client \
software-properties-common \ software-properties-common \
sudo \ sudo \
tzdata \ tzdata \
wget \ wget \
zsh \ zsh \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Python 3.11 # Install Python 3.11
RUN add-apt-repository ppa:deadsnakes/ppa \ RUN add-apt-repository ppa:deadsnakes/ppa \
&& apt update \ && apt update \
&& apt install --yes \ && apt install --yes \
python3.11 \ python3.11 \
python3-pip \ python3-pip \
libpq-dev \ libpq-dev \
python3.11-dev \ python3.11-dev \
&& rm -rf /var/lib/apt/lists* \ && rm -rf /var/lib/apt/lists* \
&& unlink /usr/bin/python3 \ && unlink /usr/bin/python3 \
&& ln -s /usr/bin/python3.11 /usr/bin/python3 && ln -s /usr/bin/python3.11 /usr/bin/python3
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user # Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
ARG USERNAME=vscode ARG USERNAME=vscode
ARG USER_UID=1000 ARG USER_UID=1000
ARG USER_GID=$USER_UID ARG USER_GID=$USER_UID
# Add non-root user and add to sudoers # Add non-root user and add to sudoers
RUN groupadd --gid $USER_GID $USERNAME \ RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /usr/bin/zsh \ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /usr/bin/zsh \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME && chmod 0440 /etc/sudoers.d/$USERNAME
# Set code to default git commit editor # Set code to default git commit editor
RUN git config --system core.editor "code --wait" RUN git config --system core.editor "code --wait"
# Set Safe Directory # Set Safe Directory
RUN git config --system safe.directory '/workspace' RUN git config --system safe.directory '/workspace'
# Configure zsh # Configure zsh
USER $USERNAME USER $USERNAME
ENV HOME /home/$USERNAME ENV HOME /home/$USERNAME
# Add zsh theme with niceties # Add zsh theme with niceties
RUN curl https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash - \ RUN curl https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash - \
&& sed -i 's/robbyrussell/kennethreitz/g' ~/.zshrc \ && sed -i 's/robbyrussell/kennethreitz/g' ~/.zshrc \
&& echo 'source <(ng completion script)' >>~/.zshrc \ && echo 'source <(ng completion script)' >>~/.zshrc \
&& echo 'export PATH=$PATH:$HOME/.local/bin' >>~/.zshrc && echo 'export PATH=$PATH:$HOME/.local/bin' >>~/.zshrc
# Set Locale for Functional Autocompletion in zsh # Set Locale for Functional Autocompletion in zsh
RUN sudo locale-gen en_US.UTF-8 RUN sudo locale-gen en_US.UTF-8
# Install Database Dependencies # Install Database Dependencies
COPY backend/requirements.txt /workspace/backend/requirements.txt COPY backend/requirements.txt /workspace/backend/requirements.txt
WORKDIR /workspace/backend WORKDIR /workspace/backend
RUN python3 -m pip install -r requirements.txt RUN python3 -m pip install -r requirements.txt

View File

@ -1,47 +1,47 @@
{ {
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "docker-compose.yml",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"service": "httpd", "service": "httpd",
"remoteUser": "vscode", "remoteUser": "vscode",
"forwardPorts": [ "forwardPorts": [
5432, 5432,
5050 5050
], ],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"ecmel.vscode-html-css", "ecmel.vscode-html-css",
"ms-vscode.vscode-typescript-next", "ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"tamasfe.even-better-toml", "tamasfe.even-better-toml",
"ckolkman.vscode-postgres", "ckolkman.vscode-postgres",
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.black-formatter", "ms-python.black-formatter",
"gruntfuggly.todo-tree", "gruntfuggly.todo-tree",
"ms-azuretools.vscode-docker" "ms-azuretools.vscode-docker"
], ],
"settings": { "settings": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnSaveMode": "file", "editor.formatOnSaveMode": "file",
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter" "editor.defaultFormatter": "ms-python.black-formatter"
}, },
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
"/backend/" "/backend/"
], ],
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportMissingParameterType": "error", "reportMissingParameterType": "error",
"reportGeneralTypeIssues": "error", "reportGeneralTypeIssues": "error",
"reportDeprecated": "error", "reportDeprecated": "error",
"reportImportCycles": "error" "reportImportCycles": "error"
} }
} }
} }
} }
} }

View File

@ -1,35 +1,35 @@
version: "3" version: "3"
services: services:
httpd: httpd:
build: build:
context: .. context: ..
dockerfile: .devcontainer/Dockerfile dockerfile: .devcontainer/Dockerfile
volumes: volumes:
- ..:/workspace - ..:/workspace
command: /bin/sh -c "while sleep 1000; do :; done" command: /bin/sh -c "while sleep 1000; do :; done"
environment: environment:
- windir # Defined on Windows but not on other platforms - windir # Defined on Windows but not on other platforms
db: db:
image: "postgres:latest" image: "postgres:latest"
ports: ports:
- "5432:5432" - "5432:5432"
env_file: env_file:
- ../backend/.env - ../backend/.env
volumes: volumes:
- compass-center-postgres:/var/lib/postgresql/data - compass-center-postgres:/var/lib/postgresql/data
# - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
pgadmin: pgadmin:
image: dpage/pgadmin4:latest image: dpage/pgadmin4:latest
environment: environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_DEFAULT_PASSWORD: admin
ports: ports:
- "5050:80" - "5050:80"
depends_on: depends_on:
- db - db
volumes: volumes:
compass-center-postgres: compass-center-postgres:

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
/backend/.env /backend/.env
__pycache__ __pycache__

210
README.md
View File

@ -1,105 +1,105 @@
# 🧭 Compass Center's Internal Resource Management App # 🧭 Compass Center's Internal Resource Management App
## 🛠 Technologies ## 🛠 Technologies
- Next.js - Next.js
- TailwindCSS - TailwindCSS
- TypeScript - TypeScript
- Supabase - Supabase
## 📁 File Setup ## 📁 File Setup
``` ```
\compass \compass
\components // Components organized in folders related to specific pages \components // Components organized in folders related to specific pages
\pages // Store all pages here \pages // Store all pages here
\api // API routes \api // API routes
\public // Local assets (minimize usage) \public // Local assets (minimize usage)
\utils // Constants, Routes, Classes, Dummy Data \utils // Constants, Routes, Classes, Dummy Data
\styles // CSS files \styles // CSS files
``` ```
## 🚀 To Start ## 🚀 To Start
Follow these steps to set up your local environment: Follow these steps to set up your local environment:
``` ```
\\ Clone this repository \\ Clone this repository
git clone https://github.com/cssgunc/compass.git git clone https://github.com/cssgunc/compass.git
\\ Go into main folder \\ Go into main folder
cd compass cd compass
\\ Install dependencies \\ Install dependencies
npm install npm install
\\ Run local environment \\ Run local environment
npm run dev npm run dev
``` ```
Also add following variables inside of a .env file inside of the backend directory Also add following variables inside of a .env file inside of the backend directory
``` ```
\\ .env file contents \\ .env file contents
POSTGRES_DATABASE=compass POSTGRES_DATABASE=compass
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=admin POSTGRES_PASSWORD=admin
POSTGRES_HOST=db POSTGRES_HOST=db
POSTGRES_PORT=5432 POSTGRES_PORT=5432
HOST=localhost HOST=localhost
``` ```
## Backend Starter ## Backend Starter
- Please open the VS Code Command Palette - Please open the VS Code Command Palette
- Run the command **Dev Containers: Rebuild and Reopen in Container** - Run the command **Dev Containers: Rebuild and Reopen in Container**
- This should open the dev container with the same file directory mounted so any changes in the dev container will be seen in the local repo - This should open the dev container with the same file directory mounted so any changes in the dev container will be seen in the local repo
### In Dev Container ### In Dev Container
Run this to reset the database and populate it with the approprate tables that reflect the entities folder Run this to reset the database and populate it with the approprate tables that reflect the entities folder
``` ```
python3 -m backend.script.reset_demo python3 -m backend.script.reset_demo
``` ```
### Possible Dev Container Errors ### Possible Dev Container Errors
- Sometimes the ports allocated to our services will be allocated (5432 for Postgres and 5050 for PgAdmin4) - Sometimes the ports allocated to our services will be allocated (5432 for Postgres and 5050 for PgAdmin4)
- Run **docker stop** to stop all containers - Run **docker stop** to stop all containers
- If that does not work using **sudo lsof -i :[PORT_NUMBER]** to find the process running on the needed ports and idenitfy the PID - If that does not work using **sudo lsof -i :[PORT_NUMBER]** to find the process running on the needed ports and idenitfy the PID
- Run **sudo kill [PID]** - Run **sudo kill [PID]**
- If you are on Windows please consult ChatGPT or set up WSL (will be very useful in the future) - If you are on Windows please consult ChatGPT or set up WSL (will be very useful in the future)
### Accesing pgAdmin 4 ### Accesing pgAdmin 4
- First go to http://localhost:5050/ on your browser - First go to http://localhost:5050/ on your browser
- Log in using the credentials admin@example.com and admin - Log in using the credentials admin@example.com and admin
- Click **Add New Server** - Click **Add New Server**
- Fill in the name field with Compass (can be anything) - Fill in the name field with Compass (can be anything)
- Click **Connection** tab and fill in the following: - Click **Connection** tab and fill in the following:
- Host name/address: db - Host name/address: db
- Maintence database: compass - Maintence database: compass
- Username: postgres - Username: postgres
- Password: admin - Password: admin
- Click **Save** at the bottom to add connection - Click **Save** at the bottom to add connection
- Click **Server** dropdown on the left and click through items inside the **Compass** server - Click **Server** dropdown on the left and click through items inside the **Compass** server
### Testing Backend Code ### Testing Backend Code
- Write tests for any service you create and any function in those services - Write tests for any service you create and any function in those services
- Make sure to add docstrings detailing what the file is doing and what each test is doing - Make sure to add docstrings detailing what the file is doing and what each test is doing
- Name all test functions with test\_[testContent] (Must be prefixed with test to be recognized by pytest) - Name all test functions with test\_[testContent] (Must be prefixed with test to be recognized by pytest)
- Utitlize dependency injection for commonly used services - Utitlize dependency injection for commonly used services
``` ```
\\ Run all tests by being in the backend directory \\ Run all tests by being in the backend directory
pytest pytest
\\ Run specific tests by passing in file as a parameter \\ Run specific tests by passing in file as a parameter
\\ Passing the -s allows us to see any print statements or debugging statements in the console \\ Passing the -s allows us to see any print statements or debugging statements in the console
pytest -s --rootdir=/workspace [testFilePath]::[testFunctionSignature] pytest -s --rootdir=/workspace [testFilePath]::[testFunctionSignature]
``` ```
## 💡 Dev Notes ## 💡 Dev Notes
- For each task, create a branch in the format '[your name]-[ticket number]-[task description]' - For each task, create a branch in the format '[your name]-[ticket number]-[task description]'
- Only commit your work to that branch and then make a git request to '/main' - Only commit your work to that branch and then make a git request to '/main'
- When creating new files in the backend and code is in python make sure to add a docstring for the file and any function you create ("""[content]"""") - When creating new files in the backend and code is in python make sure to add a docstring for the file and any function you create ("""[content]"""")

View File

@ -1,29 +1,29 @@
"""SQLAlchemy DB Engine and Session niceties for FastAPI dependency injection.""" """SQLAlchemy DB Engine and Session niceties for FastAPI dependency injection."""
import sqlalchemy import sqlalchemy
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from .env import getenv from .env import getenv
def _engine_str(database: str = getenv("POSTGRES_DATABASE")) -> str: def _engine_str(database: str = getenv("POSTGRES_DATABASE")) -> str:
"""Helper function for reading settings from environment variables to produce connection string.""" """Helper function for reading settings from environment variables to produce connection string."""
dialect = "postgresql+psycopg2" dialect = "postgresql+psycopg2"
user = getenv("POSTGRES_USER") user = getenv("POSTGRES_USER")
password = getenv("POSTGRES_PASSWORD") password = getenv("POSTGRES_PASSWORD")
host = getenv("POSTGRES_HOST") host = getenv("POSTGRES_HOST")
port = getenv("POSTGRES_PORT") port = getenv("POSTGRES_PORT")
return f"{dialect}://{user}:{password}@{host}:{port}/{database}" return f"{dialect}://{user}:{password}@{host}:{port}/{database}"
engine = sqlalchemy.create_engine(_engine_str(), echo=True) engine = sqlalchemy.create_engine(_engine_str(), echo=True)
"""Application-level SQLAlchemy database engine.""" """Application-level SQLAlchemy database engine."""
def db_session(): def db_session():
"""Generator function offering dependency injection of SQLAlchemy Sessions.""" """Generator function offering dependency injection of SQLAlchemy Sessions."""
print("ran") print("ran")
session = Session(engine) session = Session(engine)
try: try:
yield session yield session
finally: finally:
session.close() session.close()

View File

@ -1,2 +1,10 @@
from .entity_base import EntityBase from .entity_base import EntityBase
from .sample_entity import SampleEntity from .sample_entity import SampleEntity
from .tag_entity import TagEntity
from .user_entity import UserEntity
from .resource_entity import ResourceEntity
from .resource_tag_entity import ResourceTagEntity
from .service_entity import ServiceEntity
from .service_tag_entity import ServiceTagEntity
from .program_enum import ProgramEnum
from .user_enum import RoleEnum

View File

@ -1,12 +1,12 @@
"""Abstract superclass of all entities in the application. """Abstract superclass of all entities in the application.
There is no reason to instantiate this class directly. Instead, look toward the child classes. There is no reason to instantiate this class directly. Instead, look toward the child classes.
Additionally, import from the top-level entities file which indexes all entity implementations. Additionally, import from the top-level entities file which indexes all entity implementations.
""" """
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
class EntityBase(DeclarativeBase): class EntityBase(DeclarativeBase):
pass pass

View File

@ -0,0 +1,10 @@
from sqlalchemy import Enum
class ProgramEnum(Enum):
ECONOMIC = "economic"
DOMESTIC = "domestic"
COMMUNITY = "community"
def __init__(self):
super().__init__(name="program_enum")

View File

@ -0,0 +1,68 @@
""" Defines the table for storing resources """
# 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
# Import self for to model
from typing import Self
from backend.entities.program_enum import ProgramEnum
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(32), nullable=False)
summary: Mapped[str] = mapped_column(String(100), nullable=False)
link: Mapped[str] = mapped_column(String, nullable=False)
program: Mapped[ProgramEnum] = mapped_column(ProgramEnum, nullable=False)
# relationships
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
back_populates="resource", cascade="all,delete"
)
#
# @classmethod
# def from_model(cls, model: user_model) -> 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) -> user_model:
# return user_model (
# id = self.id,
# created_at = self.created_at,
# name = self.name,
# summary = self.summary,
# link = self.link,
# program = self.program,
# )

View File

@ -0,0 +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__ = "resourceTag"
# 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,
# )

View File

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

View File

@ -0,0 +1,44 @@
""" 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
class ProgramEnum(enum.Enum):
"""Determine program for Service"""
DOMESTIC = "DOMESTIC"
ECONOMIC = "ECONOMIC"
COMMUNITY = "COMMUNITY"
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)
summary: Mapped[str] = mapped_column(String(100), nullable=False)
requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
program: Mapped[ProgramEnum] = mapped_column(Enum(ProgramEnum), nullable=False)
# relationships
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
back_populates="service", cascade="all,delete"
)

View File

@ -0,0 +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__ = "serviceTag"
# 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="resourceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")

View File

@ -0,0 +1,62 @@
""" 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
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.id,
)
"""

View File

@ -0,0 +1,90 @@
""" Defines the table for storing users """
# 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
# 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.entities.program_enum import ProgramEnum
from .user_enum import RoleEnum
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[RoleEnum] = mapped_column(RoleEnum, nullable=False)
username: Mapped[str] = mapped_column(
String(32), nullable=False, default="", unique=True
)
role: Mapped[RoleEnum] = mapped_column(RoleEnum, nullable=False)
email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
program: Mapped[list[ProgramEnum]] = mapped_column(
ARRAY(ProgramEnum), nullable=False
)
program: Mapped[list[ProgramEnum]] = mapped_column(
ARRAY(ProgramEnum), nullable=False
)
experience: Mapped[int] = mapped_column(Integer, nullable=False)
group: Mapped[str] = mapped_column(String(50))
"""
@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,
username=model.username,
role=model.role,
email=model.email,
program=model.program,
experience=model.experience,
group=model.group,
)
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.id,
role=self.role,
email=self.email,
program=self.program,
experience=self.experience,
group=self.group,
)
"""

View File

@ -0,0 +1,12 @@
from sqlalchemy import Enum
class RoleEnum(Enum):
"""Determine role for User"""
ADMIN = "ADMIN"
EMPLOYEE = "EMPLOYEE"
VOLUNTEER = "VOLUNTEER"
def __init__(self):
super().__init__(name="role_enum")

View File

@ -1,21 +1,21 @@
"""Load environment variables from a .env file or the process' environment.""" """Load environment variables from a .env file or the process' environment."""
import os import os
import dotenv import dotenv
# Load envirnment variables from .env file upon module start. # Load envirnment variables from .env file upon module start.
dotenv.load_dotenv(f"{os.path.dirname(__file__)}/.env", verbose=True) dotenv.load_dotenv(f"{os.path.dirname(__file__)}/.env", verbose=True)
def getenv(variable: str) -> str: def getenv(variable: str) -> str:
"""Get value of environment variable or raise an error if undefined. """Get value of environment variable or raise an error if undefined.
Unlike `os.getenv`, our application expects all environment variables it needs to be defined Unlike `os.getenv`, our application expects all environment variables it needs to be defined
and we intentionally fast error out with a diagnostic message to avoid scenarios of running and we intentionally fast error out with a diagnostic message to avoid scenarios of running
the application when expected environment variables are not set. the application when expected environment variables are not set.
""" """
value = os.getenv(variable) value = os.getenv(variable)
if value is not None: if value is not None:
return value return value
else: else:
raise NameError(f"Error: {variable} Environment Variable not Defined") raise NameError(f"Error: {variable} Environment Variable not Defined")

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
from enum import Enum
from typing import List
from datetime import datetime
from typing import Optional
class ProgramTypeEnum(str, Enum):
DOMESTIC = "DOMESTIC"
ECONOMIC = "ECONOMIC"
COMMUNITY = "COMMUNITY"
class UserTypeEnum(str, Enum):
ADMIN = "ADMIN"
EMPLOYEE = "EMPLOYEE"
VOLUNTEER = "VOLUNTEER"

View File

@ -0,0 +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
from .resource_model import Resource
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")
programtype: ProgramTypeEnum
created_at: Optional[datetime]

View File

@ -0,0 +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]

View File

@ -0,0 +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

View File

@ -0,0 +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]

View File

@ -0,0 +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

View File

@ -0,0 +1,17 @@
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
programtype: List[ProgramTypeEnum]
usertype: UserTypeEnum
created_at: Optional[datetime]

View File

@ -1,6 +1,6 @@
fastapi[all] >=0.100.0, <0.101.0 fastapi[all] >=0.100.0, <0.101.0
sqlalchemy >=2.0.4, <2.1.0 sqlalchemy >=2.0.4, <2.1.0
psycopg2 >=2.9.5, <2.10.0 psycopg2 >=2.9.5, <2.10.0
alembic >=1.10.2, <1.11.0 alembic >=1.10.2, <1.11.0
pytest >=7.2.1, <7.3.0 pytest >=7.2.1, <7.3.0
python-dotenv >=1.0.0, <1.1.0 python-dotenv >=1.0.0, <1.1.0

View File

@ -1,14 +1,14 @@
from sqlalchemy import text, create_engine from sqlalchemy import text, create_engine
from ..database import engine, _engine_str from ..database import engine, _engine_str
from ..env import getenv from ..env import getenv
engine = create_engine(_engine_str(database=""), echo=True) engine = create_engine(_engine_str(database=""), echo=True)
"""Application-level SQLAlchemy database engine.""" """Application-level SQLAlchemy database engine."""
with engine.connect() as connection: with engine.connect() as connection:
connection.execute( connection.execute(
text("COMMIT") text("COMMIT")
) )
database = getenv("POSTGRES_DATABASE") database = getenv("POSTGRES_DATABASE")
stmt = text(f"CREATE DATABASE {database}") stmt = text(f"CREATE DATABASE {database}")
connection.execute(stmt) connection.execute(stmt)

View File

@ -1,14 +1,14 @@
from sqlalchemy import text, create_engine from sqlalchemy import text, create_engine
from ..database import engine, _engine_str from ..database import engine, _engine_str
from ..env import getenv from ..env import getenv
engine = create_engine(_engine_str(database=""), echo=True) engine = create_engine(_engine_str(database=""), echo=True)
"""Application-level SQLAlchemy database engine.""" """Application-level SQLAlchemy database engine."""
with engine.connect() as connection: with engine.connect() as connection:
connection.execute( connection.execute(
text("COMMIT") text("COMMIT")
) )
database = getenv("POSTGRES_DATABASE") database = getenv("POSTGRES_DATABASE")
stmt = text(f"DROP DATABASE IF EXISTS {database}") stmt = text(f"DROP DATABASE IF EXISTS {database}")
connection.execute(stmt) connection.execute(stmt)

View File

@ -1,18 +1,18 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
import subprocess import subprocess
from ..database import engine, _engine_str from ..database import engine, _engine_str
from ..env import getenv from ..env import getenv
from .. import entities from .. import entities
database = getenv("POSTGRES_DATABASE") database = getenv("POSTGRES_DATABASE")
engine = create_engine(_engine_str(), echo=True) engine = create_engine(_engine_str(), echo=True)
"""Application-level SQLAlchemy database engine.""" """Application-level SQLAlchemy database engine."""
# Run Delete and Create Database Scripts # Run Delete and Create Database Scripts
subprocess.run(["python3", "-m", "backend.script.delete_database"]) subprocess.run(["python3", "-m", "backend.script.delete_database"])
subprocess.run(["python3", "-m", "backend.script.create_database"]) subprocess.run(["python3", "-m", "backend.script.create_database"])
entities.EntityBase.metadata.drop_all(engine) entities.EntityBase.metadata.drop_all(engine)
entities.EntityBase.metadata.create_all(engine) entities.EntityBase.metadata.create_all(engine)

View File

@ -1,50 +1,50 @@
"""Shared pytest fixtures for database dependent tests.""" """Shared pytest fixtures for database dependent tests."""
import pytest import pytest
from sqlalchemy import Engine, create_engine, text from sqlalchemy import Engine, create_engine, text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from ...database import _engine_str from ...database import _engine_str
from ...env import getenv from ...env import getenv
from ... import entities from ... import entities
POSTGRES_DATABASE = f'{getenv("POSTGRES_DATABASE")}_test' POSTGRES_DATABASE = f'{getenv("POSTGRES_DATABASE")}_test'
POSTGRES_USER = getenv("POSTGRES_USER") POSTGRES_USER = getenv("POSTGRES_USER")
def reset_database(): def reset_database():
engine = create_engine(_engine_str(database="")) engine = create_engine(_engine_str(database=""))
with engine.connect() as connection: with engine.connect() as connection:
try: try:
conn = connection.execution_options(autocommit=False) conn = connection.execution_options(autocommit=False)
conn.execute(text("ROLLBACK")) # Get out of transactional mode... conn.execute(text("ROLLBACK")) # Get out of transactional mode...
conn.execute(text(f"DROP DATABASE IF EXISTS {POSTGRES_DATABASE}")) conn.execute(text(f"DROP DATABASE IF EXISTS {POSTGRES_DATABASE}"))
except OperationalError: except OperationalError:
print( print(
"Could not drop database because it's being accessed by others (psql open?)" "Could not drop database because it's being accessed by others (psql open?)"
) )
exit(1) exit(1)
conn.execute(text(f"CREATE DATABASE {POSTGRES_DATABASE}")) conn.execute(text(f"CREATE DATABASE {POSTGRES_DATABASE}"))
conn.execute( conn.execute(
text( text(
f"GRANT ALL PRIVILEGES ON DATABASE {POSTGRES_DATABASE} TO {POSTGRES_USER}" f"GRANT ALL PRIVILEGES ON DATABASE {POSTGRES_DATABASE} TO {POSTGRES_USER}"
) )
) )
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def test_engine() -> Engine: def test_engine() -> Engine:
reset_database() reset_database()
return create_engine(_engine_str(POSTGRES_DATABASE)) return create_engine(_engine_str(POSTGRES_DATABASE))
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def session(test_engine: Engine): def session(test_engine: Engine):
entities.EntityBase.metadata.drop_all(test_engine) entities.EntityBase.metadata.drop_all(test_engine)
entities.EntityBase.metadata.create_all(test_engine) entities.EntityBase.metadata.create_all(test_engine)
session = Session(test_engine) session = Session(test_engine)
try: try:
yield session yield session
finally: finally:
session.close() session.close()

View File

@ -1,21 +1,21 @@
"""Sample Test File""" """Sample Test File"""
from sqlalchemy import Engine, select from sqlalchemy import Engine, select
from ... import entities from ... import entities
from ...entities.sample_entity import SampleEntity from ...entities.sample_entity import SampleEntity
def test_entity_count(): def test_entity_count():
"""Checks the number of entities to be inserted""" """Checks the number of entities to be inserted"""
print(entities.EntityBase.metadata.tables.keys()) print(entities.EntityBase.metadata.tables.keys())
assert len(entities.EntityBase.metadata.tables.keys()) == 1 assert len(entities.EntityBase.metadata.tables.keys()) == 1
def test_add_sample_data(session: Engine): def test_add_sample_data(session: Engine):
"""Inserts a sample data point and verifies it is in the database""" """Inserts a sample data point and verifies it is in the database"""
entity = SampleEntity(name="Praj", age=19, email="pmoha@unc.edu") entity = SampleEntity(name="Praj", age=19, email="pmoha@unc.edu")
session.add(entity) session.add(entity)
session.commit() session.commit()
data = session.get(SampleEntity, 1) data = session.get(SampleEntity, 1)
assert data.name == "Praj" assert data.name == "Praj"

View File

@ -0,0 +1,19 @@
""" Testing Tag Entity """
from sqlalchemy import Engine
from ... import entities
from ...entities.tag_entity import TagEntity
def test_add_sample_data_tag(session: Engine):
"""Inserts a sample data point and verifies it is in the database"""
entity = TagEntity(content="Test tag")
session.add(entity)
session.commit()
data = session.get(TagEntity, 1)
assert data.id == 1
assert data.content == "Test tag"

View File

@ -0,0 +1,24 @@
""" Testing User Entity """
from sqlalchemy import Engine
from ... import entities
from ...entities.user_entity import UserEntity
from ...entities.user_entity import RoleEnum
from ...entities.user_entity import ProgramEnum
def test_add_sample_data_user(session: Engine):
"""Inserts a sample data point and verifies it is in the database"""
entity = UserEntity(id=1, username="emmalynf", role=RoleEnum.ADMIN, email="efoster@unc.edu", program=[ProgramEnum.COMMUNITY, ProgramEnum.DOMESTIC, ProgramEnum.ECONOMIC], experience=10, group="group")
session.add(entity)
session.commit()
data = session.get(UserEntity, 1)
assert data.id == 1
assert data.username == "emmalynf"
assert data.email == "efoster@unc.edu"
assert data.experience == 10
assert data.role == RoleEnum.ADMIN
assert data.program == [ProgramEnum.COMMUNITY, ProgramEnum.DOMESTIC, ProgramEnum.ECONOMIC]

View File

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

70
compass/.gitignore vendored
View File

@ -1,35 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
# testing # testing
/coverage /coverage
# next.js # next.js
/.next/ /.next/
/out/ /out/
# production # production
/build /build
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
# debug # debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env*.local .env*.local
# vercel # vercel
.vercel .vercel
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts

View File

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

View File

View File

@ -1,11 +1,10 @@
// pages/forgot-password.tsx // pages/forgot-password.tsx
"use client"; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import Input from '@/components/Input'; import Input from '@/components/Input';
import Button from '@/components/Button'; import Button from '@/components/Button';
import InlineLink from '@/components/InlineLink'; import InlineLink from '@/components/InlineLink';
import Paper from '@/components/auth/Paper';
import ErrorBanner from '@/components/auth/ErrorBanner'; import ErrorBanner from '@/components/auth/ErrorBanner';

View File

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

View File

@ -4,7 +4,6 @@
import Button from '@/components/Button'; import Button from '@/components/Button';
import Input from '@/components/Input' import Input from '@/components/Input'
import InlineLink from '@/components/InlineLink'; import InlineLink from '@/components/InlineLink';
import Paper from '@/components/auth/Paper';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from "react"; import { useState } from "react";
import PasswordInput from '@/components/auth/PasswordInput'; import PasswordInput from '@/components/auth/PasswordInput';
@ -52,7 +51,7 @@ export default function Page() {
height={91} height={91}
/> />
<h1 className='font-bold text-xl text-purple-800'>Login</h1> <h1 className='font-bold text-2xl text-purple-800'>Login</h1>
<div className="mb-6"> <div className="mb-6">
<Input type='email' valid={emailError == ""} title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} required /> <Input type='email' valid={emailError == ""} title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} required />

View File

@ -2,8 +2,6 @@
"use client"; "use client";
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Paper from '@/components/auth/Paper';
import PasswordInput from '@/components/auth/PasswordInput'; import PasswordInput from '@/components/auth/PasswordInput';
import ErrorBanner from '@/components/auth/ErrorBanner'; import ErrorBanner from '@/components/auth/ErrorBanner';

View File

@ -1,20 +1,16 @@
import '../styles/globals.css'; import '../styles/globals.css';
import { Metadata } from 'next'
export const metadata: Metadata = { export default function RootLayout({
title: 'Login', // Layouts must accept a children prop.
} // This will be populated with nested layouts or pages
children,
export default function RootLayout({ }: {
// Layouts must accept a children prop. children: React.ReactNode
// This will be populated with nested layouts or pages }) {
children, return (
}: { <html lang="en">
children: React.ReactNode <body>{children}</body>
}) { </html>
return ( )
<html lang="en">
<body>{children}</body>
</html>
)
} }

View File

@ -0,0 +1,37 @@
"use client"
import Sidebar from '@/components/resource/Sidebar';
import React, { useState } from 'react';
import { ChevronDoubleRightIcon } from '@heroicons/react/24/outline';
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
return (
<div className="flex-row">
{/* 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} />
</div>
{/* page ui */}
<div className={`flex-1 transition duration-300 ease-in-out ${isSidebarOpen ? 'ml-64' : 'ml-0'}`}>
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,39 @@
"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';
export default function Page() {
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<div className="pt-16 px-8 pb-4 flex-grow">
<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">
<Card icon={<BookmarkIcon />} text="Resources" />
<Card icon={<ClipboardIcon />} text="Services" />
<Card icon={<BookOpenIcon />} text="Training Manuals" />
</div>
{/* search bar */}
<LandingSearchBar />
</div>
</div>
)
}

View File

@ -1,37 +1,37 @@
import React, { FunctionComponent, InputHTMLAttributes, ReactNode, ChangeEvent } from 'react'; import React, { FunctionComponent, InputHTMLAttributes, ReactNode, ChangeEvent } from 'react';
type InputProps = InputHTMLAttributes<HTMLInputElement> & { type InputProps = InputHTMLAttributes<HTMLInputElement> & {
icon?: ReactNode; icon?: ReactNode;
title?: ReactNode; title?: ReactNode;
type?:ReactNode; type?:ReactNode;
placeholder?:ReactNode placeholder?:ReactNode
valid?:boolean; valid?:boolean;
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}; };
const Input: FunctionComponent<InputProps> = ({ icon, type, title, placeholder, onChange, valid = true, ...rest }) => { const Input: FunctionComponent<InputProps> = ({ icon, type, title, placeholder, onChange, valid = true, ...rest }) => {
return ( return (
<div> <div>
<label <label
htmlFor={title} 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"} 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> <span className="text-xs font-semibold text-gray-700"> {title} </span>
<div className="mt-1 flex items-center"> <div className="mt-1 flex items-center">
<input <input
type={type} type={type}
id={title} id={title}
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
className="w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm" className="w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/> />
<span className="inline-flex items-center px-3 text-gray-500"> <span className="inline-flex items-center px-3 text-gray-500">
{icon} {icon}
</span> </span>
</div> </div>
</label> </label>
</div> </div>
); );
}; };
export default Input; export default Input;

View File

@ -0,0 +1,22 @@
interface PageLayoutProps{
icon: React.ReactElement;
title: string;
table: React.ReactElement
}
export const PageLayout: React.FC<PageLayoutProps> = ({ icon, title, table }) => {
return(
<div flex-column>
<div flex-row>
<span className="h-5 text-gray-500 w-5">
{icon}
</span>
<span className="flex-grow font-medium text-xs text-gray-500">
{title}
</span>
</div>
<span> {table} </span>
</div>
);
};

View File

@ -6,7 +6,7 @@ interface PageInterface {
const Paper: React.FC<PageInterface> = ({ children }) => { const Paper: React.FC<PageInterface> = ({ children }) => {
return ( return (
<div className="w-full min-h-screen px-4 py-16 bg-gray-100 sm:px-6 lg:px-8"> <div className="w-full min-h-screen px-4 py-16 bg-gray-50 sm:px-6 lg:px-8">
{children} {children}
</div> </div>
); );

View File

@ -0,0 +1,15 @@
import { ReactNode } from "react";
interface CalloutProps {
children: ReactNode;
}
const Callout = ({ children }: CalloutProps) => {
return (
<div className="p-4 mb-4 flex items-center bg-purple-50 rounded-sm">
<span className="text-sm text-gray-800">{children}</span>
</div>
);
};
export default Callout;

View File

@ -0,0 +1,20 @@
import React, { ReactNode } from "react";
interface TagProps {
text: string;
icon: React.ReactNode;
}
const Card: React.FC<TagProps> = ({ text, icon }) => {
return (
<div className="flex flex-row space-x-2 items-start justify-start border border-gray-200 bg-white hover:bg-gray-50 shadow rounded-md p-4">
<span className="h-5 text-purple-700 w-5">
{icon}
</span>
<span className="text-sm text-gray-800 font-semibold">{text}</span>
</div>
);
};
export default Card;

View File

@ -0,0 +1,48 @@
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/solid"
import React, { useState } from 'react';
import Image from 'next/image';
export const LandingSearchBar: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
};
const clearSearch = () => {
setSearchTerm('');
};
return (
<div className="max-w mx-auto">
{/* searchbar */}
<div className="flex items-center bg-white border border-gray-200 shadow rounded-md">
<div className="flex-grow">
<input
className="sm:text-sm text-gray-800 w-full px-6 py-3 rounded-md focus:outline-none"
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
{/* input */}
{searchTerm && (
<button
onClick={clearSearch}
>
<XMarkIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
</button>
)}
<div className="p-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
</div>
</div>
{/* search results, for now since it's empty this is the default screen */}
<div className="flex flex-col pt-16 space-y-2 justify-center items-center">
<Image alt="Landing illustration" src="/landing_illustration.png" width={250} height={250} />
<h2 className="font-medium text-medium text-gray-800">Need to find something? Use the links or the search bar above to get your results.</h2>
</div>
</div>
);
};

View File

@ -0,0 +1,46 @@
import React from 'react';
import { HomeIcon, ChevronDoubleLeftIcon, BookmarkIcon, ClipboardIcon, BookOpenIcon } from '@heroicons/react/24/solid';
import { SidebarItem } from './SidebarItem';
import { UserProfile } from './UserProfile';
interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen }) => {
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 />
</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">
<SidebarItem icon={<HomeIcon />} text="Home" />
<SidebarItem icon={<BookmarkIcon />} text="Resources" />
<SidebarItem icon={<ClipboardIcon />} text="Services" />
<SidebarItem icon={<BookOpenIcon />} text="Training Manuals" />
</nav>
</div>
</div>
</div>
);
};
export default Sidebar;

View File

@ -0,0 +1,16 @@
interface SidebarItemProps {
icon: React.ReactElement;
text: string;
}
export const SidebarItem: React.FC<SidebarItemProps> = ({ icon, text }) => {
return (
<a href="#" className="flex items-center p-2 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>
</a>
);
};

View File

@ -0,0 +1,11 @@
export const UserProfile = () => {
return (
<div className="flex flex-col items-start space-y-2">
<div className="flex flex-col">
<span className="text-sm font-semibold text-gray-800">Compass Center</span>
<span className="text-xs text-gray-500">cssgunc@gmail.com</span>
</div>
<button className="text-red-600 font-semibold text-xs hover:underline mt-1">Sign out</button>
</div>
)
}

View File

@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {} const nextConfig = {
images: {
domains: ['notioly.com']
},
}
module.exports = nextConfig module.exports = nextConfig

8730
compass/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,28 @@
{ {
"name": "compass", "name": "compass",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.1", "@heroicons/react": "^2.1.1",
"next": "13.5.6", "next": "13.5.6",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10", "autoprefixer": "^10",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "13.5.6", "eslint-config-next": "13.5.6",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3", "tailwindcss": "^3",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -36,12 +36,61 @@
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('/fonts/Inter-Regular.ttf') format('ttf'), src: url('/fonts/Inter-Regular.ttf') format('truetype');
url('/fonts/Inter-Bold.ttf') format('ttf'), }
url('/fonts/Inter-Black.ttf') format('ttf'),
url('/fonts/Inter-ExtraBold.ttf') format('ttf'), /* Inter-Bold */
url('/fonts/Inter-ExtraLight.ttf') format('ttf'), @font-face {
url('/fonts/Inter-Medium.ttf') format('ttf'), font-family: 'Inter';
url('/fonts/Inter-SemiBold.ttf') format('ttf'), font-style: normal;
url('/fonts/Inter-Thin.ttf') format('ttf'); font-weight: 700;
} src: url('/fonts/Inter-Bold.ttf') format('truetype');
}
/* Inter-Black */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
src: url('/fonts/Inter-Black.ttf') format('truetype');
}
/* Inter-ExtraBold */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
src: url('/fonts/Inter-ExtraBold.ttf') format('truetype');
}
/* Inter-ExtraLight */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
src: url('/fonts/Inter-ExtraLight.ttf') format('truetype');
}
/* Inter-Medium */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url('/fonts/Inter-Medium.ttf') format('truetype');
}
/* Inter-SemiBold */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url('/fonts/Inter-SemiBold.ttf') format('truetype');
}
/* Inter-Thin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
src: url('/fonts/Inter-Thin.ttf') format('truetype');
}

View File

@ -13,8 +13,11 @@ const config: Config = {
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
fontFamily: { fontFamily: {
sans: ['Inter', 'sans-serif'], 'sans': ['Inter', 'sans-serif'], // Add 'Inter' to the fontFamily theme
}, },
fontWeight: {
'medium': 500, // Ensure medium is correctly set to 500
}
}, },
}, },
plugins: [], plugins: [],

View File

@ -1,16 +1,16 @@
class CollectionImpl { class CollectionImpl {
title: string; title: string;
icon: any; icon: any;
data: any; data: any;
constructor(title: string, icon: any) { constructor(title: string, icon: any) {
this.title = title; this.title = title;
this.icon = icon; this.icon = icon;
} }
// subject to change // subject to change
setData(data: any){ setData(data: any){
this.data = data; this.data = data;
} }
} }

View File

@ -1,54 +1,54 @@
import { ListBulletIcon, HashtagIcon, Bars3BottomLeftIcon, EnvelopeIcon, AtSymbolIcon, ClipboardIcon, ArrowsUpDownIcon, ChevronDoubleRightIcon, ChevronDoubleLeftIcon, ChevronRightIcon, ChevronLeftIcon, EyeIcon, EyeSlashIcon, UserIcon, BookOpenIcon, MagnifyingGlassIcon, LinkIcon } from '@heroicons/react/24/solid'; import { ListBulletIcon, HashtagIcon, Bars3BottomLeftIcon, EnvelopeIcon, AtSymbolIcon, ClipboardIcon, ArrowsUpDownIcon, ChevronDoubleRightIcon, ChevronDoubleLeftIcon, ChevronRightIcon, ChevronLeftIcon, EyeIcon, EyeSlashIcon, UserIcon, BookOpenIcon, MagnifyingGlassIcon, LinkIcon } from '@heroicons/react/24/solid';
export const Icons = { export const Icons = {
EmailInputIcon: EnvelopeIcon, EmailInputIcon: EnvelopeIcon,
HidePasswordIcon: EyeSlashIcon, HidePasswordIcon: EyeSlashIcon,
UnhidePasswordIcon: EyeIcon, UnhidePasswordIcon: EyeIcon,
UserIcon: UserIcon, UserIcon: UserIcon,
ResourceIcon: BookOpenIcon, ResourceIcon: BookOpenIcon,
SearchIcon: MagnifyingGlassIcon, SearchIcon: MagnifyingGlassIcon,
ServiceIcon: ClipboardIcon, ServiceIcon: ClipboardIcon,
CloseRightArrow: ChevronDoubleRightIcon, CloseRightArrow: ChevronDoubleRightIcon,
CloseLeftArrow: ChevronDoubleLeftIcon, CloseLeftArrow: ChevronDoubleLeftIcon,
LinkRightArrow:ChevronRightIcon, LinkRightArrow:ChevronRightIcon,
LinkLeftArrow:ChevronLeftIcon, LinkLeftArrow:ChevronLeftIcon,
SortIcon: ArrowsUpDownIcon, SortIcon: ArrowsUpDownIcon,
EmailTableIcon:AtSymbolIcon, EmailTableIcon:AtSymbolIcon,
LinkTableIcon: LinkIcon, LinkTableIcon: LinkIcon,
TextTableIcon: Bars3BottomLeftIcon, TextTableIcon: Bars3BottomLeftIcon,
NumberTableIcon: HashtagIcon, NumberTableIcon: HashtagIcon,
MultiselectTableIcon: ListBulletIcon MultiselectTableIcon: ListBulletIcon
}; };
export enum User { export enum User {
ADMIN, ADMIN,
EMPLOYEE, EMPLOYEE,
VOLUNTEER VOLUNTEER
} }
export enum COLLECTION { export enum COLLECTION {
RESOURCE, RESOURCE,
SERVICE, SERVICE,
USER USER
} }
export enum PROGRAM { export enum PROGRAM {
DOMESTIC_VIOLENCE, DOMESTIC_VIOLENCE,
ECONOMIC_STABILITY, ECONOMIC_STABILITY,
COMMUNITY_EDUCATION COMMUNITY_EDUCATION
} }
export enum DATATYPE { export enum DATATYPE {
INTEGER, INTEGER,
STRING, STRING,
LINK, LINK,
EMAIL, EMAIL,
MULTISELECT, MULTISELECT,
SELECT SELECT
} }
// export const COLLECTION_MAP: {[key in COLLECTION]: CollectionImpl} = { // export const COLLECTION_MAP: {[key in COLLECTION]: CollectionImpl} = {
// [COLLECTION.RESOURCE]: new CollectionImpl('Resources', Icons.ResourceIcon), // [COLLECTION.RESOURCE]: new CollectionImpl('Resources', Icons.ResourceIcon),
// [COLLECTION.SERVICE]: new CollectionImpl('Services', Icons.ServiceIcon), // [COLLECTION.SERVICE]: new CollectionImpl('Services', Icons.ServiceIcon),
// [COLLECTION.USER]: new CollectionImpl('Users', Icons.UserIcon) // [COLLECTION.USER]: new CollectionImpl('Users', Icons.UserIcon)
// } // }