├── README.md ├── backend ├── .env ├── .gitignore ├── constants │ ├── account_constants.py │ └── ad_constant.py ├── controllers │ ├── account_controller.py │ └── ad_controller.py ├── database.py ├── deps │ ├── account_dep.py │ └── ad_dep.py ├── main.py ├── repositories │ ├── account_repository_interface.py │ ├── account_respository.py │ ├── ad_repository.py │ └── ad_repository_interface.py ├── requirements.txt ├── schemas │ ├── account_schema.py │ └── ad_schema.py ├── services │ ├── account_service.py │ ├── ad_service.py │ ├── password_service.py │ ├── password_service_interface.py │ ├── token_service.py │ └── token_service_interface.py ├── settings.py ├── static │ └── images │ │ └── default.jpg └── utils │ └── custom_objectID.py └── frontend ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public ├── fonts │ ├── L0x-DF02iFML4hGCyMqlbS0.woff2 │ ├── L0x-DF02iFML4hGCyMqrbS10ig.woff2 │ ├── L0x4DF02iFML4hGCyMqgXS9sjg.woff2 │ └── L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2 ├── icon-diamond.png └── test.jpg ├── src ├── App.tsx ├── components │ ├── AdCard │ │ ├── AdCard.styled.ts │ │ └── AdCard.tsx │ ├── AdItem │ │ ├── AdItem.styled.ts │ │ └── AdItem.tsx │ ├── AdManager │ │ ├── AdManager.styled.ts │ │ └── AdManager.tsx │ ├── AdsManager │ │ ├── AdsManager.styled.ts │ │ └── AdsManager.tsx │ ├── AuthWrapper │ │ └── AuthWrapper.tsx │ ├── Canvas │ │ ├── CircleCanvas.styled.ts │ │ ├── CircleCanvas.tsx │ │ ├── LineCanvas.styled.ts │ │ └── LineCanvas.tsx │ ├── CartManager │ │ ├── CartManager.styled.ts │ │ └── CartManager.tsx │ ├── Forms │ │ ├── LoginForm │ │ │ ├── LoginForm.styled.ts │ │ │ └── LoginForm.tsx │ │ └── RegisterForm │ │ │ ├── RegisterForm.styled.ts │ │ │ └── RegisterForm.tsx │ ├── Header │ │ ├── Header.motion,.ts │ │ ├── Header.styled.ts │ │ └── Header.tsx │ ├── Layout │ │ ├── Container.styled.ts │ │ ├── Container.tsx │ │ ├── Layout.motion.ts │ │ ├── Layout.styled.ts │ │ └── Layout.tsx │ ├── UserManager │ │ ├── UserManager.styled.ts │ │ └── UserManager.tsx │ └── UserProfile │ │ ├── UserProfile.styled.ts │ │ └── UserProfile.tsx ├── context │ └── AuthContext.tsx ├── hooks │ └── useQueries.ts ├── main.tsx ├── pages │ ├── AccountPage │ │ └── AccountPage.tsx │ ├── AdPage │ │ └── AdPage.tsx │ ├── AdsPage │ │ └── AdsPage.tsx │ ├── CartPage │ │ └── CartPage.tsx │ ├── IndexPage │ │ └── IndexPage.tsx │ └── RegisterPage │ │ └── RegisterPage.tsx ├── styles │ ├── core │ │ ├── fonts.scss │ │ ├── global.scss │ │ ├── reset.scss │ │ └── variables.scss │ ├── index.scss │ └── media.ts ├── types │ ├── global.d.ts │ ├── types.ts │ └── vite-env.d.ts └── utils │ ├── apiQueries │ ├── apiAccount.ts │ └── apiAd.ts │ └── axiosUtil.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.paths.json └── vite.config.ts /README.md: -------------------------------------------------------------------------------- 1 | # ecommerce-project 2 | Ecommerce project - using React, FastAPI & MongoDB 3 | 4 | ## Author 5 | - > Leader & QC - [Ho Xuan Hieu](https://github.com/XuanHieuHo) 6 | - > Frontend - [Do Ngoc Duy Hung](https://github.com/duyhungg) 7 | - > Backend - [Le Kien Trung](https://github.com/kiritoroo) 8 | - > BA & DB - [Nguyen Van Hoai](https://github.com/unreal-world) 9 | - > UI/UX - [La Quy Quan](https://github.com/QuyQuan1) 10 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | DOMAIN='127.0.0.1' 2 | PORT=8080 3 | DB_URI='mongodb://localhost:27017' 4 | SECRET_KEY='12345@abc' 5 | ALGORITHM='HS256' 6 | EXP_TIME=5 -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | fastapi-venv/ 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # pytype static type analyzer 134 | .pytype/ 135 | 136 | # Cython debug symbols 137 | cython_debug/ 138 | 139 | # IDEs and Editors 140 | .idea/ 141 | .vscode/ -------------------------------------------------------------------------------- /backend/constants/account_constants.py: -------------------------------------------------------------------------------- 1 | from fastapi import status 2 | from enum import Enum 3 | from schemas.account_schema import Token, AccountSchema 4 | 5 | """Definition Response Constants for account""" 6 | 7 | account_response_data = { 8 | 'register': { 9 | 'status_code': status.HTTP_201_CREATED, 10 | 'response_description': "Register new Account", 11 | 'response_model': AccountSchema 12 | }, 13 | 'login': { 14 | 'status_code': status.HTTP_200_OK, 15 | 'response_description': "Login Account", 16 | 'response_model': Token 17 | }, 18 | 'detail': { 19 | 'status_code': status.HTTP_200_OK, 20 | 'response_description': "Detail Account", 21 | 'response_model': AccountSchema, 22 | 'response_model_by_alias': False 23 | }, 24 | } 25 | 26 | account_response_exept = { 27 | 'not_found': { 28 | 'status_code': status.HTTP_404_NOT_FOUND, 29 | 'detail': 'User not found' 30 | } 31 | } 32 | 33 | class TokenType(Enum): 34 | ACCESS_TOKEN = 'access_token' 35 | REFRESH_TOKEN = 'refresh_token' -------------------------------------------------------------------------------- /backend/constants/ad_constant.py: -------------------------------------------------------------------------------- 1 | from fastapi import status 2 | from schemas.ad_schema import AdSchema 3 | from typing import Union 4 | 5 | """Definition Response Constants for ad""" 6 | ad_response_data = { 7 | 'show_all': { 8 | 'status_code': status.HTTP_200_OK, 9 | 'response_description': "List all Advertise", 10 | 'response_model': list[AdSchema], 11 | 'response_model_by_alias': False 12 | }, 13 | 'detail': { 14 | 'status_code': status.HTTP_200_OK, 15 | 'response_description': "Detail Advertise", 16 | 'response_model': AdSchema, 17 | 'response_model_by_alias': False 18 | }, 19 | 'create': { 20 | 'status_code': status.HTTP_201_CREATED, 21 | 'response_description': "Create new Advertise", 22 | 'response_model': Union[AdSchema, dict], 23 | 'response_model_by_alias': False 24 | }, 25 | 'update': { 26 | 'status_code': status.HTTP_200_OK, 27 | 'response_description': "Update exists Advertise", 28 | 'response_model': AdSchema, 29 | 'response_model_by_alias': False 30 | }, 31 | 'patch': { 32 | 'status_code': status.HTTP_200_OK, 33 | 'response_description': "Patch exists Advertise", 34 | 'response_model': AdSchema, 35 | 'response_model_by_alias': False 36 | }, 37 | 'delete': { 38 | 'status_code': status.HTTP_204_NO_CONTENT, 39 | 'response_description': "Delete exists Advertise", 40 | } 41 | } 42 | 43 | ad_response_exept = { 44 | 'not_found': { 45 | 'status_code': status.HTTP_404_NOT_FOUND, 46 | 'detail': 'Ad not found' 47 | } 48 | } -------------------------------------------------------------------------------- /backend/controllers/account_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from fastapi.security.oauth2 import OAuth2PasswordRequestForm 3 | from constants.account_constants import account_response_data 4 | from deps.account_dep import get_account_service 5 | from services.account_service import AccountService 6 | from schemas.account_schema import AccountSchemaCreate 7 | from fastapi.security import HTTPBearer 8 | 9 | token_auth_scheme = HTTPBearer() 10 | 11 | """Endpoints for Account""" 12 | account_router = APIRouter(prefix='/api/account', tags=['Account']) 13 | 14 | @account_router.post('/register', **account_response_data.get('register')) 15 | async def register( 16 | user_data: AccountSchemaCreate, 17 | service_data=Depends(get_account_service) 18 | ): 19 | return await AccountService(**service_data) \ 20 | .register(user_data=user_data) 21 | 22 | @account_router.post('/login', **account_response_data.get('login')) 23 | async def login( 24 | form_data: OAuth2PasswordRequestForm = Depends(), 25 | service_data=Depends(get_account_service) 26 | ): 27 | return await AccountService(**service_data) \ 28 | .login(username=form_data.username, password=form_data.password) 29 | 30 | @account_router.get('/user/{user_id}', **account_response_data.get('detail')) 31 | async def detail_user( 32 | user_id: str, 33 | token: str = Depends(token_auth_scheme), 34 | service_data=Depends(get_account_service) 35 | ): 36 | return await AccountService(**service_data) \ 37 | .detail_user(user_id=user_id) 38 | 39 | @account_router.get('/me/{username}', **account_response_data.get('detail')) 40 | async def get_user( 41 | username: str, 42 | token: str = Depends(token_auth_scheme), 43 | service_data=Depends(get_account_service) 44 | ): 45 | return await AccountService(**service_data) \ 46 | .get_user(username=username) -------------------------------------------------------------------------------- /backend/controllers/ad_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from constants.ad_constant import ad_response_data 3 | from deps.ad_dep import get_ad_service 4 | from services.ad_service import AdService 5 | from schemas.ad_schema import AdSchemaCreate, AdSchemaPatch, AdSchemaUpdate 6 | 7 | """Endpoints for Ad""" 8 | ad_router = APIRouter(prefix='/api/ad', tags=['Advertise']) 9 | 10 | """Return all schemas.ad""" 11 | @ad_router.get('/', **ad_response_data.get('show_all')) 12 | async def retrieve_ads( 13 | service_data=Depends(get_ad_service) 14 | ): 15 | return await AdService(**service_data) \ 16 | .retrieve_ads() 17 | 18 | """Detail of schemas.ad""" 19 | @ad_router.get('/{ad_id}', **ad_response_data.get('detail')) 20 | async def detail_ad( 21 | ad_id: str, 22 | service_data=Depends(get_ad_service) 23 | ): 24 | return await AdService(**service_data) \ 25 | .detail_ad(ad_id=ad_id) 26 | 27 | """Create new schemas.ad""" 28 | @ad_router.post('/', **ad_response_data.get('create')) 29 | async def create_ad( 30 | ad_data: AdSchemaCreate, 31 | service_data=Depends(get_ad_service) 32 | ): 33 | return await AdService(**service_data) \ 34 | .create_ad(ad_data=ad_data) 35 | 36 | """Update exists schemas.ad""" 37 | @ad_router.put('/{ad_id}', **ad_response_data.get('update')) 38 | async def update_ad( 39 | ad_id: str, 40 | ad_data: AdSchemaUpdate, 41 | service_data=Depends(get_ad_service) 42 | ): 43 | return await AdService(**service_data) \ 44 | .update_ad(ad_id=ad_id, ad_data=ad_data) 45 | 46 | """Patch exists schemas.ad""" 47 | @ad_router.patch('/{ad_id}', **ad_response_data.get('patch')) 48 | async def patch_ad( 49 | ad_id: str, 50 | ad_data: AdSchemaPatch, 51 | service_data=Depends(get_ad_service) 52 | ): 53 | return await AdService(**service_data) \ 54 | .patch_ad(ad_id=ad_id, ad_data=ad_data) 55 | 56 | """Delete exists schemas.ad""" 57 | @ad_router.delete('/{ad_id}', **ad_response_data.get('delete')) 58 | async def delete_ad( 59 | ad_id:str, 60 | service_data=Depends(get_ad_service) 61 | ): 62 | return await AdService(**service_data) \ 63 | .delete_ad(ad_id=ad_id) -------------------------------------------------------------------------------- /backend/database.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | import os 3 | 4 | client = motor.motor_asyncio.AsyncIOMotorClient(os.environ.get('DB_URI')) 5 | 6 | database = client.auction_site 7 | 8 | print(database) -------------------------------------------------------------------------------- /backend/deps/account_dep.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | from passlib.context import CryptContext 3 | 4 | from repositories.account_respository import AccountRepository 5 | from services.password_service import PasswordService 6 | from services.token_service import TokenService 7 | from database import database 8 | 9 | account_collection = database.get_collection('account') 10 | 11 | async def get_account_collection(): 12 | yield account_collection 13 | 14 | async def get_account_service( 15 | _account_collection=Depends(get_account_collection) 16 | ): 17 | yield { 18 | 'repository': AccountRepository(account_collection=_account_collection), 19 | 'password_service': PasswordService(context=CryptContext(schemes=['bcrypt'], deprecated='auto')), 20 | 'token_service': TokenService() 21 | } -------------------------------------------------------------------------------- /backend/deps/ad_dep.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | 3 | from repositories.ad_repository import AdRepository 4 | from database import database 5 | 6 | ad_collection = database.get_collection('ad') 7 | 8 | async def get_ad_collection(): 9 | yield ad_collection 10 | 11 | async def get_ad_service( 12 | _ad_collection=Depends(get_ad_collection) 13 | ): 14 | yield { 'repository': AdRepository(ad_collection=_ad_collection) } -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI, Depends 3 | from fastapi.middleware.cors import CORSMiddleware 4 | from fastapi.staticfiles import StaticFiles 5 | from dotenv import load_dotenv 6 | from fastapi.security import HTTPBearer 7 | 8 | import os 9 | 10 | from settings import origins 11 | 12 | from controllers import ad_controller 13 | from controllers import account_controller 14 | 15 | load_dotenv() 16 | 17 | token_auth_scheme = HTTPBearer() 18 | 19 | app = FastAPI( 20 | # openapi_url='/docs', 21 | title='Jewerly Auction Site', 22 | version='0.5.2' 23 | ) 24 | 25 | app.mount("/static", StaticFiles(directory="static"), name="static") 26 | 27 | app.include_router(ad_controller.ad_router) 28 | app.include_router(account_controller.account_router) 29 | 30 | app.add_middleware( 31 | CORSMiddleware, 32 | allow_origins=origins, 33 | allow_credentials=True, 34 | allow_methods=["*"], 35 | allow_headers=["*"] 36 | ) 37 | 38 | @app.get("/", tags=['Root']) 39 | async def root( 40 | token: str = Depends(token_auth_scheme) 41 | ) -> dict: 42 | """Test Endpoint""" 43 | return { "message": "Jewerly Auction Site" } 44 | 45 | if __name__ == "__main__": 46 | uvicorn.run( 47 | "main:app", 48 | host=os.environ.get('DOMAIN'), 49 | port=int(os.environ.get('PORT')), 50 | log_level="info", 51 | reload=True 52 | ) -------------------------------------------------------------------------------- /backend/repositories/account_repository_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from schemas.account_schema import AccountSchemaCreate 3 | 4 | class AdRepositoryInterface(ABC): 5 | @abstractmethod 6 | async def detail_user(self, user_id: str): 7 | pass 8 | 9 | @abstractmethod 10 | async def save_user(self, user: AccountSchemaCreate): 11 | pass 12 | 13 | @abstractmethod 14 | async def get_user(self, username: str): 15 | pass 16 | -------------------------------------------------------------------------------- /backend/repositories/account_respository.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from .account_repository_interface import AdRepositoryInterface 3 | from schemas.account_schema import AccountSchemaCreate 4 | from constants.account_constants import account_response_exept 5 | from bson import ObjectId 6 | import datetime 7 | 8 | """CRUD operations to be used by endpoints for Account""" 9 | class AccountRepository(AdRepositoryInterface): 10 | def __init__(self, account_collection) -> None: 11 | self._account_collection = account_collection 12 | 13 | """Get detail of models.account from collection.account""" 14 | async def detail_user(self, user_id: str): 15 | if not ObjectId.is_valid(user_id): 16 | raise HTTPException(**account_response_exept.get('not_found')) 17 | data = { 18 | 'filter': { 19 | '_id': ObjectId(user_id) 20 | } 21 | } 22 | if not (user := await self._account_collection.find_one(**data)): 23 | raise HTTPException(**account_response_exept.get('not_found')) 24 | return user 25 | 26 | """Register new models.account from schemas.AccountSchemaCreate""" 27 | async def save_user(self, user: AccountSchemaCreate): 28 | data = dict( 29 | **user.dict(exclude_none=True), 30 | created_at=datetime.datetime.utcnow(), 31 | is_admin=False 32 | ) 33 | result = await self._account_collection.insert_one(document=data) 34 | return await self.detail_user(user_id=result.inserted_id) 35 | 36 | """Get detail of models.account by username""" 37 | async def get_user(self, username: str): 38 | data = { 39 | 'filter': { 40 | 'username': username 41 | } 42 | } 43 | return await self._account_collection.find_one(**data) -------------------------------------------------------------------------------- /backend/repositories/ad_repository.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from .ad_repository_interface import AdRepositoryInterface 3 | from schemas.ad_schema import AdSchemaCreate, AdSchemaPatch, AdSchemaUpdate 4 | from constants.ad_constant import ad_response_exept 5 | from bson import ObjectId 6 | import datetime 7 | 8 | """CRUD operations to be used by endpoints for Ad""" 9 | class AdRepository(AdRepositoryInterface): 10 | def __init__(self, ad_collection) -> None: 11 | self._ad_collection = ad_collection 12 | 13 | """Get all models.ad from collection.ad""" 14 | async def retrieve_ads(self): 15 | cursor = self._ad_collection.find() 16 | return [ad async for ad in cursor] 17 | 18 | """Get detail of models.ad from collection.ad""" 19 | async def detail_ad(self, ad_id: str): 20 | if not ObjectId.is_valid(ad_id): 21 | raise HTTPException(**ad_response_exept.get('not_found')) 22 | data = { 23 | 'filter': { 24 | '_id': ObjectId(ad_id) 25 | } 26 | } 27 | if not (ad := await self._ad_collection.find_one(**data)): 28 | raise HTTPException(**ad_response_exept.get('not_found')) 29 | return ad 30 | 31 | """Create new models.ad from schemas.AdSchemaCreate""" 32 | async def create_ad(self, ad: AdSchemaCreate): 33 | data = dict( 34 | **ad.dict(exclude_none=True), 35 | created_at=datetime.datetime.utcnow()) 36 | result = await self._ad_collection.insert_one(document=data) 37 | return await self.detail_ad(ad_id=result.inserted_id) 38 | 39 | """Update existing models.ad from schemas.AdSchemaUpdate""" 40 | async def update_ad(self, ad_id: str, ad_data: AdSchemaUpdate): 41 | if not ObjectId.is_valid(ad_id): 42 | raise HTTPException(**ad_response_exept.get('not_found')) 43 | data = { 44 | 'filter': { 45 | '_id': ObjectId(ad_id) 46 | }, 47 | 'update': { 48 | '$set': ad_data.dict(exclude_none=True) 49 | }, 50 | 'return_document': True 51 | } 52 | return await self._ad_collection.find_one_and_update(**data) 53 | 54 | """Patch existing models.ad from schemas.AdSchemaUpdate""" 55 | async def patch_ad(self, ad_id: str, ad_data: AdSchemaPatch): 56 | if not ObjectId.is_valid(ad_id): 57 | raise HTTPException(**ad_response_exept.get('not_found')) 58 | data = { 59 | 'filter': { 60 | '_id': ObjectId(ad_id) 61 | }, 62 | 'update': { 63 | '$set': ad_data.dict(exclude_none=True) 64 | }, 65 | 'return_document': True 66 | } 67 | return await self._ad_collection.find_one_and_update(**data) 68 | 69 | """Delete an ad from collection.ad by `ad_id`""" 70 | async def delete_ad(self, ad_id: str): 71 | if not ObjectId.is_valid(ad_id): 72 | raise HTTPException(**ad_response_exept.get('not_found')) 73 | data = { 74 | 'filter': { 75 | '_id': ObjectId(ad_id) 76 | } 77 | } 78 | if not (_ := await self._ad_collection.find_one(**data)): 79 | raise HTTPException(**ad_response_exept.get('not_found')) 80 | result = await self._ad_collection.delete_one(**data) 81 | 82 | return False if not result.deleted_count else True 83 | 84 | 85 | -------------------------------------------------------------------------------- /backend/repositories/ad_repository_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from schemas.ad_schema import AdSchemaCreate, AdSchemaPatch, AdSchemaUpdate 3 | 4 | class AdRepositoryInterface(ABC): 5 | @abstractmethod 6 | async def retrieve_ads(self): 7 | pass 8 | 9 | @abstractmethod 10 | async def detail_ad(self, ad_id: str): 11 | pass 12 | 13 | @abstractmethod 14 | async def create_ad(self, ad: AdSchemaCreate): 15 | pass 16 | 17 | @abstractmethod 18 | async def update_ad(self, ad_id: str, ad_data: AdSchemaUpdate): 19 | pass 20 | 21 | @abstractmethod 22 | async def patch_ad(self, ad_id: str, ad_data: AdSchemaPatch): 23 | pass 24 | 25 | @abstractmethod 26 | async def delete_ad(self, ad_id: str): 27 | pass -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.87.0 2 | uvicorn==0.19.0 3 | motor==3.0.0 4 | pydantic==1.10.2 5 | pymongo==4.3.2 6 | python-dotenv==0.21.0 7 | bson 8 | pydantic[email] 9 | python-jose==3.3.0 10 | passlib==1.7.4 11 | bcrypt==3.2.2 12 | python-multipart==0.0.5 -------------------------------------------------------------------------------- /backend/schemas/account_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, Field 2 | from typing import Optional 3 | from datetime import datetime 4 | from utils.custom_objectID import ObjID 5 | 6 | class AccountSchemaBase(BaseModel): 7 | username: str 8 | email: Optional[EmailStr] 9 | phone: Optional[int] 10 | 11 | class AccountSchemaCreate(AccountSchemaBase): 12 | password: str = Field(..., min_length=5) 13 | 14 | class AccountSchema(AccountSchemaBase): 15 | id: ObjID = Field(default_factory=ObjID, alias='_id') 16 | created_at: datetime 17 | is_admin: bool 18 | 19 | class Config: 20 | json_encoders = { 21 | ObjID: lambda x: str(x), 22 | datetime: lambda x: x.strftime('%Y:%m:%d %H:%M') 23 | } 24 | 25 | class Token(BaseModel): 26 | access_token: str 27 | refresh_token: str -------------------------------------------------------------------------------- /backend/schemas/ad_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Optional 3 | from datetime import datetime 4 | from utils.custom_objectID import ObjID 5 | 6 | class AdSchemaPatch(BaseModel): 7 | """Schema for patch ad""" 8 | current_price: int 9 | status: Optional[str] 10 | 11 | class AdSchemaUpdate(AdSchemaPatch): 12 | """Schema for update ad""" 13 | product_name: str 14 | description: Optional[str] 15 | category: Optional[str] 16 | base_price: int 17 | image: Optional[str] 18 | 19 | class AdSchemaCreate(AdSchemaUpdate): 20 | """Schema for create ad""" 21 | created_by: str 22 | 23 | class AdSchema(AdSchemaCreate): 24 | """Schema for ad""" 25 | id: ObjID = Field(default_factory=ObjID, alias='_id') 26 | created_at: datetime 27 | 28 | class Config: 29 | validate_assignment = True 30 | json_encoders = { 31 | ObjID: lambda x: str(x), 32 | datetime: lambda x: x.strftime('%Y:%m:%d %H:%M') 33 | } 34 | -------------------------------------------------------------------------------- /backend/services/account_service.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | 3 | from repositories.account_repository_interface import AdRepositoryInterface 4 | from schemas.account_schema import AccountSchemaCreate, AccountSchemaBase 5 | from .password_service_interface import PasswordServiceInterface 6 | from .token_service_interface import TokenServiceInterface 7 | from constants.account_constants import TokenType 8 | import os 9 | 10 | class AccountService: 11 | def __init__(self, 12 | repository: AdRepositoryInterface, 13 | password_service: PasswordServiceInterface, 14 | token_service: TokenServiceInterface 15 | ) -> None: 16 | self._repository = repository 17 | self._password_service = password_service 18 | self._token_service = token_service 19 | 20 | async def _create_token_data(self, username: str, token_type: TokenType, exp_time: int): 21 | return await self._token_service.encode_token( 22 | username=username, 23 | token_type=token_type, 24 | secret_key=os.environ.get('SECRET_KEY'), 25 | algorithm=os.environ.get('ALGORITHM'), 26 | exp_time=int(os.environ.get('EXP_TIME')) 27 | ) 28 | 29 | async def _authenticate(self, username: str, password: str): 30 | if not (user := await self._repository.get_user(username=username)) \ 31 | or not await self._password_service \ 32 | .verify_passwords(plain_password=password, hashed_password=user['password']): 33 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Incorrect username or password') 34 | return user 35 | 36 | async def detail_user(self, user_id: str): 37 | return await self._repository.detail_user(user_id=user_id) 38 | 39 | async def login(self, username: str, password: str): 40 | user = await self._authenticate(username=username, password=password) 41 | access_token = await self._create_token_data( 42 | username=user['username'], 43 | token_type=TokenType.ACCESS_TOKEN, 44 | exp_time=int(os.environ.get('EXP_TIME'))) 45 | 46 | refresh_toekn = await self._create_token_data( 47 | username=user['username'], 48 | token_type=TokenType.REFRESH_TOKEN, 49 | exp_time=10) 50 | return { 51 | 'access_token': access_token, 52 | 'refresh_token': refresh_toekn 53 | } 54 | 55 | async def register(self, user_data: AccountSchemaCreate): 56 | if await self._repository.get_user(username=user_data.username): 57 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='User with this username exists') 58 | hashed_password = await self._password_service.hashed_password(plain_password=user_data.password) 59 | user_data.password = hashed_password 60 | return await self._repository.save_user(user=user_data) 61 | 62 | 63 | async def get_user(self, username: str): 64 | return await self._repository.get_user(username=username) 65 | -------------------------------------------------------------------------------- /backend/services/ad_service.py: -------------------------------------------------------------------------------- 1 | from repositories.ad_repository_interface import AdRepositoryInterface 2 | from schemas.ad_schema import AdSchemaCreate, AdSchemaPatch, AdSchemaUpdate 3 | 4 | class AdService: 5 | def __init__(self, repository: AdRepositoryInterface ) -> None: 6 | self._repository = repository 7 | 8 | async def retrieve_ads(self): 9 | return await self._repository.retrieve_ads() 10 | 11 | async def detail_ad(self, ad_id: str): 12 | return await self._repository.detail_ad(ad_id=ad_id) 13 | 14 | async def create_ad(self, ad_data: AdSchemaCreate): 15 | return await self._repository.create_ad(ad=ad_data) 16 | 17 | async def update_ad(self, ad_id: str, ad_data: AdSchemaUpdate): 18 | return await self._repository.update_ad(ad_id=ad_id, ad_data=ad_data) 19 | 20 | async def patch_ad(self, ad_id: str, ad_data: AdSchemaPatch): 21 | return await self._repository.patch_ad(ad_id=ad_id, ad_data=ad_data) 22 | 23 | async def delete_ad(self, ad_id: str): 24 | return await self._repository.delete_ad(ad_id=ad_id) -------------------------------------------------------------------------------- /backend/services/password_service.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | from .password_service_interface import PasswordServiceInterface 3 | 4 | class PasswordService(PasswordServiceInterface): 5 | def __init__(self, context: CryptContext): 6 | self._context = context 7 | 8 | async def verify_passwords(self, plain_password: str, hashed_password: str) -> bool: 9 | return self._context.verify_and_update(secret=plain_password, hash=hashed_password) 10 | 11 | async def hashed_password(self, plain_password: str) -> str: 12 | return self._context.hash(secret=plain_password) -------------------------------------------------------------------------------- /backend/services/password_service_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class PasswordServiceInterface(ABC): 4 | @abstractmethod 5 | async def verify_passwords(self, plain_password: str, hashed_password: str) -> bool: 6 | pass 7 | 8 | @abstractmethod 9 | async def hashed_password(self, plain_password: str) -> str: 10 | pass 11 | -------------------------------------------------------------------------------- /backend/services/token_service.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | from jose import JWTError, jwt 3 | from constants.account_constants import TokenType 4 | from .token_service_interface import TokenServiceInterface 5 | from datetime import datetime, timedelta 6 | 7 | def token_exception(token_type: str, error_detail: str, headers: dict = None): 8 | def decorator(func): 9 | async def wrapper(*args, **kwargs): 10 | try: 11 | return await func(*args, **kwargs) 12 | except jwt.ExpiredSignatureError: 13 | raise HTTPException( 14 | status_code=status.HTTP_401_UNAUTHORIZED, 15 | detail=f'{token_type.capitalize()} expired') 16 | except JWTError: 17 | raise HTTPException( 18 | status_code=status.HTTP_401_UNAUTHORIZED, 19 | detail=error_detail, headers=headers) 20 | return wrapper 21 | return decorator 22 | 23 | class TokenService(TokenServiceInterface): 24 | @token_exception( 25 | token_type='access_token', 26 | error_detail='Could not validate credentials') 27 | async def decode_access_token(self, access_token: str, secret_key: str, algorithm: str) -> dict: 28 | payload: dict = jwt.decode( 29 | token=access_token, 30 | key=secret_key, 31 | algorithms=algorithm) 32 | if payload.get('token_type') == TokenType.ACCESS_TOKEN.value: 33 | return payload 34 | 35 | @token_exception( 36 | token_type='refresh_token', 37 | error_detail='Invalid refresh token' 38 | ) 39 | async def decode_refresh_token(self, refresh_token: str, secret_key: str, algorithm: str) -> dict: 40 | payload: dict = jwt.decode( 41 | token=refresh_token, 42 | key=secret_key, 43 | algorithms=algorithm) 44 | if payload.get('token_type') != TokenType.REFRESH_TOKEN.value: 45 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid type for token') 46 | 47 | async def encode_token(self, username: str, secret_key: str, algorithm: str, exp_time: int, token_type: TokenType): 48 | expire_time = datetime.utcnow() + timedelta(minutes=exp_time) 49 | data = { 50 | 'sub': username, 51 | 'exp': expire_time, 52 | 'token_type': token_type.value 53 | } 54 | return jwt.encode(data, key=secret_key, algorithm=algorithm) -------------------------------------------------------------------------------- /backend/services/token_service_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from constants.account_constants import TokenType 3 | 4 | 5 | class TokenServiceInterface(ABC): 6 | @abstractmethod 7 | async def decode_access_token(self, access_token: str, secret_key: str, algorithm: str) -> dict: 8 | pass 9 | 10 | @abstractmethod 11 | async def decode_refresh_token(self, refresh_token: str, secret_key: str, algorithm: str) -> dict: 12 | pass 13 | 14 | @abstractmethod 15 | async def encode_token( self, username: str, secret_key: str, algorithm: str, exp_time: int, token_type: TokenType): 16 | pass 17 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | origins = [ 2 | "http://localhost:5173", 3 | "https://localhost:5173", 4 | "http://127.0.0.1:5173", 5 | "https://127.0.0.1:5173" 6 | ] -------------------------------------------------------------------------------- /backend/static/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/backend/static/images/default.jpg -------------------------------------------------------------------------------- /backend/utils/custom_objectID.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | 3 | class ObjID(ObjectId): 4 | @classmethod 5 | def __get_validators__(cls): 6 | yield cls.validate 7 | 8 | @classmethod 9 | def validate(cls, v): 10 | if not isinstance(v, ObjectId): 11 | raise TypeError('ObjectId required') 12 | return str(v) 13 | 14 | @classmethod 15 | def __modify_schema__(cls, field_schema): 16 | field_schema.update(type="string") -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jewerly Auction Site 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@types/react-router-dom": "^5.3.3", 13 | "axios": "^1.1.3", 14 | "bootstrap": "^5.2.3", 15 | "classnames": "^2.3.2", 16 | "framer-motion": "^7.6.7", 17 | "jwt-decode": "^3.1.2", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-fast-marquee": "^1.3.5", 21 | "react-query": "^3.39.2", 22 | "react-router-dom": "^6.4.3", 23 | "react-toastify": "^9.1.1", 24 | "sass": "^1.56.1", 25 | "styled-components": "^5.3.6" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.0.17", 29 | "@types/react-dom": "^18.0.6", 30 | "@types/styled-components": "^5.1.26", 31 | "@vitejs/plugin-react": "^2.0.1", 32 | "typescript": "^4.6.4", 33 | "typescript-styled-plugin": "^0.18.2", 34 | "vite": "^3.0.7", 35 | "vite-tsconfig-paths": "^3.5.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/public/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2 -------------------------------------------------------------------------------- /frontend/public/icon-diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/icon-diamond.png -------------------------------------------------------------------------------- /frontend/public/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/public/test.jpg -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { QueryClientProvider, QueryClient } from "react-query"; 3 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 4 | import { AnimatePresence } from "framer-motion"; 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | 8 | import { AuthContextProvider } from "./context/AuthContext"; 9 | import { AuthWrapper } from "@comp/AuthWrapper/AuthWrapper"; 10 | 11 | import IndexPage from "@page/IndexPage/IndexPage"; 12 | import AdsPage from "@page/AdsPage/AdsPage"; 13 | import AdPage from "@page/AdPage/AdPage"; 14 | import AccountPage from "@page/AccountPage/AccountPage"; 15 | import RegisterPage from "@page/RegisterPage/RegisterPage"; 16 | import CartPage from "@page/CartPage/CartPage"; 17 | export default function App() { 18 | const queryClient = new QueryClient({ 19 | defaultOptions: { 20 | queries: { 21 | refetchOnWindowFocus: false, 22 | }, 23 | }, 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | } /> 34 | } /> 35 | 36 | } /> 37 | } /> 38 | } /> 39 | 40 | } /> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/components/AdCard/AdCard.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const HoverWrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | background-color: #fff; 8 | transform: scale(0); 9 | transition: all 0.3s ease; 10 | opacity: 0; 11 | position: absolute; 12 | top: 30%; 13 | left: 1em; 14 | right: 1em; 15 | padding: 3em 1em; 16 | z-index: 3; 17 | ` 18 | 19 | export const Button = styled(Link)` 20 | border: var(--b-md) solid; 21 | font-family: "Urbanist", sans-serif; 22 | background: linear-gradient(to left, white 50%, salmon 50%) right; 23 | background-size: 200%; 24 | transition: .3s ease-out; 25 | padding: 1em 0.5em; 26 | color: #000; 27 | font-size: 1.2em; 28 | font-weight: 500; 29 | text-decoration: none; 30 | text-align: center; 31 | &:hover { 32 | background-position: left; 33 | cursor: crosshair; 34 | } 35 | ` 36 | 37 | export const Wrapper = styled.div` 38 | display: flex; 39 | flex-direction: column; 40 | border: var(--b-md) solid; 41 | padding: 1em; 42 | background-color: #fff; 43 | box-shadow: 0.5em 0.5em 0 0 #ddd; 44 | position: relative; 45 | transition: 0.3s ease; 46 | &::after { 47 | display: block; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | content: ""; 52 | width: 100%; 53 | height: 100%; 54 | z-index: 2; 55 | background-color: rgba(0, 0, 0, 0.25); 56 | /* transition: 0.3s ease; */ 57 | transform: scale(0); 58 | } 59 | &:hover { 60 | box-shadow: 0 0 0 0; 61 | ${HoverWrapper} { 62 | opacity: 1; 63 | transform: scale(1); 64 | } 65 | } 66 | &:hover::after { 67 | transform: scale(1); 68 | } 69 | ` 70 | 71 | export const Title = styled.h3` 72 | margin-top: 1em; 73 | font-size: 1.5em; 74 | ` 75 | 76 | export const ImageWrapper = styled.figure` 77 | order: -1; 78 | position: relative; 79 | ` 80 | 81 | export const Image = styled.img` 82 | max-width: 100%; 83 | width: 100%; 84 | display: block; 85 | ` 86 | 87 | export const ImageCaption = styled.figcaption` 88 | position: absolute; 89 | bottom: 0; 90 | background-color: #fff; 91 | padding: 0.5em 1em 0.5em 0.25em; 92 | border-radius: 0 0.75em 0 0; 93 | z-index: 1; 94 | font-size: 1.2em; 95 | margin-left: -1px; 96 | margin-bottom: -1px; 97 | ` 98 | 99 | export const Description = styled.p` 100 | margin-top: 0.5em; 101 | font-size: 0.875em; 102 | ` 103 | 104 | export const PriceWrapper = styled.div` 105 | display: flex; 106 | justify-content: space-between; 107 | align-items: center; 108 | flex-wrap: wrap; 109 | margin-top: 1.5em; 110 | margin-bottom: 1.5em; 111 | ` 112 | 113 | export const Price = styled.p` 114 | background-color: #404040; 115 | color: #fff; 116 | padding: 0.875em; 117 | display: inline-flex; 118 | align-items: center; 119 | justify-content: center; 120 | margin-left: -1.5em; 121 | padding-left: 1em; 122 | font-size: 1.2em; 123 | font-weight: 600; 124 | ` 125 | 126 | export const Status = styled.p` 127 | background-color: #ddd; 128 | padding: 0.875em; 129 | display: inline-flex; 130 | align-items: center; 131 | justify-content: center; 132 | ` 133 | 134 | export const Category = styled.p` 135 | 136 | ` 137 | 138 | export const Time = styled.p` 139 | 140 | ` -------------------------------------------------------------------------------- /frontend/src/components/AdCard/AdCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BackendAd } from "@type/types"; 3 | import { useAuthContext } from '@context/AuthContext' 4 | 5 | import * as S from './AdCard.styled' 6 | 7 | interface Props { 8 | ad: BackendAd 9 | } 10 | 11 | export const AdCard = (props: Props) => { 12 | const { ad } = props 13 | const { user } = useAuthContext() 14 | 15 | return ( 16 | <> 17 | 18 | 19 | {ad.product_name} 20 | 21 | 22 | 23 | @{ad.created_by} 24 | 25 | {ad.description} 26 | 27 | ${ad.current_price} 28 | # {ad.status} 29 | 30 | Remaining: 22m 10s 31 | { ad.status === 'ongoing'? ( 32 | 33 | Tham gia 34 | 35 | ): null } 36 | 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /frontend/src/components/AdItem/AdItem.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | padding: 1em; 7 | background-color: #fff; 8 | position: relative; 9 | transition: 0.3s ease; 10 | ` 11 | 12 | export const Title = styled.h3` 13 | margin-top: 1em; 14 | margin-bottom: 1em; 15 | font-size: 2em; 16 | font-weight: 400; 17 | ` 18 | 19 | export const ImageWrapper = styled.figure` 20 | order: 1; 21 | position: relative; 22 | ` 23 | 24 | export const Image = styled.img` 25 | max-width: 30%; 26 | width: 100%; 27 | display: block; 28 | ` 29 | 30 | export const ImageCaption = styled.figcaption` 31 | position: absolute; 32 | bottom: 0; 33 | background-color: #fff; 34 | padding: 0.5em 1.5em 0.5em 0.5em; 35 | border-radius: 0 0.75em 0 0; 36 | z-index: 1; 37 | font-size: 1.5em; 38 | margin-left: -1px; 39 | margin-bottom: -1px; 40 | ` -------------------------------------------------------------------------------- /frontend/src/components/AdItem/AdItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { BackendAd } from "@type/types"; 3 | 4 | import * as S from './AdItem.styled' 5 | 6 | interface Props { 7 | ad: BackendAd 8 | } 9 | 10 | export const AdItem = (props: Props) => { 11 | const { ad } = props 12 | 13 | return ( 14 | <> 15 | { ad ? ( 16 | 17 | {ad.product_name} 18 | 19 | 20 | @{ad.created_by} 21 | 22 | 23 | ): null } 24 | 25 | ) 26 | } -------------------------------------------------------------------------------- /frontend/src/components/AdManager/AdManager.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | width: 95%; 5 | height: 95%; 6 | margin-left: auto; 7 | margin-right: auto; 8 | margin-top: 2em; 9 | margin-bottom: 2em; 10 | ` 11 | 12 | export const Grid = styled.div` 13 | display: grid; 14 | transition: all 0.2s ease-in-out; 15 | margin: auto; 16 | grid-gap: 1.25em; 17 | grid-template-columns: repeat(4, 1fr); 18 | grid-template-rows: repeat(2, 0.9fr); 19 | grid-template-areas: 20 | "log item item item" 21 | "log chat chat user"; 22 | ` 23 | 24 | export const WrapperItem = styled.div` 25 | grid-area: item; 26 | border: var(--b-md) solid; 27 | box-shadow: 0.5em 0.5em 0 0 #ddd; 28 | ` 29 | 30 | export const WrapperLog = styled.div` 31 | grid-area: log; 32 | border: var(--b-md) solid; 33 | ` 34 | 35 | export const WrapperUser = styled.div` 36 | grid-area: user; 37 | border: var(--b-md) solid; 38 | ` 39 | 40 | export const WrapperChat = styled.div` 41 | grid-area: chat; 42 | border: var(--b-md) solid; 43 | ` -------------------------------------------------------------------------------- /frontend/src/components/AdManager/AdManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from 'react-router-dom' 3 | 4 | import { useGetAd } from "@hook/useQueries"; 5 | import { AdItem } from "@comp/AdItem/AdItem" 6 | 7 | import * as S from './AdManager.styled' 8 | 9 | export const AdManager = () => { 10 | const { adId } = useParams() 11 | const { refetch, data, status } = useGetAd(adId!) 12 | 13 | useEffect(() => { 14 | console.log(data) 15 | }, [data]) 16 | 17 | return ( 18 | <> 19 | 20 | { data?.data ? ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ): null } 36 | 37 | 38 | 39 | ) 40 | } -------------------------------------------------------------------------------- /frontend/src/components/AdsManager/AdsManager.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | width: 90%; 5 | margin-left: auto; 6 | margin-right: auto; 7 | padding-bottom: 5em; 8 | text-align: center; 9 | ` 10 | 11 | export const WrapperAd = styled.div` 12 | margin-top: 10em; 13 | display: grid; 14 | grid-template-columns: repeat(3, 1fr); 15 | row-gap: 1.5em; 16 | column-gap: 2em; 17 | align-items: flex-start; 18 | text-align: justify; 19 | ` 20 | 21 | export const FilterWrapper = styled.div` 22 | position: absolute; 23 | top: clamp(100px, 10%, 200px); 24 | left: 10%; 25 | z-index: 3; 26 | ` 27 | 28 | export const Dropdown = styled.div` 29 | display: inline-block; 30 | margin: 20px 50px; 31 | label { 32 | border: 3px solid; 33 | position: relative; 34 | z-index: 2; 35 | transition: all 0.3s; 36 | backface-visibility: hidden; 37 | font-weight: 500; 38 | } 39 | input { 40 | transition: all 0.3s; 41 | backface-visibility: hidden; 42 | display: none; 43 | &:checked + label { 44 | background: #FA8072; 45 | } 46 | &:checked ~ ul { 47 | visibility: visible; 48 | opacity: 1; 49 | top: 0; 50 | li { 51 | border-left: 2px solid; 52 | border-right: 2px solid; 53 | border-bottom: 2px solid; 54 | } 55 | } 56 | } 57 | ul { 58 | position: relative; 59 | visibility: hidden; 60 | opacity: 0; 61 | top: -20px; 62 | z-index: 1; 63 | transition: all 0.3s; 64 | backface-visibility: hidden; 65 | li { 66 | border: 2px solid #fff; 67 | transition: all 0.3s; 68 | backface-visibility: hidden; 69 | } 70 | } 71 | label, li { 72 | display: block; 73 | width: clamp(100px, 20vw, 200px); 74 | background: #FFF; 75 | padding: 15px 20px; 76 | &:hover { 77 | background: #FA8072; 78 | border-left: 2px solid; 79 | border-right: 2px solid; 80 | border-bottom: 2px solid; 81 | cursor: crosshair; 82 | } 83 | } 84 | ` 85 | 86 | export const Search = styled.div` 87 | 88 | ` -------------------------------------------------------------------------------- /frontend/src/components/AdsManager/AdsManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import { useGetAds } from "@hook/useQueries"; 4 | import { BackendAd } from "@type/types"; 5 | import { AdCard } from "@comp/AdCard/AdCard"; 6 | 7 | import * as S from './AdsManager.styled' 8 | 9 | export const AdsManager = () => { 10 | const [checked, setChecked] = useState(false); 11 | 12 | const { refetch, data, status } = useGetAds() 13 | 14 | const [categories, setCategories] = useState([]); 15 | 16 | useEffect(() => { 17 | setCategories(Array.from(new Set(data?.data.map((ad: BackendAd) => ad.category)))) 18 | }, [data]) 19 | 20 | return ( 21 | <> 22 | 23 | 24 | 25 | console.log('pick')}/> 26 | 27 |
    28 | { categories?.sort().map((category: string) => ( 29 |
  • {category}
  • 30 | )) } 31 |
32 |
33 | 34 | 35 | 36 |
37 | 38 | 39 | { data?.data.map((ad: BackendAd) => ( 40 | 41 | )) } 42 | 43 |
44 | 45 | ) 46 | } -------------------------------------------------------------------------------- /frontend/src/components/AuthWrapper/AuthWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { useAuthContext } from "@context/AuthContext"; 4 | interface Props { 5 | children: React.ReactNode 6 | } 7 | 8 | export const AuthWrapper = (props: Props) => { 9 | const { children } = props 10 | const [ isChecked, setIsChecked ] = useState(false) 11 | 12 | const { serializeUser } = useAuthContext() 13 | 14 | useEffect(() => { 15 | const jwt = window.localStorage.getItem('JWT') 16 | if (jwt) serializeUser(jwt) 17 | setIsChecked(true) 18 | }, [serializeUser]) 19 | 20 | return ( 21 | <> 22 | { isChecked && children } 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /frontend/src/components/Canvas/CircleCanvas.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Canvas = styled.canvas` 4 | display: block; 5 | position: absolute; 6 | top: 2.5%; 7 | left: 2.5%; 8 | bottom: 0; 9 | right: 0; 10 | width: 95%; 11 | height: 95%; 12 | margin: 0; 13 | z-index: -1; 14 | ` -------------------------------------------------------------------------------- /frontend/src/components/Canvas/CircleCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react" 2 | 3 | import * as S from './CircleCanvas.styled' 4 | 5 | export const CircleCanvas = () => { 6 | const canvasRef = useRef(null); 7 | 8 | function setDPI(canvas: HTMLCanvasElement, dpi: number) { 9 | canvas.style.width = canvas.style.width || canvas.width + 'px' 10 | canvas.style.height = canvas.style.height || canvas.height + 'px' 11 | 12 | var scaleFactor = dpi / 96; 13 | var width = parseFloat(canvas.style.width) 14 | var height = parseFloat(canvas.style.height) 15 | 16 | var oldScale = canvas.width / width 17 | var backupScale = scaleFactor / oldScale 18 | var backup = canvas.cloneNode(false) 19 | backup.getContext('2d').drawImage(canvas, 0, 0) 20 | 21 | var ctx = canvas.getContext('2d')! 22 | canvas.width = Math.ceil(width * scaleFactor); 23 | canvas.height = Math.ceil(height * scaleFactor); 24 | 25 | ctx.setTransform(backupScale, 0, 0, backupScale, 0, 0) 26 | ctx.drawImage(backup, 0, 0); 27 | ctx.setTransform(scaleFactor, 0, 0, scaleFactor, 0, 0) 28 | } 29 | 30 | const drawCircle = (C: CanvasRenderingContext2D, size: number, offsetX: number, offsetY: number) => { 31 | C.lineWidth = 0.2; 32 | C.strokeStyle ='#222222'; 33 | C.save() 34 | C.beginPath() 35 | C.arc(550+offsetX, -offsetY, size, 0, Math.PI * 2, false) 36 | C.stroke() 37 | C.restore() 38 | } 39 | 40 | const draw = (C: CanvasRenderingContext2D, offsetX: number, offsetY: number) => { 41 | C.clearRect(0, 0, C.canvas.width, C.canvas.height) 42 | const size = 200 43 | const count = 3 44 | const spaceX = 300/count + size-size/22 45 | const spaceY = 250/count + size-size/20 46 | for (let i =-count-10; i < count+5; i++) { 47 | const startX = i*spaceX 48 | const startY = i*spaceY 49 | drawCircle(C, size, startX+offsetX, startY+offsetY) 50 | for(let j =-count-10; j < count+5; j++) { 51 | const spaceVertical = 550*j 52 | drawCircle(C, size, startX+offsetX, startY+spaceVertical+offsetY) 53 | } 54 | } 55 | } 56 | 57 | useEffect(() => { 58 | const canvas = canvasRef.current! 59 | const context = canvas.getContext('2d')! 60 | let offsetX = -900 61 | let offsetY = -300 62 | let animationFrameId: number 63 | 64 | // setDPI(canvas, 200) 65 | 66 | const render = () => { 67 | context.canvas.width = window.innerWidth; 68 | context.canvas.height = window.innerHeight; 69 | offsetX+=0.8 70 | offsetY+=0.5 71 | if (offsetX > 800) { 72 | offsetX = -offsetX 73 | offsetY = -offsetY 74 | } 75 | 76 | draw(context, offsetX, offsetY) 77 | animationFrameId = window.requestAnimationFrame(render) 78 | } 79 | 80 | render() 81 | return () => { 82 | window.cancelAnimationFrame(animationFrameId) 83 | } 84 | }, [draw]) 85 | 86 | return ( 87 | }/> 88 | ) 89 | } -------------------------------------------------------------------------------- /frontend/src/components/Canvas/LineCanvas.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Canvas = styled.canvas` 4 | display: block; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | bottom: 0; 9 | right: 0; 10 | width: 100%; 11 | height: 100%; 12 | margin: 0; 13 | z-index: -1; 14 | ` -------------------------------------------------------------------------------- /frontend/src/components/Canvas/LineCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { animate } from "framer-motion" 2 | import React, { useEffect, useRef } from "react" 3 | 4 | import * as S from './LineCanvas.styled' 5 | 6 | export const LineCanvas = () => { 7 | const canvasRef = useRef(null) 8 | 9 | function setDPI(canvas: HTMLCanvasElement, dpi: number) { 10 | canvas.style.width = canvas.style.width || canvas.width + 'px' 11 | canvas.style.height = canvas.style.height || canvas.height + 'px' 12 | 13 | var scaleFactor = dpi / 96; 14 | var width = parseFloat(canvas.style.width) 15 | var height = parseFloat(canvas.style.height) 16 | 17 | var oldScale = canvas.width / width 18 | var backupScale = scaleFactor / oldScale 19 | var backup = canvas.cloneNode(false) 20 | backup.getContext('2d').drawImage(canvas, 0, 0) 21 | 22 | var ctx = canvas.getContext('2d')! 23 | canvas.width = Math.ceil(width * scaleFactor); 24 | canvas.height = Math.ceil(height * scaleFactor); 25 | 26 | ctx.setTransform(backupScale, 0, 0, backupScale, 0, 0) 27 | ctx.drawImage(backup, 0, 0); 28 | ctx.setTransform(scaleFactor, 0, 0, scaleFactor, 0, 0) 29 | } 30 | 31 | const drawLine = (C: CanvasRenderingContext2D, offsetX: number, offsetY: number) => { 32 | C.strokeStyle ='#222222'; 33 | C.stroke() 34 | C.beginPath(); 35 | C.lineWidth = 0.1 36 | C.moveTo(offsetX, 0) 37 | C.lineTo(offsetX, C.canvas.height) 38 | 39 | C.moveTo(0, -offsetY) 40 | C.lineTo(C.canvas.width, -offsetY) 41 | C.closePath() 42 | 43 | C.stroke() 44 | C.beginPath() 45 | C.lineWidth = 0.1 46 | C.moveTo(offsetX-C.canvas.width*2, -offsetY-C.canvas.height*2); 47 | C.lineTo(offsetX+C.canvas.width*2, -offsetY+C.canvas.height*2) 48 | } 49 | 50 | const draw = (C: CanvasRenderingContext2D, offsetX: number, offsetY: number) => { 51 | C.clearRect(0, 0, C.canvas.width, C.canvas.height) 52 | const count = 3 53 | const spaceX = window.innerWidth/count 54 | const spaceY = window.innerHeight/count 55 | for (let i=-count-15; i < count+10; i++) { 56 | const startX = i*spaceX 57 | const startY = i*spaceY 58 | drawLine(C, startX+offsetX, startY+offsetY) 59 | } 60 | } 61 | 62 | useEffect(() => { 63 | const canvas = canvasRef.current! 64 | const context = canvas.getContext('2d')! 65 | let offsetX = 0 66 | let offsetY = 0 67 | let animationFrameId: number 68 | 69 | // setDPI(canvas, 900) 70 | 71 | const render = () => { 72 | context.canvas.width = window.innerWidth; 73 | context.canvas.height = window.innerHeight; 74 | offsetX+=1 75 | offsetY+=1 76 | if (offsetX > 900) { 77 | offsetX = -offsetX 78 | offsetY = -offsetY 79 | } 80 | 81 | draw(context, offsetX, offsetY) 82 | animationFrameId = window.requestAnimationFrame(render) 83 | } 84 | 85 | render() 86 | return () => { 87 | window.cancelAnimationFrame(animationFrameId) 88 | } 89 | }, [draw]) 90 | 91 | return ( 92 | <> 93 | }/> 94 | 95 | ) 96 | } -------------------------------------------------------------------------------- /frontend/src/components/CartManager/CartManager.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | margin-top: 70px; 5 | `; 6 | 7 | export const container = styled.div``; 8 | 9 | export const cart = styled.div` 10 | display: flex; 11 | justify-content: space-around; 12 | align-items: center; 13 | padding: 10px 0; 14 | border-radius: 5px; 15 | border-bottom: 1px solid #333; 16 | `; 17 | 18 | export const cartImage = styled.div` 19 | height: 50px; 20 | width: 50px; 21 | background-color: black; 22 | `; 23 | export const cartName = styled.div``; 24 | export const cartAmount = styled.div``; 25 | export const cartPrice = styled.div``; 26 | export const cartAmountInput = styled.input` 27 | width: 50px; 28 | `; 29 | export const btnDelete = styled.button` 30 | width: 80px; 31 | height: 50px; 32 | border-radius: 5px; 33 | &:hover { 34 | cursor: pointer; 35 | background-image: linear-gradient( 36 | to right, 37 | #f5ce62, 38 | #e43603, 39 | #fa7199, 40 | #e85a19 41 | ); 42 | box-shadow: 0 4px 15px 0 rgba(229, 66, 10, 0.75); 43 | } 44 | `; 45 | export const btnBuy = styled.button` 46 | &:hover { 47 | background-position: 100% 0; 48 | -o-transition: all 0.4s ease-in-out; 49 | -webkit-transition: all 0.4s ease-in-out; 50 | transition: all 0.4s ease-in-out; 51 | background-image: linear-gradient( 52 | to right, 53 | #25aae1, 54 | #40e495, 55 | #30dd8a, 56 | #2bb673 57 | ); 58 | box-shadow: 0 4px 15px 0 rgba(49, 196, 190, 0.75); 59 | cursor: pointer; 60 | } 61 | width: 80px; 62 | height: 50px; 63 | border-radius: 5px; 64 | margin-top: 10px; 65 | margin-left: 1270px; 66 | `; 67 | -------------------------------------------------------------------------------- /frontend/src/components/CartManager/CartManager.tsx: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from "@context/AuthContext"; 2 | 3 | import * as S from "./CartManager.styled"; 4 | 5 | interface Props {} 6 | 7 | export const CartManager = (props: Props) => { 8 | const { user } = useAuthContext(); 9 | 10 | console.log(user); 11 | 12 | return ( 13 | <> 14 | 15 | Your Cart 16 | 17 | 18 | Hinhf anhr 19 | Name: kim cuong 20 | 21 | Amount: 22 | 23 | 24 | Price: 100000 25 | Delete 26 | 27 | 28 | {/* 2 */} 29 | 30 | 31 | Hinhf anhr 32 | Name: kim cuong 33 | 34 | Amount: 35 | 36 | 37 | Price: 100000 38 | Delete 39 | 40 | 41 | Payment 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/components/Forms/LoginForm/LoginForm.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const Wrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | min-height: 90%; 10 | top: 0; 11 | ` 12 | 13 | export const Form = styled.form` 14 | max-width: 40%; 15 | min-width: 35%; 16 | padding: 2em; 17 | margin-bottom: 2em; 18 | ` 19 | 20 | export const Label = styled.label` 21 | width: 100%; 22 | text-align: left; 23 | font-size: 1.25em; 24 | font-weight: 500; 25 | display: block; 26 | margin-top: 1.25em; 27 | ` 28 | 29 | export const Grid = styled.div` 30 | margin-top: 3em; 31 | display: grid; 32 | grid-gap: 3em; 33 | grid-template-columns: repeat(2, 1fr); 34 | ` 35 | 36 | export const Button = styled.button` 37 | border: var(--b-md) solid; 38 | font-family: "Urbanist", sans-serif; 39 | background: linear-gradient(to left, white 50%, salmon 50%) right; 40 | background-size: 200%; 41 | transition: .3s ease-out; 42 | padding: 1em 0.5em; 43 | color: #000; 44 | font-size: 1.2em; 45 | font-weight: 500; 46 | text-decoration: none; 47 | text-align: center; 48 | &:hover { 49 | background-position: left; 50 | cursor: crosshair; 51 | } 52 | ` 53 | 54 | export const LinkButton = styled(Link)` 55 | border: var(--b-md) solid; 56 | font-family: "Urbanist", sans-serif; 57 | background: linear-gradient(to left, white 50%, salmon 50%) right; 58 | background-size: 200%; 59 | transition: .3s ease-out; 60 | padding: 1em 0.5em; 61 | color: #000; 62 | font-size: 1.2em; 63 | font-weight: 500; 64 | text-decoration: none; 65 | text-align: center; 66 | &:hover { 67 | background-position: left; 68 | cursor: crosshair; 69 | } 70 | ` 71 | 72 | export const Input = styled.input` 73 | width: 100%; 74 | margin-top: 0.875em; 75 | border: 2px solid; 76 | font-size: 1em; 77 | padding: 1em; 78 | transition: 0.3s ease; 79 | 80 | &:focus { 81 | border-color: transparent; 82 | outline: none; 83 | box-shadow: 0 0 0 4px var(--c-red-300); 84 | } 85 | ` -------------------------------------------------------------------------------- /frontend/src/components/Forms/LoginForm/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import * as S from'./LoginForm.styled' 3 | import { loginPOST } from '@util/apiQueries/apiAccount' 4 | import { useAuthContext } from '@context/AuthContext' 5 | import { Form, useNavigate } from 'react-router-dom' 6 | 7 | interface Props { 8 | 9 | } 10 | 11 | export const LoginForm = (props: Props) => { 12 | const navigate = useNavigate() 13 | const { serializeUser } = useAuthContext(); 14 | 15 | const initialForm = { 16 | username: "", 17 | password: "" 18 | } 19 | 20 | const [formData, setFormData] = useState(initialForm) 21 | 22 | const onFormDataChange = (field: string, data: any) => { 23 | setFormData((prevState) => ( 24 | { ...prevState, [field]: data} 25 | )) 26 | } 27 | 28 | const onSubmitHandler = async (e: any) => { 29 | e.preventDefault() 30 | try { 31 | const res = await loginPOST({ ...formData }) 32 | console.log(res) 33 | if (res.status !== 200) { 34 | console.log('API Error') 35 | } else { 36 | serializeUser(res.data.access_token); 37 | } 38 | } catch(err) { 39 | console.log('API Error: ' + err) 40 | } 41 | } 42 | 43 | return ( 44 | <> 45 | 46 | 47 | Tên người dùng 48 | { 52 | return onFormDataChange('username', e.target.value)} 53 | } 54 | /> 55 | Mật khẩu 56 | { 61 | return onFormDataChange('password', e.target.value)} 62 | } 63 | /> 64 | 65 | Đăng nhập 66 | Đăng ký 67 | 68 | 69 | 70 | 71 | ) 72 | } -------------------------------------------------------------------------------- /frontend/src/components/Forms/RegisterForm/RegisterForm.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const Wrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | min-height: 90%; 10 | top: 0; 11 | > div { 12 | max-width: 40%; 13 | min-width: 35%; 14 | padding: 2em; 15 | margin-bottom: 2em; 16 | } 17 | ` 18 | 19 | export const Label = styled.label` 20 | width: 100%; 21 | text-align: left; 22 | font-size: 1.25em; 23 | font-weight: 500; 24 | display: block; 25 | margin-top: 1.25em; 26 | ` 27 | 28 | export const Grid = styled.div` 29 | margin-top: 3em; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: center; 34 | ` 35 | 36 | export const Button = styled.button` 37 | border: var(--b-md) solid; 38 | font-family: "Urbanist", sans-serif; 39 | background: linear-gradient(to left, white 50%, salmon 50%) right; 40 | background-size: 200%; 41 | transition: .3s ease-out; 42 | padding: 1em 2em; 43 | color: #000; 44 | font-size: 1.2em; 45 | font-weight: 500; 46 | text-decoration: none; 47 | text-align: center; 48 | &:hover { 49 | background-position: left; 50 | cursor: crosshair; 51 | } 52 | ` 53 | 54 | export const Input = styled.input` 55 | width: 100%; 56 | margin-top: 0.875em; 57 | border: 2px solid; 58 | font-size: 1em; 59 | padding: 1em; 60 | transition: 0.3s ease; 61 | 62 | &:focus { 63 | border-color: transparent; 64 | outline: none; 65 | box-shadow: 0 0 0 4px var(--c-red-300); 66 | } 67 | ` -------------------------------------------------------------------------------- /frontend/src/components/Forms/RegisterForm/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from'./RegisterForm.styled' 3 | 4 | interface Props { 5 | 6 | } 7 | 8 | export const RegisterForm = (props: Props) => { 9 | 10 | return ( 11 | <> 12 | 13 |
14 | Tên người dùng 15 | 16 | Địa chỉ Email 17 | 18 | Số điện thoại 19 | 20 | Mật khẩu 21 | 22 | Nhập lại mật khẩu 23 | 24 | 25 | Đăng ký 26 | 27 |
28 |
29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.motion,.ts: -------------------------------------------------------------------------------- 1 | import { Variants } from 'framer-motion' 2 | 3 | export const WrapperV: Variants = { 4 | 5 | } -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { motion } from "framer-motion"; 3 | 4 | import { media } from "@style/media"; 5 | 6 | export const Wrapper = styled(motion.header)` 7 | display: flex; 8 | align-items: stretch; 9 | border-bottom: var(--b-md) solid; 10 | position: sticky; 11 | /* margin-bottom: 4%; */ 12 | top: 0; 13 | width: 100%; 14 | /* height: 10%; */ 15 | z-index: 10; 16 | background: var(--c-white); 17 | ` 18 | 19 | const BaseWrapper = styled.div` 20 | padding: 0.5em 1em; 21 | display: flex; 22 | align-items: center; 23 | ` 24 | 25 | export const LogoWrapper = styled(BaseWrapper)` 26 | position: absolute; 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | top: 0; 31 | left: 0px; 32 | bottom: 0; 33 | right: 0; 34 | width: clamp(50px, 10vw, 120px); 35 | height: clamp(50px, 10vw, 120px); 36 | z-index: 11; 37 | background-color: var(--c-white); 38 | border-right: var(--b-md) solid; 39 | border-bottom: var(--b-md) solid; 40 | &:hover { 41 | cursor: crosshair; 42 | } 43 | img { 44 | max-width: 100%; 45 | display: block; 46 | } 47 | ` 48 | 49 | export const MarqueeWrapper = styled(BaseWrapper)` 50 | width: 100%; 51 | padding: 0; 52 | margin-left: auto; 53 | margin-right: auto; 54 | div { 55 | font-size: 1.2em; 56 | overflow-x: hidden; 57 | overflow-y: hidden; 58 | } 59 | ` 60 | 61 | export const BrandWrapper = styled(BaseWrapper)` 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | width: 50%; 66 | border-left: 2px solid; 67 | h1 { 68 | font-size: 1.5em; 69 | font-weight: 500; 70 | } 71 | ` 72 | 73 | export const Grid = styled.div` 74 | display: grid; 75 | width: 80%; 76 | margin: 0; 77 | grid-template-columns: repeat(2, 1fr); 78 | grid-template-rows: repeat(2, auto); 79 | div { 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | width: 100%; 84 | height: 100%; 85 | margin: 0; 86 | padding: 0.5em; 87 | background: linear-gradient(to left, white 50%, salmon 50%) right; 88 | background-size: 200%; 89 | transition: .3s ease-out; 90 | &:hover { 91 | background-position: left; 92 | cursor: crosshair; 93 | } 94 | a { 95 | padding: 0; 96 | margin: 0; 97 | width: 100%; 98 | height: 100%; 99 | z-index: 1; 100 | color: #000; 101 | text-decoration: none; 102 | &:hover { 103 | cursor: crosshair; 104 | } 105 | } 106 | } 107 | 108 | & > :nth-child(1), & :nth-child(2) { 109 | border-bottom: var(--b-sm) solid; 110 | border-left: var(--b-sm) solid; 111 | } 112 | & :nth-child(3), & :nth-child(4) { 113 | border-left: var(--b-sm) solid; 114 | } 115 | ` -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import Marquee from 'react-fast-marquee' 3 | 4 | import * as S from './Header.styled' 5 | 6 | export const Header = () => { 7 | 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | Best auction site for you! 17 | 18 | 19 | 20 |

Yabe Auction Site

21 |
22 | 23 |
24 | Home 25 |
26 |
27 | Advertise 28 |
29 |
30 | Contact 31 |
32 |
33 | Account 34 |
35 |
36 |
37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /frontend/src/components/Layout/Container.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { motion } from 'framer-motion' 3 | 4 | export const Wrapper = styled(motion.div)` 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | bottom: 0; 9 | right: 0; 10 | min-width: 90vw; 11 | min-height: 90vh; 12 | max-width: 95vw; 13 | max-height: 95vh; 14 | width: 95vw; 15 | height: 95vh; 16 | margin: auto; 17 | overflow-y: auto; 18 | overflow-x: hidden; 19 | border: var(--b-bg) solid; 20 | scroll-behavior: smooth; 21 | z-index: 99; 22 | /* pointer-events: none; 23 | user-select: none; */ 24 | 25 | &::-webkit-scrollbar { 26 | background: #ddd; 27 | width: 16px; 28 | } 29 | &::-webkit-scrollbar-thumb { 30 | background: #404040; 31 | border: 5px solid #ddd; 32 | } 33 | ` -------------------------------------------------------------------------------- /frontend/src/components/Layout/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import * as S from './Container.styled' 4 | 5 | interface Props { 6 | children: React.ReactNode 7 | } 8 | 9 | export const Container = (props: Props) => { 10 | const { children } = props 11 | 12 | return ( 13 | <> 14 | 15 | {children} 16 | 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /frontend/src/components/Layout/Layout.motion.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/src/components/Layout/Layout.motion.ts -------------------------------------------------------------------------------- /frontend/src/components/Layout/Layout.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { motion } from 'framer-motion' 3 | 4 | export const Wrapper = styled(motion.div)` 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | bottom: 0; 9 | right: 0; 10 | min-width: 90vw; 11 | min-height: 90vh; 12 | max-width: 95vw; 13 | max-height: 95vh; 14 | width: 95vw; 15 | height: 95vh; 16 | margin: auto; 17 | overflow-y: auto; 18 | overflow-x: hidden; 19 | border: var(--b-bg) solid; 20 | scroll-behavior: smooth; 21 | z-index: 99; 22 | /* pointer-events: none; 23 | user-select: none; */ 24 | 25 | &::-webkit-scrollbar { 26 | background: #ddd; 27 | width: 16px; 28 | } 29 | &::-webkit-scrollbar-thumb { 30 | background: #404040; 31 | border: 5px solid #ddd; 32 | } 33 | ` -------------------------------------------------------------------------------- /frontend/src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Header } from "@comp/Header/Header" 4 | 5 | import * as S from './Layout.styled' 6 | 7 | interface Props { 8 | children: React.ReactNode 9 | } 10 | 11 | export const Layout = (props: Props) => { 12 | const { children } = props 13 | 14 | return ( 15 | <> 16 | 17 |
18 | {children} 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /frontend/src/components/UserManager/UserManager.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | bottom: 0; 8 | right: 0; 9 | width: 100%; 10 | height: 100%; 11 | ` -------------------------------------------------------------------------------- /frontend/src/components/UserManager/UserManager.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAuthContext } from '@context/AuthContext' 3 | import { LoginForm } from '@comp/Forms/LoginForm/LoginForm' 4 | import { UserProfile } from "@comp/UserProfile/UserProfile" 5 | 6 | import * as S from './UserManager.styled' 7 | 8 | interface Props { 9 | 10 | } 11 | 12 | export const UserManager = (props: Props) => { 13 | const { user } = useAuthContext() 14 | 15 | return ( 16 | <> 17 | 18 | { user.username === null 19 | ? 20 | : } 21 | 22 | 23 | ) 24 | } -------------------------------------------------------------------------------- /frontend/src/components/UserProfile/UserProfile.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | 5 | ` -------------------------------------------------------------------------------- /frontend/src/components/UserProfile/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import * as S from './UserProfile.styled' 3 | import { Layout } from '@comp/Layout/Layout' 4 | import { useAuthContext } from '../../context/AuthContext' 5 | import { useNavigate } from 'react-router-dom' 6 | import { User } from '@context/AuthContext' 7 | import { useGetUser } from '@hook/useQueries' 8 | 9 | interface Props { 10 | user: User; 11 | } 12 | 13 | export const UserProfile = (props: Props) => { 14 | const { user } = props 15 | const navigate = useNavigate() 16 | const { refetch, data: userData, status } = useGetUser(user.accessToken, user.username) 17 | 18 | console.log(userData?.data) 19 | 20 | return ( 21 | <> 22 | 23 | { userData?.data.username } 24 | 25 | 26 | ) 27 | } -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useRef, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import jwt_decode from 'jwt-decode'; 4 | 5 | type Props = { 6 | children: React.ReactNode 7 | } 8 | 9 | export interface LoginJWT { 10 | scopes: Scope[]; 11 | exp: number; 12 | sub: string; 13 | } 14 | 15 | export type Scope = 'ADMIN' | 'USER'; 16 | 17 | export interface User { 18 | username: string | null; 19 | scope: Scope | null; 20 | tokenExpiration: number | null; 21 | accessToken: unknown | null; 22 | } 23 | 24 | type AuthContextTypes = { 25 | user: User; 26 | setUser: React.Dispatch>; 27 | serializeUser: (userToken: string) => void; 28 | logoutUser: () => void; 29 | } 30 | 31 | 32 | const AuthContext = createContext(undefined); 33 | 34 | const userDefault: User = { 35 | scope: null, 36 | username: null, 37 | accessToken: null, 38 | tokenExpiration: null, 39 | }; 40 | 41 | export const AuthContextProvider = React.memo(props => { 42 | const { children } = props 43 | 44 | const logoutTimeoutId = useRef | null>(null); 45 | 46 | const [user, setUser] = useState(userDefault) 47 | 48 | const logoutUser = React.useCallback(() => { 49 | setUser(userDefault) 50 | localStorage.removeItem('JWT'); 51 | toast.info('You have been logged out') 52 | }, []) 53 | 54 | const serializeUser = React.useCallback( 55 | (userToken: string) => { 56 | const decoded = jwt_decode(userToken) as LoginJWT 57 | //Check if token is not expired 58 | const endDate = new Date(decoded.exp * 1000) 59 | const startDate = new Date(); 60 | const miliSecondsDifference = endDate.getTime() - startDate.getTime(); 61 | if (miliSecondsDifference <= 0) return logoutUser(); 62 | 63 | window.localStorage.removeItem('JWT'); 64 | window.localStorage.setItem('JWT', userToken); 65 | 66 | setUser({ 67 | accessToken: userToken, 68 | // scope: decoded.scopes[0].toUpperCase() as Scope, 69 | scope: 'USER', 70 | username: decoded.sub, 71 | tokenExpiration: decoded.exp, 72 | }) 73 | 74 | 75 | toast.success('Login successful!') 76 | }, [logoutUser] 77 | ) 78 | 79 | useEffect(() => { 80 | const timeout = logoutTimeoutId.current; 81 | 82 | if (user.tokenExpiration) { 83 | const endDate = new Date(user.tokenExpiration * 1000); 84 | const startDate = new Date(); 85 | const miliSecondsDifference = endDate.getTime() - startDate.getTime(); 86 | 87 | //Log out the user after the time has expired 88 | logoutTimeoutId.current = setTimeout(() => { 89 | logoutUser() 90 | }, miliSecondsDifference) 91 | } 92 | 93 | return () => { 94 | if (timeout) clearTimeout(timeout) 95 | } 96 | }, [logoutUser, user]) 97 | 98 | return ( 99 | 107 | {children} 108 | 109 | ) 110 | }) 111 | 112 | export const useAuthContext = () => { 113 | const ctx = useContext(AuthContext); 114 | if (ctx === undefined) { 115 | throw new Error('useAuthContext must be used within a AuthContextProvider'); 116 | } 117 | return ctx 118 | } 119 | 120 | AuthContext.displayName = 'AuthContext' 121 | AuthContextProvider.displayName = 'AuthContextProvider' -------------------------------------------------------------------------------- /frontend/src/hooks/useQueries.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, useQueryClient } from "react-query"; 2 | import { getAd, getAds } from "@util/apiQueries/apiAd"; 3 | import { userGET } from "@util/apiQueries/apiAccount"; 4 | 5 | export const useGetAds = () => { 6 | return useQuery(['ads'], () => getAds()) 7 | } 8 | 9 | export const useGetAd = (adId: string) => { 10 | const queryClient = useQueryClient() 11 | return useQuery(['ad', adId], 12 | getAd, { 13 | initialData: () => { 14 | const ad = queryClient 15 | .getQueryData('ads') 16 | ?.data?.find((ad) => ad.id === parseInt(adId)) 17 | 18 | if (ad) { 19 | return { data: ad } 20 | } 21 | } 22 | } 23 | ) 24 | } 25 | 26 | export const useGetUser = (token: string | any, username: string | any) => useQuery(['user'], () => { 27 | return userGET({ token, username }) 28 | }) -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './styles/index.scss' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | // 9 | 10 | // 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/src/pages/AccountPage/AccountPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout } from '@comp/Layout/Layout' 3 | import { UserManager } from '@comp/UserManager/UserManager' 4 | import { LineCanvas } from '@comp/Canvas/LineCanvas' 5 | 6 | interface Props { 7 | 8 | } 9 | 10 | export default function AccoutPage(props: Props) { 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } -------------------------------------------------------------------------------- /frontend/src/pages/AdPage/AdPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { Container } from "@comp/Layout/Container" 4 | import { CircleCanvas } from "@comp/Canvas/CircleCanvas"; 5 | import { AdManager } from "@comp/AdManager/AdManager"; 6 | 7 | export default function AdPage() { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } -------------------------------------------------------------------------------- /frontend/src/pages/AdsPage/AdsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { Layout } from "@comp/Layout/Layout" 4 | import { CircleCanvas } from "@comp/Canvas/CircleCanvas"; 5 | import { AdsManager } from "@comp/AdsManager/AdsManager" 6 | 7 | export default function AdsPage() { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } -------------------------------------------------------------------------------- /frontend/src/pages/CartPage/CartPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layout } from "@comp/Layout/Layout"; 3 | import { LineCanvas } from "@comp/Canvas/LineCanvas"; 4 | import { CartManager } from "@comp/CartManager/CartManager"; 5 | 6 | export default function CartPage() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/pages/IndexPage/IndexPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Layout } from "@comp/Layout/Layout"; 4 | import { LineCanvas } from "@comp/Canvas/LineCanvas"; 5 | 6 | interface Props { 7 | 8 | } 9 | 10 | export default function IndexPage(props: Props) { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | ) 18 | } -------------------------------------------------------------------------------- /frontend/src/pages/RegisterPage/RegisterPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout } from '@comp/Layout/Layout' 3 | import { RegisterForm } from '@comp/Forms/RegisterForm/RegisterForm' 4 | import { LineCanvas } from '@comp/Canvas/LineCanvas' 5 | 6 | interface Props { 7 | 8 | } 9 | 10 | export default function RegisterPage(props: Props) { 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } -------------------------------------------------------------------------------- /frontend/src/styles/core/fonts.scss: -------------------------------------------------------------------------------- 1 | /* latin-ext */ 2 | @font-face { 3 | font-family: 'Urbanist'; 4 | font-style: italic; 5 | font-weight: 200; 6 | font-display: swap; 7 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 8 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 9 | } 10 | /* latin */ 11 | @font-face { 12 | font-family: 'Urbanist'; 13 | font-style: italic; 14 | font-weight: 200; 15 | font-display: swap; 16 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 18 | } 19 | /* latin-ext */ 20 | @font-face { 21 | font-family: 'Urbanist'; 22 | font-style: italic; 23 | font-weight: 300; 24 | font-display: swap; 25 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 26 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 27 | } 28 | /* latin */ 29 | @font-face { 30 | font-family: 'Urbanist'; 31 | font-style: italic; 32 | font-weight: 300; 33 | font-display: swap; 34 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 35 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 36 | } 37 | /* latin-ext */ 38 | @font-face { 39 | font-family: 'Urbanist'; 40 | font-style: italic; 41 | font-weight: 400; 42 | font-display: swap; 43 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 44 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 45 | } 46 | /* latin */ 47 | @font-face { 48 | font-family: 'Urbanist'; 49 | font-style: italic; 50 | font-weight: 400; 51 | font-display: swap; 52 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 53 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 54 | } 55 | /* latin-ext */ 56 | @font-face { 57 | font-family: 'Urbanist'; 58 | font-style: italic; 59 | font-weight: 500; 60 | font-display: swap; 61 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 62 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 63 | } 64 | /* latin */ 65 | @font-face { 66 | font-family: 'Urbanist'; 67 | font-style: italic; 68 | font-weight: 500; 69 | font-display: swap; 70 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 71 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 72 | } 73 | /* latin-ext */ 74 | @font-face { 75 | font-family: 'Urbanist'; 76 | font-style: italic; 77 | font-weight: 600; 78 | font-display: swap; 79 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 80 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 81 | } 82 | /* latin */ 83 | @font-face { 84 | font-family: 'Urbanist'; 85 | font-style: italic; 86 | font-weight: 600; 87 | font-display: swap; 88 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 89 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 90 | } 91 | /* latin-ext */ 92 | @font-face { 93 | font-family: 'Urbanist'; 94 | font-style: italic; 95 | font-weight: 700; 96 | font-display: swap; 97 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 98 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 99 | } 100 | /* latin */ 101 | @font-face { 102 | font-family: 'Urbanist'; 103 | font-style: italic; 104 | font-weight: 700; 105 | font-display: swap; 106 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 107 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 108 | } 109 | /* latin-ext */ 110 | @font-face { 111 | font-family: 'Urbanist'; 112 | font-style: italic; 113 | font-weight: 800; 114 | font-display: swap; 115 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 116 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 117 | } 118 | /* latin */ 119 | @font-face { 120 | font-family: 'Urbanist'; 121 | font-style: italic; 122 | font-weight: 800; 123 | font-display: swap; 124 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 125 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 126 | } 127 | /* latin-ext */ 128 | @font-face { 129 | font-family: 'Urbanist'; 130 | font-style: italic; 131 | font-weight: 900; 132 | font-display: swap; 133 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXSFsjkK3.woff2') format('woff2'); 134 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 135 | } 136 | /* latin */ 137 | @font-face { 138 | font-family: 'Urbanist'; 139 | font-style: italic; 140 | font-weight: 900; 141 | font-display: swap; 142 | src: url('/fonts/L0x4DF02iFML4hGCyMqgXS9sjg.woff2') format('woff2'); 143 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 144 | } 145 | /* latin-ext */ 146 | @font-face { 147 | font-family: 'Urbanist'; 148 | font-style: normal; 149 | font-weight: 200; 150 | font-display: swap; 151 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 152 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 153 | } 154 | /* latin */ 155 | @font-face { 156 | font-family: 'Urbanist'; 157 | font-style: normal; 158 | font-weight: 200; 159 | font-display: swap; 160 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 161 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 162 | } 163 | /* latin-ext */ 164 | @font-face { 165 | font-family: 'Urbanist'; 166 | font-style: normal; 167 | font-weight: 300; 168 | font-display: swap; 169 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 170 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 171 | } 172 | /* latin */ 173 | @font-face { 174 | font-family: 'Urbanist'; 175 | font-style: normal; 176 | font-weight: 300; 177 | font-display: swap; 178 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 179 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 180 | } 181 | /* latin-ext */ 182 | @font-face { 183 | font-family: 'Urbanist'; 184 | font-style: normal; 185 | font-weight: 400; 186 | font-display: swap; 187 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 188 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 189 | } 190 | /* latin */ 191 | @font-face { 192 | font-family: 'Urbanist'; 193 | font-style: normal; 194 | font-weight: 400; 195 | font-display: swap; 196 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 197 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 198 | } 199 | /* latin-ext */ 200 | @font-face { 201 | font-family: 'Urbanist'; 202 | font-style: normal; 203 | font-weight: 500; 204 | font-display: swap; 205 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 206 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 207 | } 208 | /* latin */ 209 | @font-face { 210 | font-family: 'Urbanist'; 211 | font-style: normal; 212 | font-weight: 500; 213 | font-display: swap; 214 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 215 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 216 | } 217 | /* latin-ext */ 218 | @font-face { 219 | font-family: 'Urbanist'; 220 | font-style: normal; 221 | font-weight: 600; 222 | font-display: swap; 223 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 224 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 225 | } 226 | /* latin */ 227 | @font-face { 228 | font-family: 'Urbanist'; 229 | font-style: normal; 230 | font-weight: 600; 231 | font-display: swap; 232 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 233 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 234 | } 235 | /* latin-ext */ 236 | @font-face { 237 | font-family: 'Urbanist'; 238 | font-style: normal; 239 | font-weight: 700; 240 | font-display: swap; 241 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 242 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 243 | } 244 | /* latin */ 245 | @font-face { 246 | font-family: 'Urbanist'; 247 | font-style: normal; 248 | font-weight: 700; 249 | font-display: swap; 250 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 251 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 252 | } 253 | /* latin-ext */ 254 | @font-face { 255 | font-family: 'Urbanist'; 256 | font-style: normal; 257 | font-weight: 800; 258 | font-display: swap; 259 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 260 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 261 | } 262 | /* latin */ 263 | @font-face { 264 | font-family: 'Urbanist'; 265 | font-style: normal; 266 | font-weight: 800; 267 | font-display: swap; 268 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 269 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 270 | } 271 | /* latin-ext */ 272 | @font-face { 273 | font-family: 'Urbanist'; 274 | font-style: normal; 275 | font-weight: 900; 276 | font-display: swap; 277 | src: url('/fonts/L0x-DF02iFML4hGCyMqrbS10ig.woff2') format('woff2'); 278 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 279 | } 280 | /* latin */ 281 | @font-face { 282 | font-family: 'Urbanist'; 283 | font-style: normal; 284 | font-weight: 900; 285 | font-display: swap; 286 | src: url('/fonts/L0x-DF02iFML4hGCyMqlbS0.woff2') format('woff2'); 287 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 288 | } -------------------------------------------------------------------------------- /frontend/src/styles/core/global.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: inherit; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | html { 10 | box-sizing: border-box; 11 | font-family: "Urbanist", sans-serif; 12 | overflow-x: hidden; 13 | } 14 | 15 | body::-webkit-scrollbar { 16 | background: #ddd; 17 | width: 16px; 18 | } 19 | 20 | body::-webkit-scrollbar-thumb { 21 | background: #404040; 22 | border: 5px solid #ddd; 23 | } 24 | 25 | body { 26 | line-height: 1em; 27 | font-size: clamp(0.5rem, 1.5vw, 1rem) 28 | } -------------------------------------------------------------------------------- /frontend/src/styles/core/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ''; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /frontend/src/styles/core/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-white: #ffffff; 3 | --c-green-500: #80b895; 4 | --c-green-300: #bad5ca; 5 | --c-red-300: salmon; 6 | --b-md: 3px; 7 | --b-sm: 2px; 8 | --b-bg: 5px; 9 | } -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './core/variables.scss'; 2 | 3 | @import './core/fonts.scss'; 4 | @import './core/reset.scss'; 5 | @import './core/global.scss'; -------------------------------------------------------------------------------- /frontend/src/styles/media.ts: -------------------------------------------------------------------------------- 1 | export const breakpoints = { 2 | tablet: 767, 3 | desktop: 1920 4 | } 5 | 6 | const mediaQuery = (minWidth: number) => { 7 | `@media only screen and (min-width: ${minWidth / 16}em)` 8 | } 9 | 10 | export const media = { 11 | custom: mediaQuery, 12 | tablet: mediaQuery(breakpoints.tablet), 13 | desktop: mediaQuery(breakpoints.desktop), 14 | } -------------------------------------------------------------------------------- /frontend/src/types/global.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiritoroo/jewelry-auction-site/2d082bbdda337f06927f61897e750117c01158be/frontend/src/types/global.d.ts -------------------------------------------------------------------------------- /frontend/src/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface BackendAd { 2 | id: string; 3 | product_name: string; 4 | description: string; 5 | category: string; 6 | image: string; 7 | created_at: string; 8 | created_by: string; 9 | base_price: number; 10 | current_price: string; 11 | status: string; 12 | } -------------------------------------------------------------------------------- /frontend/src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/utils/apiQueries/apiAccount.ts: -------------------------------------------------------------------------------- 1 | import axios from '@util/axiosUtil' 2 | 3 | interface LoginPOST { 4 | username: string; 5 | password: string; 6 | } 7 | 8 | interface userGET{ 9 | token: string; 10 | username: string; 11 | } 12 | 13 | export const loginPOST = ({ username, password }: LoginPOST) => { 14 | const params = new URLSearchParams() 15 | params.append('username', username) 16 | params.append('password', password) 17 | return axios.post('account/login', params) 18 | } 19 | 20 | export const userGET = ({ token, username }: userGET) => { 21 | const config = { 22 | headers: { Authorization: `Bearer ${token}` } 23 | } 24 | return axios.get(`account/me/${username}`, config) 25 | } 26 | 27 | export const registerPOST = ({ }) => { 28 | 29 | } -------------------------------------------------------------------------------- /frontend/src/utils/apiQueries/apiAd.ts: -------------------------------------------------------------------------------- 1 | import axios from '@util/axiosUtil' 2 | 3 | export const getAds = () => { 4 | return axios.get('ad') 5 | } 6 | 7 | export const getAd = ({ queryKey }) => { 8 | const adId = queryKey[1] 9 | const config = { 10 | headers: { Authorization: `Bearer...`} 11 | } 12 | return axios.get(`ad/${adId}`) 13 | } -------------------------------------------------------------------------------- /frontend/src/utils/axiosUtil.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const Instance = axios.create({ 4 | baseURL: '/api', 5 | headers: { 6 | 'Cache-Control': 'no-cache' 7 | } 8 | }) 9 | 10 | export default Instance -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": [ "src", "**/*.d.ts" ], 20 | "exclude": ["node_modules"], 21 | "extends": "./tsconfig.paths.json", 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@comp/*": ["src/components/*"], 6 | "@hook/*": ["src/hooks/*"], 7 | "@type/*": ["src/types/*"], 8 | "@page/*": ["src/pages/*"], 9 | "@style/*": ["src/styles/*"], 10 | "@context/*": ["src/context/*"], 11 | "@util/*": ["src/utils/*"] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), tsconfigPaths()], 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: 'http://127.0.0.1:8080', 11 | changeOrigin: true, 12 | secure: false 13 | }, 14 | '/static': { 15 | target: 'http://127.0.0.1:8080', 16 | changeOrigin: true, 17 | secure: false 18 | } 19 | } 20 | } 21 | }) 22 | --------------------------------------------------------------------------------