Merge remote-tracking branch 'origin/main' into mel-GEN-75-dummy-data

This commit is contained in:
Meliora Ho 2024-03-30 13:20:45 +00:00
commit df0b0614ac
68 changed files with 5913 additions and 5008 deletions

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

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

210
README.md
View File

@ -1,105 +1,105 @@
# 🧭 Compass Center's Internal Resource Management App
## 🛠 Technologies
- Next.js
- TailwindCSS
- TypeScript
- Supabase
## 📁 File Setup
```
\compass
\components // Components organized in folders related to specific pages
\pages // Store all pages here
\api // API routes
\public // Local assets (minimize usage)
\utils // Constants, Routes, Classes, Dummy Data
\styles // CSS files
```
## 🚀 To Start
Follow these steps to set up your local environment:
```
\\ Clone this repository
git clone https://github.com/cssgunc/compass.git
\\ Go into main folder
cd compass
\\ Install dependencies
npm install
\\ Run local environment
npm run dev
```
Also add following variables inside of a .env file inside of the backend directory
```
\\ .env file contents
POSTGRES_DATABASE=compass
POSTGRES_USER=postgres
POSTGRES_PASSWORD=admin
POSTGRES_HOST=db
POSTGRES_PORT=5432
HOST=localhost
```
## Backend Starter
- Please open the VS Code Command Palette
- 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
### In Dev Container
Run this to reset the database and populate it with the approprate tables that reflect the entities folder
```
python3 -m backend.script.reset_demo
```
### Possible Dev Container Errors
- Sometimes the ports allocated to our services will be allocated (5432 for Postgres and 5050 for PgAdmin4)
- 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
- Run **sudo kill [PID]**
- If you are on Windows please consult ChatGPT or set up WSL (will be very useful in the future)
### Accesing pgAdmin 4
- First go to http://localhost:5050/ on your browser
- Log in using the credentials admin@example.com and admin
- Click **Add New Server**
- Fill in the name field with Compass (can be anything)
- Click **Connection** tab and fill in the following:
- Host name/address: db
- Maintence database: compass
- Username: postgres
- Password: admin
- Click **Save** at the bottom to add connection
- Click **Server** dropdown on the left and click through items inside the **Compass** server
### Testing Backend Code
- 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
- Name all test functions with test\_[testContent] (Must be prefixed with test to be recognized by pytest)
- Utitlize dependency injection for commonly used services
```
\\ Run all tests by being in the backend directory
pytest
\\ 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
pytest -s --rootdir=/workspace [testFilePath]::[testFunctionSignature]
```
## 💡 Dev Notes
- 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'
- 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]"""")
# 🧭 Compass Center's Internal Resource Management App
## 🛠 Technologies
- Next.js
- TailwindCSS
- TypeScript
- Supabase
## 📁 File Setup
```
\compass
\components // Components organized in folders related to specific pages
\pages // Store all pages here
\api // API routes
\public // Local assets (minimize usage)
\utils // Constants, Routes, Classes, Dummy Data
\styles // CSS files
```
## 🚀 To Start
Follow these steps to set up your local environment:
```
\\ Clone this repository
git clone https://github.com/cssgunc/compass.git
\\ Go into main folder
cd compass
\\ Install dependencies
npm install
\\ Run local environment
npm run dev
```
Also add following variables inside of a .env file inside of the backend directory
```
\\ .env file contents
POSTGRES_DATABASE=compass
POSTGRES_USER=postgres
POSTGRES_PASSWORD=admin
POSTGRES_HOST=db
POSTGRES_PORT=5432
HOST=localhost
```
## Backend Starter
- Please open the VS Code Command Palette
- 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
### In Dev Container
Run this to reset the database and populate it with the approprate tables that reflect the entities folder
```
python3 -m backend.script.reset_demo
```
### Possible Dev Container Errors
- Sometimes the ports allocated to our services will be allocated (5432 for Postgres and 5050 for PgAdmin4)
- 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
- Run **sudo kill [PID]**
- If you are on Windows please consult ChatGPT or set up WSL (will be very useful in the future)
### Accesing pgAdmin 4
- First go to http://localhost:5050/ on your browser
- Log in using the credentials admin@example.com and admin
- Click **Add New Server**
- Fill in the name field with Compass (can be anything)
- Click **Connection** tab and fill in the following:
- Host name/address: db
- Maintence database: compass
- Username: postgres
- Password: admin
- Click **Save** at the bottom to add connection
- Click **Server** dropdown on the left and click through items inside the **Compass** server
### Testing Backend Code
- 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
- Name all test functions with test\_[testContent] (Must be prefixed with test to be recognized by pytest)
- Utitlize dependency injection for commonly used services
```
\\ Run all tests by being in the backend directory
pytest
\\ 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
pytest -s --rootdir=/workspace [testFilePath]::[testFunctionSignature]
```
## 💡 Dev Notes
- 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'
- 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."""
import sqlalchemy
from sqlalchemy.orm import Session
from .env import getenv
def _engine_str(database: str = getenv("POSTGRES_DATABASE")) -> str:
"""Helper function for reading settings from environment variables to produce connection string."""
dialect = "postgresql+psycopg2"
user = getenv("POSTGRES_USER")
password = getenv("POSTGRES_PASSWORD")
host = getenv("POSTGRES_HOST")
port = getenv("POSTGRES_PORT")
return f"{dialect}://{user}:{password}@{host}:{port}/{database}"
engine = sqlalchemy.create_engine(_engine_str(), echo=True)
"""Application-level SQLAlchemy database engine."""
def db_session():
"""Generator function offering dependency injection of SQLAlchemy Sessions."""
print("ran")
session = Session(engine)
try:
yield session
finally:
session.close()
"""SQLAlchemy DB Engine and Session niceties for FastAPI dependency injection."""
import sqlalchemy
from sqlalchemy.orm import Session
from .env import getenv
def _engine_str(database: str = getenv("POSTGRES_DATABASE")) -> str:
"""Helper function for reading settings from environment variables to produce connection string."""
dialect = "postgresql+psycopg2"
user = getenv("POSTGRES_USER")
password = getenv("POSTGRES_PASSWORD")
host = getenv("POSTGRES_HOST")
port = getenv("POSTGRES_PORT")
return f"{dialect}://{user}:{password}@{host}:{port}/{database}"
engine = sqlalchemy.create_engine(_engine_str(), echo=True)
"""Application-level SQLAlchemy database engine."""
def db_session():
"""Generator function offering dependency injection of SQLAlchemy Sessions."""
print("ran")
session = Session(engine)
try:
yield session
finally:
session.close()

View File

@ -1,2 +1,10 @@
from .entity_base import EntityBase
from .sample_entity import SampleEntity
from .entity_base import EntityBase
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.
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.
"""
from sqlalchemy.orm import DeclarativeBase
class EntityBase(DeclarativeBase):
pass
"""Abstract superclass of all entities in the application.
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.
"""
from sqlalchemy.orm import DeclarativeBase
class EntityBase(DeclarativeBase):
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.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)
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)

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."""
import os
import dotenv
# Load envirnment variables from .env file upon module start.
dotenv.load_dotenv(f"{os.path.dirname(__file__)}/.env", verbose=True)
def getenv(variable: str) -> str:
"""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
and we intentionally fast error out with a diagnostic message to avoid scenarios of running
the application when expected environment variables are not set.
"""
value = os.getenv(variable)
if value is not None:
return value
else:
raise NameError(f"Error: {variable} Environment Variable not Defined")
"""Load environment variables from a .env file or the process' environment."""
import os
import dotenv
# Load envirnment variables from .env file upon module start.
dotenv.load_dotenv(f"{os.path.dirname(__file__)}/.env", verbose=True)
def getenv(variable: str) -> str:
"""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
and we intentionally fast error out with a diagnostic message to avoid scenarios of running
the application when expected environment variables are not set.
"""
value = os.getenv(variable)
if value is not None:
return value
else:
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
sqlalchemy >=2.0.4, <2.1.0
psycopg2 >=2.9.5, <2.10.0
alembic >=1.10.2, <1.11.0
pytest >=7.2.1, <7.3.0
fastapi[all] >=0.100.0, <0.101.0
sqlalchemy >=2.0.4, <2.1.0
psycopg2 >=2.9.5, <2.10.0
alembic >=1.10.2, <1.11.0
pytest >=7.2.1, <7.3.0
python-dotenv >=1.0.0, <1.1.0

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session
class ResourceService:
def __init__(self, session: Session = Depends(db_session)):
self._session = session

View File

@ -0,0 +1,9 @@
from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session
class ServiceService:
def __init__(self, session: Session = Depends(db_session)):
self._session = session

9
backend/services/tag.py Normal file
View File

@ -0,0 +1,9 @@
from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session
class TagService:
def __init__(self, session: Session = Depends(db_session)):
self._session = session

9
backend/services/user.py Normal file
View File

@ -0,0 +1,9 @@
from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session
class UserService:
def __init__(self, session: Session = Depends(db_session)):
self._session = session

53
backend/test/README.md Normal file
View File

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

View File

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

View File

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

View File

View File

View File

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.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
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).
## 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.

View File

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

View File

@ -1,6 +1,6 @@
import Paper from '@/components/auth/Paper';
export default function RootLayout({
// Layouts must accept a children prop.
@ -11,12 +11,12 @@ export default function RootLayout({
}) {
return (
<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">
{children}
</form>
<p className="text-center mt-6 text-gray-500 text-xs">
&copy; 2024 Compass Center
</p>
</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">
&copy; 2024 Compass Center
</p>
</Paper>
)
}

View File

@ -51,7 +51,7 @@ export default function Page() {
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">
<Input type='email' valid={emailError == ""} title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} required />

View File

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

View File

@ -1,20 +1,16 @@
import '../styles/globals.css';
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Login',
}
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>
)
import '../styles/globals.css';
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>
)
}

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';
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;

View File

@ -6,7 +6,7 @@ interface PageInterface {
const Paper: React.FC<PageInterface> = ({ children }) => {
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}
</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} */
const nextConfig = {}
const nextConfig = {
images: {
domains: ['notioly.com']
},
}
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",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.1",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^5"
}
}
{
"name": "compass",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.1",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^5"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -36,12 +36,61 @@
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('/fonts/Inter-Regular.ttf') format('ttf'),
url('/fonts/Inter-Bold.ttf') format('ttf'),
url('/fonts/Inter-Black.ttf') format('ttf'),
url('/fonts/Inter-ExtraBold.ttf') format('ttf'),
url('/fonts/Inter-ExtraLight.ttf') format('ttf'),
url('/fonts/Inter-Medium.ttf') format('ttf'),
url('/fonts/Inter-SemiBold.ttf') format('ttf'),
url('/fonts/Inter-Thin.ttf') format('ttf');
}
src: url('/fonts/Inter-Regular.ttf') format('truetype');
}
/* Inter-Bold */
@font-face {
font-family: 'Inter';
font-style: normal;
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))',
},
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: [],

View File

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