├── 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 |
18 |
19 |
20 | Yabe Auction Site
21 |
22 |
23 |
24 | Home
25 |
26 |
27 | Advertise
28 |
29 |
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 |
--------------------------------------------------------------------------------