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,2 +1,10 @@
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

@ -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

@ -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

@ -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

@ -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

@ -9,7 +9,7 @@ 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
assert len(entities.EntityBase.metadata.tables.keys()) == 7
def test_add_sample_data(session: Engine):

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,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

@ -11,7 +11,7 @@ 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">
<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">

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,9 +1,5 @@
import '../styles/globals.css';
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Login',
}
export default function RootLayout({
// Layouts must accept a children prop.

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

@ -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

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