Compare commits
10 Commits
09ddb05efe
...
456570d4f0
Author | SHA1 | Date | |
---|---|---|---|
456570d4f0 | |||
7db93e9e8a | |||
6903a8deb0 | |||
f2af9dc566 | |||
2fe5dbcb21 | |||
c60ec969d5 | |||
0986336aea | |||
cd9f68dfc9 | |||
d2101247d1 | |||
cbb799f879 |
62
README.md
62
README.md
@ -1,15 +1,55 @@
|
||||
# SHOP API
|
||||
Simple API (still WIP)
|
||||
# Swag Shop
|
||||
|
||||
## Requires:
|
||||
1. Redis
|
||||
Simple redis installation, no further configuration needed
|
||||
An e-commerence multitenant shop app.
|
||||
|
||||
2. MariaDB (or MySQL)
|
||||
Make sure a proper database is set up with `shop.sql`
|
||||
## Core idea
|
||||
This app provides a dashboard for managing individual shop data like products and similar things. Then it's up to the tenant to craft their own frontend and make use of the API.
|
||||
|
||||
## Running
|
||||
### Backend
|
||||
1. Dependencies
|
||||
Straight forward, you need to install dependencies via *[uv](https://docs.astral.sh/uv/)*
|
||||
|
||||
## Running:
|
||||
Gunicorn is the simplest way to run this project
|
||||
```sh
|
||||
gunicorn -w 4 -b HOST:PORT main:app
|
||||
```
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
2. Fill out `.env`
|
||||
You can use `.env.example` as a reference
|
||||
|
||||
3. Run it!
|
||||
```
|
||||
fastapi dev backend/app/main.py
|
||||
```
|
||||
|
||||
### Frontend
|
||||
1. Dependencies
|
||||
Using npm (or your favorite package manager)
|
||||
```
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Fill out `.env`
|
||||
As with backend, there is `.env.example`
|
||||
|
||||
3. Run it!
|
||||
There is a dev package script
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
<br>
|
||||
|
||||
- For deployment a `docker-compose.yml` is provided
|
||||
|
||||
## Tech Stack
|
||||
- **PostgreSQL**
|
||||
- **Backend** - *FastAPI* + *SQLModel*
|
||||
- **Frontend** - *Vite* + *Typescript* + *ShadcnUI* + *Tanstack Query*
|
||||
|
||||
## Future plans
|
||||
|
||||
- True API integration
|
||||
- Audit log
|
||||
- Stripe integration
|
@ -1,25 +1,41 @@
|
||||
# Base Image
|
||||
FROM python:3.13
|
||||
FROM python:3.12
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
# Environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app/
|
||||
|
||||
# Copy dependency files first to leverage caching
|
||||
COPY pyproject.toml poetry.lock /app/
|
||||
# Install uv
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Compile bytecode
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# uv Cache
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install dependencies
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
COPY ./pyproject.toml ./uv.lock /app/
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY ./app /app/app
|
||||
|
||||
# Ensure dependencies are installed correctly
|
||||
RUN poetry install --no-interaction --no-ansi --without dev
|
||||
# Sync the project
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync
|
||||
|
||||
# Expose port for FastAPI
|
||||
EXPOSE 8000
|
||||
|
||||
# Command to run the app
|
||||
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
|
261
currency_names.txt
Normal file
261
currency_names.txt
Normal file
@ -0,0 +1,261 @@
|
||||
"Abkhazian apsar[E]",
|
||||
"₽",
|
||||
"Afghan afghani",
|
||||
"Euro",
|
||||
"Albanian lek",
|
||||
"Algerian dinar",
|
||||
"Euro",
|
||||
"Angolan kwanza",
|
||||
"Eastern Caribbean dollar",
|
||||
"Eastern Caribbean dollar",
|
||||
"Argentine peso",
|
||||
"Armenian dram",
|
||||
"Aruban florin",
|
||||
"Saint Helena pound",
|
||||
"Australian dollar",
|
||||
"Euro",
|
||||
"Azerbaijani manat",
|
||||
"Bahamian dollar",
|
||||
"Bahraini dinar",
|
||||
"Bangladeshi taka",
|
||||
"Barbadian dollar",
|
||||
"Belarusian ruble",
|
||||
"Euro",
|
||||
"Belize dollar",
|
||||
"West African CFA franc",
|
||||
"Bermudian dollar",
|
||||
"Bhutanese ngultrum[F]",
|
||||
"₹",
|
||||
"Bolivian boliviano",
|
||||
"United States dollar[G]",
|
||||
"Bosnia and Herzegovina convertible mark",
|
||||
"Botswana pula",
|
||||
"Brazilian real",
|
||||
"Sterling",
|
||||
"United States dollar",
|
||||
"Brunei dollar",
|
||||
"$",
|
||||
"Bulgarian lev",
|
||||
"West African CFA franc",
|
||||
"Burundian franc",
|
||||
"Cambodian riel",
|
||||
"Central African CFA franc",
|
||||
"Canadian dollar",
|
||||
"Cape Verdean escudo",
|
||||
"Cayman Islands dollar",
|
||||
"Central African CFA franc",
|
||||
"Central African CFA franc",
|
||||
"Chilean peso",
|
||||
"Renminbi",
|
||||
"Colombian peso",
|
||||
"Comorian franc",
|
||||
"Congolese franc",
|
||||
"Central African CFA franc",
|
||||
"Cook Islands dollar",
|
||||
"$",
|
||||
"Costa Rican colón",
|
||||
"West African CFA franc",
|
||||
"Euro",
|
||||
"Cuban peso",
|
||||
"Netherlands Antillean guilder",
|
||||
"Euro",
|
||||
"Czech koruna",
|
||||
"Danish krone",
|
||||
"Djiboutian franc",
|
||||
"Eastern Caribbean dollar",
|
||||
"Dominican peso",
|
||||
"United States dollar",
|
||||
"Egyptian pound",
|
||||
"United States dollar",
|
||||
"Central African CFA franc",
|
||||
"Eritrean nakfa",
|
||||
"Euro",
|
||||
"Swazi lilangeni",
|
||||
"R",
|
||||
"Ethiopian birr",
|
||||
"Falkland Islands pound",
|
||||
"£",
|
||||
"Danish krone",
|
||||
"kr",
|
||||
"Fijian dollar",
|
||||
"Euro",
|
||||
"Euro",
|
||||
"CFP franc",
|
||||
"Central African CFA franc",
|
||||
"Gambian dalasi",
|
||||
"Georgian lari",
|
||||
"Euro",
|
||||
"Ghanaian cedi",
|
||||
"Gibraltar pound",
|
||||
"£",
|
||||
"Euro",
|
||||
"Danish krone",
|
||||
"Eastern Caribbean dollar",
|
||||
"Guatemalan quetzal",
|
||||
"Guernsey pound",
|
||||
"£",
|
||||
"Guinean franc",
|
||||
"West African CFA franc",
|
||||
"Guyanese dollar",
|
||||
"Haitian gourde",
|
||||
"Honduran lempira",
|
||||
"Hong Kong dollar",
|
||||
"Hungarian forint",
|
||||
"Icelandic króna",
|
||||
"Indian rupee",
|
||||
"Indonesian rupiah",
|
||||
"Iranian rial",
|
||||
"Iraqi dinar",
|
||||
"Euro",
|
||||
"Manx pound",
|
||||
"£",
|
||||
"Israeli new shekel",
|
||||
"Euro",
|
||||
"Jamaican dollar",
|
||||
"Japanese yen",
|
||||
"Jersey pound",
|
||||
"£",
|
||||
"Jordanian dinar",
|
||||
"Kazakhstani tenge",
|
||||
"Kenyan shilling",
|
||||
"Kiribati dollar[E]",
|
||||
"$",
|
||||
"North Korean won",
|
||||
"South Korean won",
|
||||
"Euro",
|
||||
"Kuwaiti dinar",
|
||||
"Kyrgyz som",
|
||||
"Lao kip",
|
||||
"Euro",
|
||||
"Lebanese pound",
|
||||
"Lesotho loti",
|
||||
"R",
|
||||
"Falkland Islands pound",
|
||||
"£",
|
||||
"Liberian dollar",
|
||||
"$",
|
||||
"Libyan dinar",
|
||||
"Swiss franc",
|
||||
"Euro",
|
||||
"Euro",
|
||||
"Macanese pataca",
|
||||
"Malagasy ariary",
|
||||
"Malawian kwacha",
|
||||
"Malaysian ringgit",
|
||||
"Maldivian rufiyaa",
|
||||
"West African CFA franc",
|
||||
"Euro",
|
||||
"United States dollar",
|
||||
"Mauritanian ouguiya",
|
||||
"Mauritian rupee",
|
||||
"Mexican peso",
|
||||
"United States dollar",
|
||||
"Moldovan leu",
|
||||
"Euro",
|
||||
"Mongolian tögrög",
|
||||
"Euro",
|
||||
"Eastern Caribbean dollar",
|
||||
"Moroccan dirham",
|
||||
"Mozambican metical",
|
||||
"Burmese kyat",
|
||||
"Namibian dollar",
|
||||
"R",
|
||||
"Australian dollar",
|
||||
"Nepalese rupee",
|
||||
"Euro",
|
||||
"CFP franc",
|
||||
"New Zealand dollar",
|
||||
"Nicaraguan córdoba",
|
||||
"West African CFA franc",
|
||||
"Nigerian naira",
|
||||
"New Zealand dollar",
|
||||
"$",
|
||||
"Macedonian denar",
|
||||
"Turkish lira",
|
||||
"Norwegian krone",
|
||||
"Omani rial",
|
||||
"Pakistani rupee",
|
||||
"United States dollar",
|
||||
"Israeli new shekel",
|
||||
"LE",
|
||||
"JD",
|
||||
"Panamanian balboa",
|
||||
"$",
|
||||
"Papua New Guinean kina",
|
||||
"Paraguayan guaraní",
|
||||
"Peruvian sol",
|
||||
"Philippine peso",
|
||||
"New Zealand dollar",
|
||||
"$",
|
||||
"Polish złoty",
|
||||
"Euro",
|
||||
"Qatari riyal",
|
||||
"Romanian leu",
|
||||
"Russian ruble",
|
||||
"Rwandan franc",
|
||||
"United States dollar[G]",
|
||||
"Moroccan dirham",
|
||||
"Pta or Pts (pl.)",
|
||||
"Saint Helena pound",
|
||||
"£",
|
||||
"Eastern Caribbean dollar",
|
||||
"Eastern Caribbean dollar",
|
||||
"Eastern Caribbean dollar",
|
||||
"Samoan tālā",
|
||||
"Euro",
|
||||
"São Tomé and Príncipe dobra",
|
||||
"Saudi riyal",
|
||||
"West African CFA franc",
|
||||
"Serbian dinar",
|
||||
"Seychellois rupee",
|
||||
"Sierra Leonean leone",
|
||||
"Singapore dollar",
|
||||
"$",
|
||||
"United States dollar[G]",
|
||||
"Netherlands Antillean guilder",
|
||||
"Euro",
|
||||
"Euro",
|
||||
"Solomon Islands dollar",
|
||||
"Somali shilling",
|
||||
"Somaliland shilling",
|
||||
"South African rand",
|
||||
"Russian ruble",
|
||||
"South Sudanese pound",
|
||||
"Euro",
|
||||
"Sri Lankan rupee",
|
||||
"Sudanese pound",
|
||||
"Surinamese dollar",
|
||||
"Swedish krona",
|
||||
"Swiss franc",
|
||||
"Syrian pound",
|
||||
"New Taiwan dollar",
|
||||
"Tajikistani somoni",
|
||||
"Tanzanian shilling",
|
||||
"Thai baht",
|
||||
"United States dollar",
|
||||
"West African CFA franc",
|
||||
"Tongan paʻanga[O]",
|
||||
"Transnistrian ruble",
|
||||
"Trinidad and Tobago dollar",
|
||||
"Tunisian dinar",
|
||||
"Turkish lira",
|
||||
"Turkmenistani manat",
|
||||
"United States dollar",
|
||||
"Tuvaluan dollar",
|
||||
"$",
|
||||
"Ugandan shilling",
|
||||
"Ukrainian hryvnia",
|
||||
"United Arab Emirates dirham",
|
||||
"Sterling",
|
||||
"United States dollar",
|
||||
"Uruguayan peso",
|
||||
"Uzbekistani sum",
|
||||
"Vanuatu vatu",
|
||||
"Euro",
|
||||
"Venezuelan sovereign bolívar",
|
||||
"Bs.D",
|
||||
"Vietnamese đồng",
|
||||
"CFP franc",
|
||||
"Yemeni rial",
|
||||
"Zambian kwacha",
|
||||
"Zimbabwe gold",
|
156
currency_names_new.txt
Normal file
156
currency_names_new.txt
Normal file
@ -0,0 +1,156 @@
|
||||
"Afghan afghani",
|
||||
"Albanian lek",
|
||||
"Algerian dinar",
|
||||
"Angolan kwanza",
|
||||
"Argentine peso",
|
||||
"Armenian dram",
|
||||
"Aruban florin",
|
||||
"Australian dollar",
|
||||
"Azerbaijani manat",
|
||||
"Bahamian dollar",
|
||||
"Bahraini dinar",
|
||||
"Bangladeshi taka",
|
||||
"Barbadian dollar",
|
||||
"Belarusian ruble",
|
||||
"Belize dollar",
|
||||
"Bermudian dollar",
|
||||
"Bhutanese ngultrum[F]",
|
||||
"Bolivian boliviano",
|
||||
"Bosnia and Herzegovina convertible mark",
|
||||
"Botswana pula",
|
||||
"Brazilian real",
|
||||
"Brunei dollar",
|
||||
"Bulgarian lev",
|
||||
"Burmese kyat",
|
||||
"Burundian franc",
|
||||
"Cambodian riel",
|
||||
"Canadian dollar",
|
||||
"Cape Verdean escudo",
|
||||
"Cayman Islands dollar",
|
||||
"Central African CFA franc",
|
||||
"CFP franc",
|
||||
"Colombian peso",
|
||||
"Comorian franc",
|
||||
"Congolese franc",
|
||||
"Costa Rican colón",
|
||||
"Cuban peso",
|
||||
"Czech koruna",
|
||||
"Danish krone",
|
||||
"Djiboutian franc",
|
||||
"Dominican peso",
|
||||
"Eastern Caribbean dollar",
|
||||
"Egyptian pound",
|
||||
"Eritrean nakfa",
|
||||
"Ethiopian birr",
|
||||
"Euro",
|
||||
"Falkland Islands pound",
|
||||
"Fijian dollar",
|
||||
"Gambian dalasi",
|
||||
"Georgian lari",
|
||||
"Ghanaian cedi",
|
||||
"Gibraltar pound",
|
||||
"Guatemalan quetzal",
|
||||
"Guinean franc",
|
||||
"Guyanese dollar",
|
||||
"Haitian gourde",
|
||||
"Honduran lempira",
|
||||
"Hong Kong dollar",
|
||||
"Hungarian forint",
|
||||
"Chilean peso",
|
||||
"Icelandic króna",
|
||||
"Indian rupee",
|
||||
"Indonesian rupiah",
|
||||
"Iranian rial",
|
||||
"Iraqi dinar",
|
||||
"Israeli new shekel",
|
||||
"Jamaican dollar",
|
||||
"Japanese yen",
|
||||
"Jordanian dinar",
|
||||
"Kazakhstani tenge",
|
||||
"Kenyan shilling",
|
||||
"Kuwaiti dinar",
|
||||
"Kyrgyz som",
|
||||
"Lao kip",
|
||||
"Lebanese pound",
|
||||
"Lesotho loti",
|
||||
"Liberian dollar",
|
||||
"Libyan dinar",
|
||||
"Macanese pataca",
|
||||
"Macedonian denar",
|
||||
"Malagasy ariary",
|
||||
"Malawian kwacha",
|
||||
"Malaysian ringgit",
|
||||
"Maldivian rufiyaa",
|
||||
"Mauritanian ouguiya",
|
||||
"Mauritian rupee",
|
||||
"Mexican peso",
|
||||
"Moldovan leu",
|
||||
"Mongolian tögrög",
|
||||
"Moroccan dirham",
|
||||
"Mozambican metical",
|
||||
"Namibian dollar",
|
||||
"Nepalese rupee",
|
||||
"Netherlands Antillean guilder",
|
||||
"New Taiwan dollar",
|
||||
"New Zealand dollar",
|
||||
"Nicaraguan córdoba",
|
||||
"Nigerian naira",
|
||||
"North Korean won",
|
||||
"Norwegian krone",
|
||||
"Omani rial",
|
||||
"Pakistani rupee",
|
||||
"Panamanian balboa",
|
||||
"Papua New Guinean kina",
|
||||
"Paraguayan guaraní",
|
||||
"Peruvian sol",
|
||||
"Philippine peso",
|
||||
"Polish złoty",
|
||||
"Qatari riyal",
|
||||
"Renminbi",
|
||||
"Romanian leu",
|
||||
"Russian ruble",
|
||||
"Rwandan franc",
|
||||
"Saint Helena pound",
|
||||
"Samoan tālā",
|
||||
"São Tomé and Príncipe dobra",
|
||||
"Saudi riyal",
|
||||
"Serbian dinar",
|
||||
"Seychellois rupee",
|
||||
"Sierra Leonean leone",
|
||||
"Singapore dollar",
|
||||
"Solomon Islands dollar",
|
||||
"Somali shilling",
|
||||
"South African rand",
|
||||
"South Korean won",
|
||||
"South Sudanese pound",
|
||||
"Sri Lankan rupee",
|
||||
"Sterling",
|
||||
"Sudanese pound",
|
||||
"Surinamese dollar",
|
||||
"Swazi lilangeni",
|
||||
"Swedish krona",
|
||||
"Swiss franc",
|
||||
"Syrian pound",
|
||||
"Tajikistani somoni",
|
||||
"Tanzanian shilling",
|
||||
"Thai baht",
|
||||
"Tongan paʻanga[O]",
|
||||
"Trinidad and Tobago dollar",
|
||||
"Tunisian dinar",
|
||||
"Turkish lira",
|
||||
"Turkmenistani manat",
|
||||
"Tuvaluan dollar",
|
||||
"Ugandan shilling",
|
||||
"Ukrainian hryvnia",
|
||||
"United Arab Emirates dirham",
|
||||
"United States dollar",
|
||||
"United States dollar[G]",
|
||||
"Uruguayan peso",
|
||||
"Uzbekistani sum",
|
||||
"Vanuatu vatu",
|
||||
"Venezuelan sovereign bolívar",
|
||||
"Vietnamese đồng",
|
||||
"West African CFA franc",
|
||||
"Yemeni rial",
|
||||
"Zambian kwacha",
|
||||
"Zimbabwe gold",
|
@ -1,68 +1,55 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
container_name: swagshop-backend
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- internal
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend/
|
||||
container_name: swagshop-frontend
|
||||
env_file:
|
||||
- ./frontend/.env
|
||||
ports:
|
||||
- "5173:5173"
|
||||
|
||||
db:
|
||||
image: postgres:12
|
||||
restart: no
|
||||
image: postgres:16
|
||||
container_name: swagshop-postgres
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: swagshop
|
||||
volumes:
|
||||
- app-db-data:/var/lib/postgresql/data/pgdata
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
|
||||
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: no
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- default
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- ADMINER_DESIGN=pepa-linha-dark
|
||||
|
||||
backend:
|
||||
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
|
||||
restart: no
|
||||
networks:
|
||||
- default
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
|
||||
- ENVIRONMENT=${ENVIRONMENT}
|
||||
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
|
||||
- SECRET_KEY=${SECRET_KEY?Variable not set}
|
||||
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
|
||||
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
|
||||
- MYSQL_SERVER=db
|
||||
- MYSQL_PORT=${MYSQL_PORT}
|
||||
- MYSQL_DB=${MYSQL_DB}
|
||||
- MYSQL_USER=${MYSQL_USER?Variable not set}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
build:
|
||||
context: ./backend
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
app-db-data:
|
||||
pgdata:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
|
2
frontend/.dockerignore
Normal file
2
frontend/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
@ -1 +1,2 @@
|
||||
VITE_API_URL=
|
||||
VITE_API_URL=http://localhost:8000
|
||||
VITE_USE_MOCK_API=true
|
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:20
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build && npm install -g serve
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["serve", "-s", "dist", "-l", "5173"]
|
30
frontend/biome.json.old
Normal file
30
frontend/biome.json.old
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": []
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
1
frontend/currencies.txt
Normal file
1
frontend/currencies.txt
Normal file
@ -0,0 +1 @@
|
||||
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HTG", "HUF", "CHF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VED", "VES", "VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWG",
|
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"knip": "knip",
|
||||
"generate-client": "openapi-ts"
|
||||
"generate-client": "openapi-ts",
|
||||
"format:write": "biome format ./src/ --write",
|
||||
"format:check": "biome check ./src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
@ -29,14 +29,14 @@
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||
"@tabler/icons-react": "^3.24.0",
|
||||
"@tanstack/react-query": "^5.62.3",
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"@tanstack/react-router": "^1.86.1",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@ -45,7 +45,9 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^3.6.0",
|
||||
"dexie": "^4.0.11",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "^9.6.0",
|
||||
@ -55,10 +57,12 @@
|
||||
"recharts": "^2.14.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@hey-api/client-axios": "^0.6.2",
|
||||
@ -85,6 +89,7 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vite": "^6.1.0"
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
}
|
||||
}
|
||||
|
6157
frontend/pnpm-lock.yaml
generated
Normal file
6157
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
frontend/src/api/api-definition.ts
Normal file
19
frontend/src/api/api-definition.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
UserPublic,
|
||||
UserRegister,
|
||||
UserUpdate,
|
||||
ShopLoginAccessTokenData,
|
||||
} from "@/client";
|
||||
import { Shop } from "./mock/models";
|
||||
|
||||
export interface AuthAPI {
|
||||
getCurrentUser(): Promise<UserPublic | null>;
|
||||
registerUser(data: UserRegister): Promise<void>;
|
||||
loginUser(data: ShopLoginAccessTokenData): Promise<{ access_token: string }>;
|
||||
updateUser(data: UserUpdate): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ShopAPI {
|
||||
getShop(): Promise<Shop | null>;
|
||||
updateShop(data: Partial<Shop>): Promise<void>;
|
||||
}
|
21
frontend/src/api/api.ts
Normal file
21
frontend/src/api/api.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { RealAuthAPI } from "./real/auth-real-api";
|
||||
import { MockAuthAPI } from "./mock/auth-mock-api";
|
||||
import { AuthAPI, ShopAPI } from "./api-definition";
|
||||
import { MockShopAPI } from "./mock/shop-mock-api";
|
||||
import { MockProductAPI } from "./mock/products-mock-api";
|
||||
import { MockUserAPI } from "./mock/user-mock-api";
|
||||
import { MockCouponAPI } from "./mock/coupon-mock-api";
|
||||
import { MockPurchaseAPI } from "./mock/purchase-mock-api";
|
||||
|
||||
export const authAPI: AuthAPI =
|
||||
import.meta.env.VITE_USE_MOCK_API === "true" ? MockAuthAPI : RealAuthAPI;
|
||||
|
||||
export const shopAPI: ShopAPI = MockShopAPI;
|
||||
|
||||
export const productsAPI = MockProductAPI;
|
||||
|
||||
export const usersAPI = MockUserAPI;
|
||||
|
||||
export const couponAPI = MockCouponAPI;
|
||||
|
||||
export const purchaseAPI = MockPurchaseAPI;
|
127
frontend/src/api/mock/auth-mock-api.ts
Normal file
127
frontend/src/api/mock/auth-mock-api.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { AuthAPI } from "../api-definition";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { UserRegister, UserUpdate, ShopLoginAccessTokenData } from "@/client";
|
||||
import { extractSubFromJWT, generateFakeJWT } from "@/utils/jwt";
|
||||
import { mockDB } from "./db";
|
||||
import { MockUser, Shop } from "./models";
|
||||
|
||||
const db = mockDB;
|
||||
|
||||
export const MockAuthAPI: AuthAPI = {
|
||||
async getCurrentUser() {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) return null;
|
||||
|
||||
const userUUID = extractSubFromJWT(token);
|
||||
if (!userUUID) return null;
|
||||
|
||||
const user = await db.users.where("uuid").equals(userUUID).first();
|
||||
if (!user) return null;
|
||||
|
||||
const publicUser = {
|
||||
...user,
|
||||
first_name: user.first_name ?? null,
|
||||
last_name: user.last_name ?? null,
|
||||
profile_picture: user.profile_picture ?? null,
|
||||
};
|
||||
return publicUser;
|
||||
},
|
||||
|
||||
async registerUser(data: UserRegister) {
|
||||
const now = new Date().toISOString();
|
||||
const userId = Date.now();
|
||||
const userUUID = uuidv4();
|
||||
const shopUUID = uuidv4();
|
||||
|
||||
const newUser: MockUser = {
|
||||
id: userId,
|
||||
uuid: userUUID,
|
||||
user_role: "owner",
|
||||
status: "owner",
|
||||
shop_id: null,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
first_name: undefined,
|
||||
last_name: undefined,
|
||||
phone_number: data.phone_number,
|
||||
profile_picture: undefined,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_login: now,
|
||||
};
|
||||
|
||||
const newShop: Shop = {
|
||||
id: userId,
|
||||
uuid: shopUUID,
|
||||
owner_id: userId,
|
||||
name: `${data.username}'s Shop`,
|
||||
description: "A newly created shop.",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
status: "inactive",
|
||||
logo: null,
|
||||
contact_email: data.email,
|
||||
contact_phone_number: data.phone_number,
|
||||
currency: "USD",
|
||||
address: {
|
||||
street: "",
|
||||
city: "",
|
||||
state: null,
|
||||
postal_code: "",
|
||||
country: "",
|
||||
},
|
||||
};
|
||||
|
||||
await mockDB.transaction(
|
||||
"rw",
|
||||
mockDB.users,
|
||||
mockDB.shops,
|
||||
mockDB.preferences,
|
||||
mockDB.statistics,
|
||||
async () => {
|
||||
await mockDB.users.add(newUser);
|
||||
await mockDB.preferences.add({ user_id: userId });
|
||||
await mockDB.statistics.add({ user_id: userId, total_spend: 0 });
|
||||
await mockDB.shops.add(newShop);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async loginUser(data: ShopLoginAccessTokenData) {
|
||||
const user = await db.users
|
||||
.where("email")
|
||||
.equals(data.formData.username)
|
||||
.first();
|
||||
|
||||
if (!user || user.password != data.formData.password) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
await db.users.update(user.id, { last_login: new Date().toISOString() });
|
||||
|
||||
const token = generateFakeJWT(user.uuid);
|
||||
localStorage.setItem("access_token", token);
|
||||
return { access_token: token };
|
||||
},
|
||||
|
||||
async updateUser(data: UserUpdate) {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) return;
|
||||
|
||||
const userUUID = extractSubFromJWT(token);
|
||||
if (!userUUID) return;
|
||||
|
||||
await db.users
|
||||
.where("uuid")
|
||||
.equals(userUUID)
|
||||
.modify({
|
||||
updated_at: new Date().toISOString(),
|
||||
email: data.email ?? "",
|
||||
phone_number: data.phone_number ?? "",
|
||||
username: data.username ?? "",
|
||||
first_name: data.first_name ?? undefined,
|
||||
last_name: data.last_name ?? undefined,
|
||||
});
|
||||
},
|
||||
};
|
45
frontend/src/api/mock/coupon-mock-api.ts
Normal file
45
frontend/src/api/mock/coupon-mock-api.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { mockDB } from "./db";
|
||||
import { Coupon } from "./models";
|
||||
|
||||
export const MockCouponAPI = {
|
||||
async getAllCoupons(): Promise<Coupon[]> {
|
||||
return mockDB.coupons.toArray();
|
||||
},
|
||||
|
||||
async getCouponById(id: number): Promise<Coupon | null> {
|
||||
return (await mockDB.coupons.get(id)) ?? null;
|
||||
},
|
||||
|
||||
async createCoupon(couponData: Omit<Coupon, "id">): Promise<Coupon> {
|
||||
const id = Date.now();
|
||||
|
||||
const newCoupon: Coupon = {
|
||||
...couponData,
|
||||
id,
|
||||
};
|
||||
|
||||
await mockDB.coupons.add(newCoupon);
|
||||
return newCoupon;
|
||||
},
|
||||
|
||||
async updateCoupon(
|
||||
id: number,
|
||||
data: Partial<Omit<Coupon, "id">>,
|
||||
): Promise<Coupon | null> {
|
||||
const existing = await mockDB.coupons.get(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const updated: Coupon = {
|
||||
...existing,
|
||||
...data,
|
||||
};
|
||||
|
||||
await mockDB.coupons.put(updated);
|
||||
return updated;
|
||||
},
|
||||
|
||||
async deleteCoupon(id: number): Promise<boolean> {
|
||||
await mockDB.coupons.delete(id);
|
||||
return true;
|
||||
},
|
||||
};
|
73
frontend/src/api/mock/data/create-purchase-data.ts
Normal file
73
frontend/src/api/mock/data/create-purchase-data.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { MockPurchaseAPI } from "../purchase-mock-api";
|
||||
import { mockDB } from "../db";
|
||||
|
||||
export async function createSamplePurchase() {
|
||||
const users = await mockDB.users.where("user_role").equals("customer").toArray();
|
||||
if (users.length === 0) throw new Error("No customers found");
|
||||
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const products = await mockDB.products.toArray();
|
||||
if (products.length === 0) throw new Error("No products found");
|
||||
|
||||
const entries = [];
|
||||
|
||||
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
|
||||
const product = products[Math.floor(Math.random() * products.length)];
|
||||
const quantity = Math.floor(Math.random() * 4) + 1;
|
||||
|
||||
entries.push({
|
||||
purchase_id: 0,
|
||||
product_id: product.id,
|
||||
product_variant_id: 0,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
if (entries.length === 0) throw new Error("No suitable product variants found");
|
||||
|
||||
let total = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const product = await mockDB.products.get(entry.product_id);
|
||||
if (product) {
|
||||
total += product.price * entry.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
const coupons = await mockDB.coupons.toArray();
|
||||
const validCoupons = coupons.filter(
|
||||
(c) => new Date(c.valid_due) > new Date()
|
||||
);
|
||||
const useCoupon = validCoupons.length > 0 && Math.random() < 0.5;
|
||||
const chosenCoupon = useCoupon
|
||||
? validCoupons[Math.floor(Math.random() * validCoupons.length)]
|
||||
: null;
|
||||
|
||||
const discountedTotal = chosenCoupon
|
||||
? Math.max(0, total - chosenCoupon.discount_amount)
|
||||
: total;
|
||||
|
||||
// ✅ Generate a random date within the past year
|
||||
const now = new Date();
|
||||
const monthsAgo = Math.floor(Math.random() * 12); // 0–11 months ago
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - monthsAgo, 1);
|
||||
|
||||
const randomDay = Math.floor(Math.random() * 28) + 1;
|
||||
date.setDate(randomDay);
|
||||
const isoDate = date.toISOString();
|
||||
|
||||
const purchase = await MockPurchaseAPI.createPurchase(
|
||||
{
|
||||
user_id: user.id,
|
||||
used_coupon_id: chosenCoupon?.id ?? null,
|
||||
date_purchased: isoDate,
|
||||
total: discountedTotal,
|
||||
},
|
||||
entries,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Created mock purchase${chosenCoupon ? " with coupon" : ""} on ${date.toDateString()}:`,
|
||||
purchase,
|
||||
);
|
||||
}
|
50
frontend/src/api/mock/db.ts
Normal file
50
frontend/src/api/mock/db.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import Dexie, { Table } from "dexie";
|
||||
import {
|
||||
Coupon,
|
||||
MockUser,
|
||||
MockUserPreferences,
|
||||
MockUserStatistics,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCategoryJunction,
|
||||
ProductImage,
|
||||
ProductVariant,
|
||||
Purchase,
|
||||
PurchaseEntry,
|
||||
Shop,
|
||||
} from "./models";
|
||||
|
||||
class MockDB extends Dexie {
|
||||
users!: Table<MockUser, number>;
|
||||
preferences!: Table<MockUserPreferences, number>;
|
||||
statistics!: Table<MockUserStatistics, number>;
|
||||
shops!: Table<Shop, number>;
|
||||
products!: Table<Product, number>;
|
||||
product_variants!: Table<ProductVariant, number>;
|
||||
product_images!: Table<ProductImage, number>;
|
||||
product_categories!: Table<ProductCategory, number>;
|
||||
product_category_junctions!: Table<ProductCategoryJunction, [number, number]>;
|
||||
coupons!: Table<Coupon, number>;
|
||||
purchases!: Table<Purchase, number>;
|
||||
purchase_entries!: Table<PurchaseEntry, number>;
|
||||
|
||||
constructor() {
|
||||
super("MockDB");
|
||||
this.version(1).stores({
|
||||
users: "++id,username,email,uuid,user_role",
|
||||
preferences: "user_id",
|
||||
statistics: "user_id",
|
||||
shops: "++id,uuid,name",
|
||||
products: "++id,shop_id,name",
|
||||
product_variants: "++id,product_id",
|
||||
product_images: "++id,product_id",
|
||||
product_categories: "++id,parent_category_id",
|
||||
product_category_junctions: "[product_id+category_id]",
|
||||
coupons: "++id,valid_due,name",
|
||||
purchases: "++id,user_id,date_purchased",
|
||||
purchase_entries: "++id,purchase_id,product_id,product_variant_id",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const mockDB = new MockDB();
|
140
frontend/src/api/mock/models.ts
Normal file
140
frontend/src/api/mock/models.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { UserRegister } from "@/client";
|
||||
|
||||
export type UserRole = "owner" | "customer" | "employee" | "manager" | "admin";
|
||||
|
||||
export interface MockUser extends Omit<UserRegister, "password"> {
|
||||
id: number;
|
||||
uuid: string;
|
||||
user_role: UserRole;
|
||||
shop_id: number | null;
|
||||
status: UserRole;
|
||||
password: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
profile_picture?: string;
|
||||
}
|
||||
|
||||
export interface MockUserPreferences {
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface MockUserStatistics {
|
||||
user_id: number;
|
||||
total_spend?: number;
|
||||
}
|
||||
|
||||
export type ShopStatus = "active" | "inactive" | "suspended";
|
||||
|
||||
export interface ShopAddress {
|
||||
street: string;
|
||||
city: string;
|
||||
state?: string | null;
|
||||
postal_code: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
id: number;
|
||||
uuid: string;
|
||||
owner_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: ShopStatus;
|
||||
logo?: string | null;
|
||||
contact_email: string;
|
||||
contact_phone_number: string;
|
||||
address: ShopAddress;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
shop_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
stock_quantity: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export interface ProductCategoryJunction {
|
||||
product_id: number;
|
||||
category_id: number;
|
||||
}
|
||||
|
||||
export interface ProductCategory {
|
||||
id: number;
|
||||
parent_category_id?: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProductImage {
|
||||
id: number;
|
||||
product_id: number;
|
||||
image_id: number;
|
||||
image_url: string;
|
||||
alt_text: string;
|
||||
}
|
||||
|
||||
export interface ProductCreate {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
stock_quantity: number;
|
||||
image_data?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ProductWithDetails extends Product {
|
||||
images: ProductImage[];
|
||||
categories: ProductCategory[];
|
||||
}
|
||||
|
||||
export interface ProductVariant {
|
||||
id: number;
|
||||
product_id: number;
|
||||
index: number;
|
||||
name: string;
|
||||
price: number;
|
||||
comment?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Coupon {
|
||||
id: number;
|
||||
name: string;
|
||||
text: string;
|
||||
valid_due: string;
|
||||
discount_amount: number;
|
||||
}
|
||||
|
||||
export interface Purchase {
|
||||
id: number;
|
||||
user_id: number;
|
||||
used_coupon_id: number | null;
|
||||
date_purchased: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface PurchaseEntry {
|
||||
id: number;
|
||||
purchase_id: number;
|
||||
product_id: number;
|
||||
product_variant_id: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface PurchaseWithDetails extends Purchase {
|
||||
user: MockUser;
|
||||
coupon?: Coupon | null;
|
||||
entries: PurchaseEntryWithProduct[];
|
||||
}
|
||||
|
||||
export interface PurchaseEntryWithProduct extends PurchaseEntry {
|
||||
product: Product;
|
||||
}
|
158
frontend/src/api/mock/products-mock-api.ts
Normal file
158
frontend/src/api/mock/products-mock-api.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { mockDB } from "./db";
|
||||
import { ProductCategory, ProductCreate, ProductWithDetails } from "./models";
|
||||
import { getCurrentUserDirect } from "./utils/currentUser";
|
||||
|
||||
export const MockProductAPI = {
|
||||
async getProductsForShop(): Promise<ProductWithDetails[]> {
|
||||
const user = await getCurrentUserDirect();
|
||||
if (!user?.id) return [];
|
||||
|
||||
const products = await mockDB.products
|
||||
.where("shop_id")
|
||||
.equals(user.id)
|
||||
.toArray();
|
||||
|
||||
const detailedProducts = await Promise.all(
|
||||
products.map(async (product) => {
|
||||
const images = await mockDB.product_images
|
||||
.where("product_id")
|
||||
.equals(product.id)
|
||||
.toArray();
|
||||
const variants = await mockDB.product_variants
|
||||
.where("product_id")
|
||||
.equals(product.id)
|
||||
.toArray();
|
||||
const categoryLinks = await mockDB.product_category_junctions
|
||||
.where("product_id")
|
||||
.equals(product.id)
|
||||
.toArray();
|
||||
const rawCategories = await Promise.all(
|
||||
categoryLinks.map((link) =>
|
||||
mockDB.product_categories.get(link.category_id),
|
||||
),
|
||||
);
|
||||
const categories = rawCategories.filter(
|
||||
(c): c is ProductCategory => c !== undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
...product,
|
||||
images,
|
||||
variants,
|
||||
categories,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return detailedProducts;
|
||||
},
|
||||
|
||||
async getProductById(productId: number): Promise<ProductWithDetails | null> {
|
||||
const product = await mockDB.products.get(productId);
|
||||
if (!product) return null;
|
||||
|
||||
const images = await mockDB.product_images
|
||||
.where("product_id")
|
||||
.equals(productId)
|
||||
.toArray();
|
||||
const categoryLinks = await mockDB.product_category_junctions
|
||||
.where("product_id")
|
||||
.equals(productId)
|
||||
.toArray();
|
||||
|
||||
const rawCategories = await Promise.all(
|
||||
categoryLinks.map((link) =>
|
||||
mockDB.product_categories.get(link.category_id),
|
||||
),
|
||||
);
|
||||
const categories = rawCategories.filter(
|
||||
(c): c is ProductCategory => c !== undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
...product,
|
||||
images,
|
||||
categories,
|
||||
};
|
||||
},
|
||||
|
||||
async createProduct(productData: ProductCreate) {
|
||||
const user = await getCurrentUserDirect();
|
||||
if (!user?.id) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const productId = Date.now();
|
||||
|
||||
await mockDB.products.add({
|
||||
id: productId,
|
||||
...productData,
|
||||
shop_id: user.id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
if ("image_data" in productData && productData.image_data) {
|
||||
const image_id = Date.now();
|
||||
await mockDB.product_images.add({
|
||||
id: image_id,
|
||||
product_id: productId,
|
||||
image_id: image_id,
|
||||
image_url: productData.image_data,
|
||||
alt_text: `${productData.name} image`,
|
||||
});
|
||||
}
|
||||
|
||||
return mockDB.products.get(productId);
|
||||
},
|
||||
|
||||
async updateProduct(productId: number, data: Partial<ProductCreate>) {
|
||||
const product = await mockDB.products.get(productId);
|
||||
if (!product) return null;
|
||||
|
||||
const updatedProduct = {
|
||||
...product,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
await mockDB.products.put(updatedProduct);
|
||||
|
||||
if (data.image_data) {
|
||||
const existingImage = await mockDB.product_images
|
||||
.where("product_id")
|
||||
.equals(productId)
|
||||
.first();
|
||||
if (existingImage) {
|
||||
await mockDB.product_images.put({
|
||||
...existingImage,
|
||||
image_url: data.image_data,
|
||||
alt_text: `${data.name ?? product.name} image`,
|
||||
});
|
||||
} else {
|
||||
const image_id = Date.now();
|
||||
await mockDB.product_images.add({
|
||||
id: image_id,
|
||||
product_id: productId,
|
||||
image_id: image_id,
|
||||
image_url: data.image_data,
|
||||
alt_text: `${data.name ?? product.name} image`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updatedProduct;
|
||||
},
|
||||
|
||||
async deleteProduct(productId: number) {
|
||||
await mockDB.product_images.where("product_id").equals(productId).delete();
|
||||
await mockDB.product_variants
|
||||
.where("product_id")
|
||||
.equals(productId)
|
||||
.delete();
|
||||
await mockDB.product_category_junctions
|
||||
.where("product_id")
|
||||
.equals(productId)
|
||||
.delete();
|
||||
await mockDB.products.delete(productId);
|
||||
return true;
|
||||
},
|
||||
};
|
99
frontend/src/api/mock/purchase-mock-api.ts
Normal file
99
frontend/src/api/mock/purchase-mock-api.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { mockDB } from "./db";
|
||||
import {
|
||||
Purchase,
|
||||
PurchaseWithDetails,
|
||||
PurchaseEntry,
|
||||
PurchaseEntryWithProduct,
|
||||
} from "./models";
|
||||
|
||||
export const MockPurchaseAPI = {
|
||||
async getAllPurchases(): Promise<Purchase[]> {
|
||||
return mockDB.purchases.toArray();
|
||||
},
|
||||
|
||||
async getPurchaseById(purchaseId: number): Promise<Purchase | null> {
|
||||
return (await mockDB.purchases.get(purchaseId)) ?? null;
|
||||
},
|
||||
|
||||
async getPurchaseWithDetails(
|
||||
purchaseId: number,
|
||||
): Promise<PurchaseWithDetails | null> {
|
||||
const purchase = await mockDB.purchases.get(purchaseId);
|
||||
if (!purchase) return null;
|
||||
|
||||
const [user, coupon, entries] = await Promise.all([
|
||||
mockDB.users.get(purchase.user_id),
|
||||
purchase.used_coupon_id
|
||||
? mockDB.coupons.get(purchase.used_coupon_id)
|
||||
: null,
|
||||
mockDB.purchase_entries.where("purchase_id").equals(purchaseId).toArray(),
|
||||
]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const entriesWithProducts: PurchaseEntryWithProduct[] = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const product = await mockDB.products.get(entry.product_id);
|
||||
if (!product) throw new Error("Product not found");
|
||||
return { ...entry, product };
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...purchase,
|
||||
user,
|
||||
coupon,
|
||||
entries: entriesWithProducts,
|
||||
};
|
||||
},
|
||||
|
||||
async getPurchasesByUser(userId: number): Promise<Purchase[]> {
|
||||
return mockDB.purchases.where("user_id").equals(userId).toArray();
|
||||
},
|
||||
|
||||
async createPurchase(
|
||||
purchase: Omit<Purchase, "id">,
|
||||
entries: Omit<PurchaseEntry, "id">[] = [],
|
||||
): Promise<Purchase> {
|
||||
const id = Date.now();
|
||||
const newPurchase: Purchase = {
|
||||
...purchase,
|
||||
id,
|
||||
};
|
||||
|
||||
await mockDB.purchases.add(newPurchase);
|
||||
|
||||
if (entries.length > 0) {
|
||||
await mockDB.purchase_entries.bulkAdd(
|
||||
entries.map((entry) => ({
|
||||
...entry,
|
||||
purchase_id: id,
|
||||
id: Date.now() + Math.random(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return newPurchase;
|
||||
},
|
||||
|
||||
async updatePurchase(
|
||||
purchaseId: number,
|
||||
updates: Partial<Omit<Purchase, "id">>,
|
||||
): Promise<Purchase | null> {
|
||||
const existing = await mockDB.purchases.get(purchaseId);
|
||||
if (!existing) return null;
|
||||
|
||||
const updated = { ...existing, ...updates };
|
||||
await mockDB.purchases.put(updated);
|
||||
return updated;
|
||||
},
|
||||
|
||||
async deletePurchase(purchaseId: number): Promise<boolean> {
|
||||
await mockDB.purchase_entries
|
||||
.where("purchase_id")
|
||||
.equals(purchaseId)
|
||||
.delete();
|
||||
await mockDB.purchases.delete(purchaseId);
|
||||
return true;
|
||||
},
|
||||
};
|
41
frontend/src/api/mock/shop-mock-api.ts
Normal file
41
frontend/src/api/mock/shop-mock-api.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { ShopAPI } from "../api-definition";
|
||||
import { mockDB } from "./db";
|
||||
import { extractSubFromJWT } from "@/utils/jwt";
|
||||
|
||||
const db = mockDB;
|
||||
|
||||
export const MockShopAPI: ShopAPI = {
|
||||
async getShop() {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) return null;
|
||||
|
||||
const uuid = extractSubFromJWT(token);
|
||||
if (!uuid) return null;
|
||||
|
||||
const user = await db.users.where("uuid").equals(uuid).first();
|
||||
if (!user?.id) return null;
|
||||
|
||||
const dbShop = await db.shops.get(user.id);
|
||||
return dbShop ?? null;
|
||||
},
|
||||
|
||||
async updateShop(data) {
|
||||
const token = localStorage.getItem("access_token");
|
||||
console.log("Token ->", token);
|
||||
if (!token) return;
|
||||
|
||||
const uuid = extractSubFromJWT(token);
|
||||
console.log("UUID ->", uuid);
|
||||
if (!uuid) return;
|
||||
|
||||
const user = await db.users.where("uuid").equals(uuid).first();
|
||||
console.log("User ->", user);
|
||||
if (!user?.id) return;
|
||||
|
||||
console.log("Saving data ->", data);
|
||||
await db.shops.update(user.id, {
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
};
|
60
frontend/src/api/mock/user-mock-api.ts
Normal file
60
frontend/src/api/mock/user-mock-api.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { mockDB } from "./db";
|
||||
import { MockUser } from "./models";
|
||||
import { getCurrentUserDirect } from "./utils/currentUser";
|
||||
|
||||
export const MockUserAPI = {
|
||||
async getAllUsers(): Promise<MockUser[]> {
|
||||
return mockDB.users.toArray();
|
||||
},
|
||||
|
||||
async getUserById(userId: number): Promise<MockUser | null> {
|
||||
return (await mockDB.users.get(userId)) ?? null;
|
||||
},
|
||||
|
||||
async createUser(
|
||||
userData: Omit<MockUser, "id" | "created_at" | "updated_at">,
|
||||
): Promise<MockUser> {
|
||||
const now = new Date().toISOString();
|
||||
const id = Date.now();
|
||||
|
||||
const newUser: MockUser = {
|
||||
...userData,
|
||||
id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
await mockDB.users.add(newUser);
|
||||
return newUser;
|
||||
},
|
||||
|
||||
async updateUser(
|
||||
userId: number,
|
||||
data: Partial<Omit<MockUser, "id" | "created_at">>,
|
||||
): Promise<MockUser | null> {
|
||||
const existingUser = await mockDB.users.get(userId);
|
||||
if (!existingUser) return null;
|
||||
|
||||
const updatedUser: MockUser = {
|
||||
...existingUser,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await mockDB.users.put(updatedUser);
|
||||
return updatedUser;
|
||||
},
|
||||
|
||||
async deleteUser(userId: number): Promise<boolean> {
|
||||
await mockDB.preferences.where("user_id").equals(userId).delete();
|
||||
await mockDB.statistics.where("user_id").equals(userId).delete();
|
||||
await mockDB.users.delete(userId);
|
||||
return true;
|
||||
},
|
||||
|
||||
async getCurrentUser(): Promise<MockUser | null> {
|
||||
const user = await getCurrentUserDirect();
|
||||
if (!user?.id) return null;
|
||||
return (await mockDB.users.get(user.id)) ?? null;
|
||||
},
|
||||
};
|
13
frontend/src/api/mock/utils/currentUser.ts
Normal file
13
frontend/src/api/mock/utils/currentUser.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { mockDB } from "../db";
|
||||
import { extractSubFromJWT } from "@/utils/jwt";
|
||||
|
||||
export async function getCurrentUserDirect() {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) return null;
|
||||
|
||||
const uuid = extractSubFromJWT(token);
|
||||
if (!uuid) return null;
|
||||
|
||||
const user = await mockDB.users.where("uuid").equals(uuid).first();
|
||||
return user ?? null;
|
||||
}
|
7
frontend/src/api/mock/utils/imageToBase64.ts
Normal file
7
frontend/src/api/mock/utils/imageToBase64.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const fileToBase64 = (file: File): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
19
frontend/src/api/real/auth-real-api.ts
Normal file
19
frontend/src/api/real/auth-real-api.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { DashboardService, LoginService, UserService } from "@/client";
|
||||
import { AuthAPI } from "../api-definition";
|
||||
|
||||
export const RealAuthAPI: AuthAPI = {
|
||||
async getCurrentUser() {
|
||||
return DashboardService.userGetUser();
|
||||
},
|
||||
async registerUser(data) {
|
||||
await DashboardService.userRegister({ requestBody: data });
|
||||
},
|
||||
async loginUser(data) {
|
||||
return LoginService.dashboardLoginAccessToken({
|
||||
formData: data.formData,
|
||||
});
|
||||
},
|
||||
async updateUser(data) {
|
||||
await UserService.userUpdateUser({ requestBody: data });
|
||||
},
|
||||
};
|
12
frontend/src/assets/placeholder.svg
Normal file
12
frontend/src/assets/placeholder.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg
|
||||
viewBox="0 0 120 120"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="120" height="120" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 763 B |
@ -1,5 +1,5 @@
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import type { ApiResult } from "./ApiResult";
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
@ -8,10 +8,14 @@ export class ApiError extends Error {
|
||||
public readonly body: unknown;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
constructor(
|
||||
request: ApiRequestOptions,
|
||||
response: ApiResult,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.name = "ApiError";
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
|
@ -6,13 +6,13 @@ export type ApiRequestOptions<T = unknown> = {
|
||||
readonly headers?: Record<string, unknown>;
|
||||
readonly mediaType?: string;
|
||||
readonly method:
|
||||
| 'DELETE'
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'OPTIONS'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT';
|
||||
| "DELETE"
|
||||
| "GET"
|
||||
| "HEAD"
|
||||
| "OPTIONS"
|
||||
| "PATCH"
|
||||
| "POST"
|
||||
| "PUT";
|
||||
readonly path?: Record<string, unknown>;
|
||||
readonly query?: Record<string, unknown>;
|
||||
readonly responseHeader?: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
export class CancelError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelError';
|
||||
this.name = "CancelError";
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
@ -30,8 +30,8 @@ export class CancelablePromise<T> implements Promise<T> {
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
onCancel: OnCancel
|
||||
) => void
|
||||
onCancel: OnCancel,
|
||||
) => void,
|
||||
) {
|
||||
this._isResolved = false;
|
||||
this._isRejected = false;
|
||||
@ -64,15 +64,15 @@ export class CancelablePromise<T> implements Promise<T> {
|
||||
this.cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, 'isResolved', {
|
||||
Object.defineProperty(onCancel, "isResolved", {
|
||||
get: (): boolean => this._isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isRejected', {
|
||||
Object.defineProperty(onCancel, "isRejected", {
|
||||
get: (): boolean => this._isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isCancelled', {
|
||||
Object.defineProperty(onCancel, "isCancelled", {
|
||||
get: (): boolean => this._isCancelled,
|
||||
});
|
||||
|
||||
@ -86,13 +86,13 @@ export class CancelablePromise<T> implements Promise<T> {
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
|
||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
|
||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onRejected);
|
||||
}
|
||||
@ -112,12 +112,12 @@ export class CancelablePromise<T> implements Promise<T> {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cancellation threw an error', error);
|
||||
console.warn("Cancellation threw an error", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.cancelHandlers.length = 0;
|
||||
if (this._reject) this._reject(new CancelError('Request aborted'));
|
||||
if (this._reject) this._reject(new CancelError("Request aborted"));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
|
||||
type Headers = Record<string, string>;
|
||||
type Middleware<T> = (value: T) => T | Promise<T>;
|
||||
@ -26,7 +26,7 @@ export class Interceptors<T> {
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
@ -41,14 +41,14 @@ export type OpenAPIConfig = {
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '',
|
||||
CREDENTIALS: 'include',
|
||||
BASE: "",
|
||||
CREDENTIALS: "include",
|
||||
ENCODE_PATH: undefined,
|
||||
HEADERS: undefined,
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: '0.0.1',
|
||||
VERSION: "0.0.1",
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
|
@ -1,19 +1,24 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
||||
import axios from "axios";
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosInstance,
|
||||
} from "axios";
|
||||
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import { CancelablePromise } from './CancelablePromise';
|
||||
import type { OnCancel } from './CancelablePromise';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
import { ApiError } from "./ApiError";
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import type { ApiResult } from "./ApiResult";
|
||||
import { CancelablePromise } from "./CancelablePromise";
|
||||
import type { OnCancel } from "./CancelablePromise";
|
||||
import type { OpenAPIConfig } from "./OpenAPI";
|
||||
|
||||
export const isString = (value: unknown): value is string => {
|
||||
return typeof value === 'string';
|
||||
return typeof value === "string";
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: unknown): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
return isString(value) && value !== "";
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
@ -33,7 +38,7 @@ export const base64 = (str: string): string => {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
return Buffer.from(str).toString("base64");
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,8 +57,8 @@ export const getQueryString = (params: Record<string, unknown>): string => {
|
||||
if (value instanceof Date) {
|
||||
append(key, value.toISOString());
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(v => encodePair(key, v));
|
||||
} else if (typeof value === 'object') {
|
||||
value.forEach((v) => encodePair(key, v));
|
||||
} else if (typeof value === "object") {
|
||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
||||
} else {
|
||||
append(key, value);
|
||||
@ -62,14 +67,14 @@ export const getQueryString = (params: Record<string, unknown>): string => {
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
||||
|
||||
return qs.length ? `?${qs.join('&')}` : '';
|
||||
return qs.length ? `?${qs.join("&")}` : "";
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace("{api-version}", config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
@ -81,7 +86,9 @@ const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
return options.query ? url + getQueryString(options.query) : url;
|
||||
};
|
||||
|
||||
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
export const getFormData = (
|
||||
options: ApiRequestOptions,
|
||||
): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
@ -97,7 +104,7 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
value.forEach((v) => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
@ -110,14 +117,20 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
export const resolve = async <T>(
|
||||
options: ApiRequestOptions<T>,
|
||||
resolver?: T | Resolver<T>,
|
||||
): Promise<T | undefined> => {
|
||||
if (typeof resolver === "function") {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): Promise<Record<string, string>> => {
|
||||
export const getHeaders = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
// @ts-ignore
|
||||
resolve(options, config.TOKEN),
|
||||
@ -130,38 +143,41 @@ export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOp
|
||||
]);
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
Accept: "application/json",
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.reduce((headers, [key, value]) => ({
|
||||
.reduce(
|
||||
(headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
}),
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
headers["Authorization"] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
headers["Content-Type"] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
headers["Content-Type"] = options.body.type || "application/octet-stream";
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
headers["Content-Type"] = "text/plain";
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
} else if (options.formData !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
headers["Content-Type"] = options.mediaType;
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +199,7 @@ export const sendRequest = async <T>(
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance
|
||||
axiosClient: AxiosInstance,
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const controller = new AbortController();
|
||||
|
||||
@ -213,7 +229,10 @@ export const sendRequest = async <T>(
|
||||
}
|
||||
};
|
||||
|
||||
export const getResponseHeader = (response: AxiosResponse<unknown>, responseHeader?: string): string | undefined => {
|
||||
export const getResponseHeader = (
|
||||
response: AxiosResponse<unknown>,
|
||||
responseHeader?: string,
|
||||
): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader];
|
||||
if (isString(content)) {
|
||||
@ -230,50 +249,53 @@ export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
export const catchErrorCodes = (
|
||||
options: ApiRequestOptions,
|
||||
result: ApiResult,
|
||||
): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
402: 'Payment Required',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
405: 'Method Not Allowed',
|
||||
406: 'Not Acceptable',
|
||||
407: 'Proxy Authentication Required',
|
||||
408: 'Request Timeout',
|
||||
409: 'Conflict',
|
||||
410: 'Gone',
|
||||
411: 'Length Required',
|
||||
412: 'Precondition Failed',
|
||||
413: 'Payload Too Large',
|
||||
414: 'URI Too Long',
|
||||
415: 'Unsupported Media Type',
|
||||
416: 'Range Not Satisfiable',
|
||||
417: 'Expectation Failed',
|
||||
418: 'Im a teapot',
|
||||
421: 'Misdirected Request',
|
||||
422: 'Unprocessable Content',
|
||||
423: 'Locked',
|
||||
424: 'Failed Dependency',
|
||||
425: 'Too Early',
|
||||
426: 'Upgrade Required',
|
||||
428: 'Precondition Required',
|
||||
429: 'Too Many Requests',
|
||||
431: 'Request Header Fields Too Large',
|
||||
451: 'Unavailable For Legal Reasons',
|
||||
500: 'Internal Server Error',
|
||||
501: 'Not Implemented',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
504: 'Gateway Timeout',
|
||||
505: 'HTTP Version Not Supported',
|
||||
506: 'Variant Also Negotiates',
|
||||
507: 'Insufficient Storage',
|
||||
508: 'Loop Detected',
|
||||
510: 'Not Extended',
|
||||
511: 'Network Authentication Required',
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Payload Too Large",
|
||||
414: "URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "Im a teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Content",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
...options.errors,
|
||||
}
|
||||
};
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
@ -281,8 +303,8 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? 'unknown';
|
||||
const errorStatusText = result.statusText ?? 'unknown';
|
||||
const errorStatus = result.status ?? "unknown";
|
||||
const errorStatusText = result.statusText ?? "unknown";
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
@ -291,8 +313,10 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(options, result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||
throw new ApiError(
|
||||
options,
|
||||
result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -305,7 +329,11 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
|
||||
export const request = <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
@ -314,18 +342,30 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>,
|
||||
const headers = await getHeaders(config, options);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
let response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
|
||||
let response = await sendRequest<T>(
|
||||
config,
|
||||
options,
|
||||
url,
|
||||
body,
|
||||
formData,
|
||||
headers,
|
||||
onCancel,
|
||||
axiosClient,
|
||||
);
|
||||
|
||||
for (const fn of config.interceptors.response._fns) {
|
||||
response = await fn(response);
|
||||
}
|
||||
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
const responseHeader = getResponseHeader(
|
||||
response,
|
||||
options.responseHeader,
|
||||
);
|
||||
|
||||
let transformedBody = responseBody;
|
||||
if (options.responseTransformer && isSuccess(response.status)) {
|
||||
transformedBody = await options.responseTransformer(responseBody)
|
||||
transformedBody = await options.responseTransformer(responseBody);
|
||||
}
|
||||
|
||||
const result: ApiResult = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
|
||||
export * from './sdk.gen';
|
||||
export * from './types.gen';
|
||||
export { ApiError } from "./core/ApiError";
|
||||
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
|
||||
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI";
|
||||
export * from "./sdk.gen";
|
||||
export * from "./types.gen";
|
||||
|
@ -1,9 +1,31 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { CancelablePromise } from './core/CancelablePromise';
|
||||
import { OpenAPI } from './core/OpenAPI';
|
||||
import { request as __request } from './core/request';
|
||||
import type { UserGetUserResponse, UserUpdateUserData, UserUpdateUserResponse, UserRegisterData, UserRegisterResponse, UserDeleteUserResponse, DashboardLoginAccessTokenData, DashboardLoginAccessTokenResponse, DashboardRegisterNewShopData, DashboardRegisterNewShopResponse, ShopLoginAccessTokenData, ShopLoginAccessTokenResponse, ShopDeleteUserData, ShopDeleteUserResponse, ShopLogoutResponse, ShopRegisterData, ShopRegisterResponse, ShopUpdateUserData, ShopUpdateUserResponse, UtilsHealthCheckResponse, UtilsTestDbResponse } from './types.gen';
|
||||
import type { CancelablePromise } from "./core/CancelablePromise";
|
||||
import { OpenAPI } from "./core/OpenAPI";
|
||||
import { request as __request } from "./core/request";
|
||||
import type {
|
||||
UserGetUserResponse,
|
||||
UserUpdateUserData,
|
||||
UserUpdateUserResponse,
|
||||
UserRegisterData,
|
||||
UserRegisterResponse,
|
||||
UserDeleteUserResponse,
|
||||
DashboardLoginAccessTokenData,
|
||||
DashboardLoginAccessTokenResponse,
|
||||
DashboardRegisterNewShopData,
|
||||
DashboardRegisterNewShopResponse,
|
||||
ShopLoginAccessTokenData,
|
||||
ShopLoginAccessTokenResponse,
|
||||
ShopDeleteUserData,
|
||||
ShopDeleteUserResponse,
|
||||
ShopLogoutResponse,
|
||||
ShopRegisterData,
|
||||
ShopRegisterResponse,
|
||||
ShopUpdateUserData,
|
||||
ShopUpdateUserResponse,
|
||||
UtilsHealthCheckResponse,
|
||||
UtilsTestDbResponse,
|
||||
} from "./types.gen";
|
||||
|
||||
export class DashboardService {
|
||||
/**
|
||||
@ -13,8 +35,8 @@ export class DashboardService {
|
||||
*/
|
||||
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/user'
|
||||
method: "GET",
|
||||
url: "/user",
|
||||
});
|
||||
}
|
||||
|
||||
@ -25,15 +47,17 @@ export class DashboardService {
|
||||
* @returns boolean Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
|
||||
public static userUpdateUser(
|
||||
data: UserUpdateUserData,
|
||||
): CancelablePromise<UserUpdateUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/user',
|
||||
method: "PUT",
|
||||
url: "/user",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,15 +68,17 @@ export class DashboardService {
|
||||
* @returns boolean Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
|
||||
public static userRegister(
|
||||
data: UserRegisterData,
|
||||
): CancelablePromise<UserRegisterResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/user',
|
||||
method: "POST",
|
||||
url: "/user",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -63,8 +89,8 @@ export class DashboardService {
|
||||
*/
|
||||
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/user'
|
||||
method: "DELETE",
|
||||
url: "/user",
|
||||
});
|
||||
}
|
||||
|
||||
@ -86,15 +112,17 @@ export class DashboardService {
|
||||
* @returns Token Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||
public static dashboardLoginAccessToken(
|
||||
data: DashboardLoginAccessTokenData,
|
||||
): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/login/access-token',
|
||||
method: "POST",
|
||||
url: "/login/access-token",
|
||||
formData: data.formData,
|
||||
mediaType: 'application/x-www-form-urlencoded',
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -105,18 +133,19 @@ export class DashboardService {
|
||||
* @returns boolean Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static dashboardRegisterNewShop(data: DashboardRegisterNewShopData): CancelablePromise<DashboardRegisterNewShopResponse> {
|
||||
public static dashboardRegisterNewShop(
|
||||
data: DashboardRegisterNewShopData,
|
||||
): CancelablePromise<DashboardRegisterNewShopResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/shop',
|
||||
method: "POST",
|
||||
url: "/shop",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class LoginService {
|
||||
@ -138,15 +167,17 @@ export class LoginService {
|
||||
* @returns Token Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||
public static dashboardLoginAccessToken(
|
||||
data: DashboardLoginAccessTokenData,
|
||||
): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/login/access-token',
|
||||
method: "POST",
|
||||
url: "/login/access-token",
|
||||
formData: data.formData,
|
||||
mediaType: 'application/x-www-form-urlencoded',
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -158,18 +189,19 @@ export class LoginService {
|
||||
* @returns Token Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||
public static shopLoginAccessToken(
|
||||
data: ShopLoginAccessTokenData,
|
||||
): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/shop/{shop_uuid}/login/access-token',
|
||||
method: "POST",
|
||||
url: "/shop/{shop_uuid}/login/access-token",
|
||||
formData: data.formData,
|
||||
mediaType: 'application/x-www-form-urlencoded',
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ShopService {
|
||||
@ -181,15 +213,17 @@ export class ShopService {
|
||||
* @returns Token Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||
public static shopLoginAccessToken(
|
||||
data: ShopLoginAccessTokenData,
|
||||
): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/shop/{shop_uuid}/login/access-token',
|
||||
method: "POST",
|
||||
url: "/shop/{shop_uuid}/login/access-token",
|
||||
formData: data.formData,
|
||||
mediaType: 'application/x-www-form-urlencoded',
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -200,16 +234,18 @@ export class ShopService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
|
||||
public static shopDeleteUser(
|
||||
data: ShopDeleteUserData,
|
||||
): CancelablePromise<ShopDeleteUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/shop/{shop_uuid}/user/delete',
|
||||
method: "DELETE",
|
||||
url: "/shop/{shop_uuid}/user/delete",
|
||||
path: {
|
||||
shop_uuid: data.shopUuid
|
||||
shop_uuid: data.shopUuid,
|
||||
},
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -220,8 +256,8 @@ export class ShopService {
|
||||
*/
|
||||
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/shop/{shop_uuid}/user/logout'
|
||||
method: "DELETE",
|
||||
url: "/shop/{shop_uuid}/user/logout",
|
||||
});
|
||||
}
|
||||
|
||||
@ -233,18 +269,20 @@ export class ShopService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
|
||||
public static shopRegister(
|
||||
data: ShopRegisterData,
|
||||
): CancelablePromise<ShopRegisterResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/shop/{shop_uuid}/user/register',
|
||||
method: "POST",
|
||||
url: "/shop/{shop_uuid}/user/register",
|
||||
path: {
|
||||
shop_uuid: data.shopUuid
|
||||
shop_uuid: data.shopUuid,
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -255,18 +293,19 @@ export class ShopService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
|
||||
public static shopUpdateUser(
|
||||
data: ShopUpdateUserData,
|
||||
): CancelablePromise<ShopUpdateUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/shop/{shop_uuid}/user/update',
|
||||
method: "PUT",
|
||||
url: "/shop/{shop_uuid}/user/update",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
@ -277,8 +316,8 @@ export class UserService {
|
||||
*/
|
||||
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/user'
|
||||
method: "GET",
|
||||
url: "/user",
|
||||
});
|
||||
}
|
||||
|
||||
@ -289,15 +328,17 @@ export class UserService {
|
||||
* @returns boolean Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
|
||||
public static userUpdateUser(
|
||||
data: UserUpdateUserData,
|
||||
): CancelablePromise<UserUpdateUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/user',
|
||||
method: "PUT",
|
||||
url: "/user",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -308,15 +349,17 @@ export class UserService {
|
||||
* @returns boolean Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
|
||||
public static userRegister(
|
||||
data: UserRegisterData,
|
||||
): CancelablePromise<UserRegisterResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/user',
|
||||
method: "POST",
|
||||
url: "/user",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -327,8 +370,8 @@ export class UserService {
|
||||
*/
|
||||
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/user'
|
||||
method: "DELETE",
|
||||
url: "/user",
|
||||
});
|
||||
}
|
||||
|
||||
@ -339,16 +382,18 @@ export class UserService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
|
||||
public static shopDeleteUser(
|
||||
data: ShopDeleteUserData,
|
||||
): CancelablePromise<ShopDeleteUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/shop/{shop_uuid}/user/delete',
|
||||
method: "DELETE",
|
||||
url: "/shop/{shop_uuid}/user/delete",
|
||||
path: {
|
||||
shop_uuid: data.shopUuid
|
||||
shop_uuid: data.shopUuid,
|
||||
},
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -359,8 +404,8 @@ export class UserService {
|
||||
*/
|
||||
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/shop/{shop_uuid}/user/logout'
|
||||
method: "DELETE",
|
||||
url: "/shop/{shop_uuid}/user/logout",
|
||||
});
|
||||
}
|
||||
|
||||
@ -372,18 +417,20 @@ export class UserService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
|
||||
public static shopRegister(
|
||||
data: ShopRegisterData,
|
||||
): CancelablePromise<ShopRegisterResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/shop/{shop_uuid}/user/register',
|
||||
method: "POST",
|
||||
url: "/shop/{shop_uuid}/user/register",
|
||||
path: {
|
||||
shop_uuid: data.shopUuid
|
||||
shop_uuid: data.shopUuid,
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -394,18 +441,19 @@ export class UserService {
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
|
||||
public static shopUpdateUser(
|
||||
data: ShopUpdateUserData,
|
||||
): CancelablePromise<ShopUpdateUserResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/shop/{shop_uuid}/user/update',
|
||||
method: "PUT",
|
||||
url: "/shop/{shop_uuid}/user/update",
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: 'Validation Error'
|
||||
}
|
||||
422: "Validation Error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UtilsService {
|
||||
@ -417,8 +465,8 @@ export class UtilsService {
|
||||
*/
|
||||
public static utilsHealthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/utils/health-check/'
|
||||
method: "GET",
|
||||
url: "/utils/health-check/",
|
||||
});
|
||||
}
|
||||
|
||||
@ -430,9 +478,8 @@ export class UtilsService {
|
||||
*/
|
||||
public static utilsTestDb(): CancelablePromise<UtilsTestDbResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/utils/test-db/'
|
||||
method: "GET",
|
||||
url: "/utils/test-db/",
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type Body_Dashboard_login_access_token = {
|
||||
grant_type?: (string | null);
|
||||
grant_type?: string | null;
|
||||
username: string;
|
||||
password: string;
|
||||
scope?: string;
|
||||
client_id?: (string | null);
|
||||
client_secret?: (string | null);
|
||||
client_id?: string | null;
|
||||
client_secret?: string | null;
|
||||
};
|
||||
|
||||
export type Body_Shop_login_access_token = {
|
||||
grant_type?: (string | null);
|
||||
grant_type?: string | null;
|
||||
username: string;
|
||||
password: string;
|
||||
scope?: string;
|
||||
client_id?: (string | null);
|
||||
client_secret?: (string | null);
|
||||
client_id?: string | null;
|
||||
client_secret?: string | null;
|
||||
};
|
||||
|
||||
export type HTTPValidationError = {
|
||||
@ -48,8 +48,8 @@ export type UserPublic = {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
first_name: (string | null);
|
||||
last_name: (string | null);
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
phone_number: string;
|
||||
};
|
||||
|
||||
@ -67,67 +67,67 @@ export type UserRegister = {
|
||||
};
|
||||
|
||||
export type UserUpdate = {
|
||||
email: (string | null);
|
||||
phone_number: (string | null);
|
||||
username: (string | null);
|
||||
first_name?: (string | null);
|
||||
last_name?: (string | null);
|
||||
email: string | null;
|
||||
phone_number: string | null;
|
||||
username: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
};
|
||||
|
||||
export type ValidationError = {
|
||||
loc: Array<(string | number)>;
|
||||
loc: Array<string | number>;
|
||||
msg: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type UserGetUserResponse = (UserPublic);
|
||||
export type UserGetUserResponse = UserPublic;
|
||||
|
||||
export type UserUpdateUserData = {
|
||||
requestBody: UserUpdate;
|
||||
};
|
||||
|
||||
export type UserUpdateUserResponse = (boolean);
|
||||
export type UserUpdateUserResponse = boolean;
|
||||
|
||||
export type UserRegisterData = {
|
||||
requestBody: UserRegister;
|
||||
};
|
||||
|
||||
export type UserRegisterResponse = (boolean);
|
||||
export type UserRegisterResponse = boolean;
|
||||
|
||||
export type UserDeleteUserResponse = (boolean);
|
||||
export type UserDeleteUserResponse = boolean;
|
||||
|
||||
export type DashboardLoginAccessTokenData = {
|
||||
formData: Body_Dashboard_login_access_token;
|
||||
};
|
||||
|
||||
export type DashboardLoginAccessTokenResponse = (Token);
|
||||
export type DashboardLoginAccessTokenResponse = Token;
|
||||
|
||||
export type DashboardRegisterNewShopData = {
|
||||
requestBody: ShopCreate;
|
||||
};
|
||||
|
||||
export type DashboardRegisterNewShopResponse = (boolean);
|
||||
export type DashboardRegisterNewShopResponse = boolean;
|
||||
|
||||
export type ShopLoginAccessTokenData = {
|
||||
formData: Body_Shop_login_access_token;
|
||||
};
|
||||
|
||||
export type ShopLoginAccessTokenResponse = (Token);
|
||||
export type ShopLoginAccessTokenResponse = Token;
|
||||
|
||||
export type ShopDeleteUserData = {
|
||||
shopUuid: unknown;
|
||||
};
|
||||
|
||||
export type ShopDeleteUserResponse = (unknown);
|
||||
export type ShopDeleteUserResponse = unknown;
|
||||
|
||||
export type ShopLogoutResponse = (unknown);
|
||||
export type ShopLogoutResponse = unknown;
|
||||
|
||||
export type ShopRegisterData = {
|
||||
requestBody: UserRegister;
|
||||
shopUuid: unknown;
|
||||
};
|
||||
|
||||
export type ShopRegisterResponse = (unknown);
|
||||
export type ShopRegisterResponse = unknown;
|
||||
|
||||
export type ShopUpdateUserData = {
|
||||
requestBody: {
|
||||
@ -135,8 +135,8 @@ export type ShopUpdateUserData = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopUpdateUserResponse = (unknown);
|
||||
export type ShopUpdateUserResponse = unknown;
|
||||
|
||||
export type UtilsHealthCheckResponse = (boolean);
|
||||
export type UtilsHealthCheckResponse = boolean;
|
||||
|
||||
export type UtilsTestDbResponse = (boolean);
|
||||
export type UtilsTestDbResponse = boolean;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
IconArrowRightDashed,
|
||||
IconDeviceLaptop,
|
||||
IconMoon,
|
||||
IconSun
|
||||
IconSun,
|
||||
} from "@tabler/icons-react";
|
||||
import { useSearch } from "@/context/search-context";
|
||||
import { useTheme } from "@/context/theme-context";
|
||||
@ -15,7 +15,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { sidebarData } from "./layout/data/sidebar-data";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
@ -30,7 +30,7 @@ export function CommandMenu() {
|
||||
setOpen(false);
|
||||
command();
|
||||
},
|
||||
[setOpen]
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -49,7 +49,8 @@ export function CommandMenu() {
|
||||
value={navItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: navItem.url }));
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||
</div>
|
||||
@ -63,7 +64,8 @@ export function CommandMenu() {
|
||||
value={subItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: subItem.url }));
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@ -56,7 +56,8 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||
<Button
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}>
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{confirmText ?? "Continue"}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
|
157
frontend/src/components/currency-input.tsx
Normal file
157
frontend/src/components/currency-input.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
export const currencies = [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYN",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"CHF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRU",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLE",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STN",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VED",
|
||||
"VES",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMW",
|
||||
"ZWG",
|
||||
];
|
@ -1,33 +1,19 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarRail
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { NavGroup } from "@/components/layout/nav-group";
|
||||
import { sidebarData } from "./data/sidebar-data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="icon" variant="floating" {...props}>
|
||||
<SidebarHeader>
|
||||
<h1
|
||||
className={cn(
|
||||
"header-fixed peer/header flex h-16 w-[inherit] items-center gap-3 rounded-md bg-background p-4 text-xl font-bold sm:gap-4"
|
||||
)}
|
||||
{...props}>
|
||||
SwagShop
|
||||
</h1>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props) => (
|
||||
<NavGroup key={props.title} {...props} />
|
||||
))}
|
||||
</SidebarContent>
|
||||
{/* <SidebarFooter>
|
||||
<NavUser user={sidebarData.user} />
|
||||
</SidebarFooter> */}
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
|
@ -1,21 +1,14 @@
|
||||
import {
|
||||
IconBrowserCheck,
|
||||
import
|
||||
{
|
||||
IconBuildingStore,
|
||||
IconCoin,
|
||||
IconForklift,
|
||||
IconHelp,
|
||||
IconLayoutDashboard,
|
||||
IconNotification,
|
||||
IconPackage,
|
||||
IconClipboardCheckFilled,
|
||||
IconCoin, IconLayoutDashboard, IconPackage,
|
||||
IconPalette,
|
||||
IconPercentage,
|
||||
IconSettings,
|
||||
IconTag,
|
||||
IconTool,
|
||||
IconUserCog,
|
||||
IconUsers
|
||||
} from "@tabler/icons-react";
|
||||
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
|
||||
import { type SidebarData } from "../types";
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
@ -26,45 +19,40 @@ export const sidebarData: SidebarData = {
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/dashboard",
|
||||
icon: IconLayoutDashboard
|
||||
icon: IconLayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: "Shop",
|
||||
url: "/dashboard/shop",
|
||||
icon: IconBuildingStore
|
||||
icon: IconBuildingStore,
|
||||
},
|
||||
{
|
||||
title: "Products",
|
||||
url: "/dashboard/products",
|
||||
icon: IconPackage
|
||||
},
|
||||
{
|
||||
title: "Inventory",
|
||||
url: "/dashboard/tasks",
|
||||
icon: IconForklift
|
||||
icon: IconPackage,
|
||||
},
|
||||
{
|
||||
title: "Sales",
|
||||
icon: IconCoin,
|
||||
items: [
|
||||
{
|
||||
title: "Discounts",
|
||||
url: "/dashboard/sales/discounts",
|
||||
icon: IconPercentage
|
||||
title: "Recent sales",
|
||||
url: "/dashboard/sales/recent-sales",
|
||||
icon: IconClipboardCheckFilled,
|
||||
},
|
||||
{
|
||||
title: "Coupons",
|
||||
url: "/dashboard/sales/coupons",
|
||||
icon: IconTag
|
||||
}
|
||||
]
|
||||
icon: IconTag,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Customers",
|
||||
url: "/dashboard/users",
|
||||
icon: IconUsers
|
||||
}
|
||||
]
|
||||
icon: IconUsers,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Other",
|
||||
@ -76,36 +64,17 @@ export const sidebarData: SidebarData = {
|
||||
{
|
||||
title: "Profile",
|
||||
url: "/dashboard/settings",
|
||||
icon: IconUserCog
|
||||
},
|
||||
{
|
||||
title: "Account",
|
||||
url: "/dashboard/settings/account",
|
||||
icon: IconTool
|
||||
icon: IconUserCog,
|
||||
},
|
||||
{
|
||||
title: "Appearance",
|
||||
url: "/dashboard/settings/appearance",
|
||||
icon: IconPalette
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: IconNotification
|
||||
},
|
||||
{
|
||||
title: "Display",
|
||||
url: "/dashboard/settings/display",
|
||||
icon: IconBrowserCheck
|
||||
icon: IconPalette,
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Help Center",
|
||||
url: "/help-center",
|
||||
icon: IconHelp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -34,9 +34,10 @@ export const Header = ({
|
||||
"flex h-16 items-center gap-3 bg-background p-4 sm:gap-4",
|
||||
fixed && "header-fixed peer/header fixed z-50 w-[inherit] rounded-md",
|
||||
offset > 10 && fixed ? "shadow" : "shadow-none",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
{children}
|
||||
|
@ -12,7 +12,7 @@ export const Main = ({ fixed, ...props }: MainProps) => {
|
||||
className={cn(
|
||||
"peer-[.header-fixed]/header:mt-16",
|
||||
"px-4 py-6",
|
||||
fixed && "fixed-main flex flex-grow flex-col overflow-hidden"
|
||||
fixed && "fixed-main flex flex-grow flex-col overflow-hidden",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -4,7 +4,7 @@ import { ChevronRight } from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
@ -15,7 +15,7 @@ import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Badge } from "../ui/badge";
|
||||
import {
|
||||
@ -24,7 +24,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
|
||||
|
||||
@ -64,7 +64,8 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, item)}
|
||||
tooltip={item.title}>
|
||||
tooltip={item.title}
|
||||
>
|
||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
@ -77,7 +78,7 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
|
||||
|
||||
const SidebarMenuCollapsible = ({
|
||||
item,
|
||||
href
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible;
|
||||
href: string;
|
||||
@ -87,7 +88,8 @@ const SidebarMenuCollapsible = ({
|
||||
<Collapsible
|
||||
asChild
|
||||
defaultOpen={checkIsActive(href, item, true)}
|
||||
className="group/collapsible">
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
@ -103,7 +105,8 @@ const SidebarMenuCollapsible = ({
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, subItem)}>
|
||||
isActive={checkIsActive(href, subItem)}
|
||||
>
|
||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
@ -121,7 +124,7 @@ const SidebarMenuCollapsible = ({
|
||||
|
||||
const SidebarMenuCollapsedDropdown = ({
|
||||
item,
|
||||
href
|
||||
href,
|
||||
}: {
|
||||
item: NavCollapsible;
|
||||
href: string;
|
||||
@ -132,7 +135,8 @@ const SidebarMenuCollapsedDropdown = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={checkIsActive(href, item)}>
|
||||
isActive={checkIsActive(href, item)}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
@ -148,7 +152,8 @@ const SidebarMenuCollapsedDropdown = ({
|
||||
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
||||
<Link
|
||||
to={sub.url}
|
||||
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}>
|
||||
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}
|
||||
>
|
||||
{sub.icon && <sub.icon />}
|
||||
<span className="max-w-52 text-wrap">{sub.title}</span>
|
||||
{sub.badge && (
|
||||
|
@ -1,112 +0,0 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavUser({
|
||||
user
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/account">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings">
|
||||
<CreditCard />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/notifications">
|
||||
<Bell />
|
||||
Notifications
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
@ -7,17 +7,17 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function TeamSwitcher({
|
||||
teams
|
||||
teams,
|
||||
}: {
|
||||
teams: {
|
||||
name: string;
|
||||
@ -35,7 +35,8 @@ export function TeamSwitcher({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
@ -52,7 +53,8 @@ export function TeamSwitcher({
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}>
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Teams
|
||||
</DropdownMenuLabel>
|
||||
@ -60,7 +62,8 @@ export function TeamSwitcher({
|
||||
<DropdownMenuItem
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
className="gap-2 p-2">
|
||||
className="gap-2 p-2"
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||
<team.logo className="size-4 shrink-0" />
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
@ -34,7 +34,8 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
|
||||
<Link
|
||||
to={href}
|
||||
className={!isActive ? "text-muted-foreground" : ""}
|
||||
disabled={disabled}>
|
||||
disabled={disabled}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
@ -46,15 +47,17 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
|
||||
<nav
|
||||
className={cn(
|
||||
"hidden items-center space-x-4 md:flex lg:space-x-6",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{links.map(({ title, href, isActive, disabled }) => (
|
||||
<Link
|
||||
key={`${title}-${href}`}
|
||||
to={href}
|
||||
disabled={disabled}
|
||||
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}>
|
||||
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
|
@ -3,13 +3,13 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface Props {
|
||||
@ -21,7 +21,7 @@ interface Props {
|
||||
export default function LongText({
|
||||
children,
|
||||
className = "",
|
||||
contentClassName = ""
|
||||
contentClassName = "",
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isOverflown, setIsOverflown] = useState(false);
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Menu } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
@ -18,9 +18,9 @@ const MainNavbar = () => {
|
||||
<li className="font-medium text-primary">
|
||||
<a href="#home">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* <li>
|
||||
<a href="#pricing">Pricing</a>
|
||||
</li>
|
||||
</li> */}
|
||||
<li>
|
||||
<a href="#faqs">FAQs</a>
|
||||
</li>
|
||||
@ -29,7 +29,6 @@ const MainNavbar = () => {
|
||||
<div className="flex items-center">
|
||||
<DynamicLoginButton />
|
||||
<div className="mr-2 flex items-center gap-2 md:hidden">
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
@ -44,9 +43,9 @@ const MainNavbar = () => {
|
||||
<DropdownMenuItem>
|
||||
<a href="#features">Features</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
{/* <DropdownMenuItem>
|
||||
<a href="#pricing">Pricing</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem>
|
||||
<a href="#faqs">FAQs</a>
|
||||
</DropdownMenuItem>
|
||||
|
@ -5,7 +5,7 @@ import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
@ -15,11 +15,11 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavMain({
|
||||
items
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
@ -41,7 +41,8 @@ export function NavMain({
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible">
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Forward,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
type LucideIcon
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
@ -13,7 +13,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
@ -22,11 +22,11 @@ import {
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavProjects({
|
||||
projects
|
||||
projects,
|
||||
}: {
|
||||
projects: {
|
||||
name: string;
|
||||
@ -58,7 +58,8 @@ export function NavProjects({
|
||||
<DropdownMenuContent
|
||||
className="w-48 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}>
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@ -17,17 +17,17 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavUser({
|
||||
user
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
@ -44,7 +44,8 @@ export function NavUser({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
@ -60,7 +61,8 @@ export function NavUser({
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}>
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
|
@ -26,12 +26,13 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
variant="ghost"
|
||||
disabled={disabled}
|
||||
className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground"
|
||||
onClick={() => setShowPassword((prev) => !prev)}>
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
>
|
||||
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
PasswordInput.displayName = "PasswordInput";
|
||||
|
||||
|
@ -106,7 +106,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly
|
||||
readOnly,
|
||||
});
|
||||
|
||||
/* call onChange func if pinValue changes */
|
||||
@ -171,7 +171,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
||||
} else {
|
||||
refMap?.delete(pinIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
skipRef.current = skipRef.current + 1;
|
||||
@ -220,7 +220,7 @@ const PinInputField = <T extends React.ElementType = "input">({
|
||||
const isInsidePinInput = React.useContext(PinInputContext);
|
||||
if (!isInsidePinInput) {
|
||||
throw new Error(
|
||||
`PinInputField must be used within ${PinInput.displayName}.`
|
||||
`PinInputField must be used within ${PinInput.displayName}.`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -254,7 +254,7 @@ const usePinInput = ({
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly
|
||||
readOnly,
|
||||
}: UsePinInputProps) => {
|
||||
const pinInputs = React.useMemo(
|
||||
() =>
|
||||
@ -263,9 +263,9 @@ const usePinInput = ({
|
||||
? defaultValue.charAt(index)
|
||||
: value
|
||||
? value.charAt(index)
|
||||
: ""
|
||||
: "",
|
||||
),
|
||||
[defaultValue, length, value]
|
||||
[defaultValue, length, value],
|
||||
);
|
||||
|
||||
const [pins, setPins] = React.useState(pinInputs);
|
||||
@ -305,7 +305,7 @@ const usePinInput = ({
|
||||
|
||||
function handleFocus(
|
||||
event: React.FocusEvent<HTMLInputElement>,
|
||||
index: number
|
||||
index: number,
|
||||
) {
|
||||
event.target.select();
|
||||
focusInput(index);
|
||||
@ -332,7 +332,7 @@ const usePinInput = ({
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -384,7 +384,7 @@ const usePinInput = ({
|
||||
|
||||
function handleKeyDown(
|
||||
event: React.KeyboardEvent<HTMLInputElement>,
|
||||
index: number
|
||||
index: number,
|
||||
) {
|
||||
const { ctrlKey, key, shiftKey, metaKey } = event;
|
||||
|
||||
@ -429,7 +429,7 @@ const usePinInput = ({
|
||||
handleBlur,
|
||||
handleChange,
|
||||
handlePaste,
|
||||
handleKeyDown
|
||||
handleKeyDown,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
|
||||
@ -45,12 +45,6 @@ export function ProfileDropdown() {
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/dashboard/settings">
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/dashboard/settings">
|
||||
Settings
|
||||
|
@ -16,9 +16,10 @@ export function Search({ className = "", placeholder = "Search" }: Props) {
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
onClick={() => setOpen(true)}>
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<IconSearch
|
||||
aria-hidden="true"
|
||||
className="absolute left-1.5 top-1/2 -translate-y-1/2"
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface SelectDropdownProps {
|
||||
@ -28,7 +28,7 @@ export function SelectDropdown({
|
||||
placeholder,
|
||||
disabled,
|
||||
className = "",
|
||||
isControlled = false
|
||||
isControlled = false,
|
||||
}: SelectDropdownProps) {
|
||||
const defaultState = isControlled
|
||||
? { value: defaultValue, onValueChange }
|
||||
|
@ -2,7 +2,8 @@ const SkipToMain = () => {
|
||||
return (
|
||||
<a
|
||||
className={`fixed left-44 z-[999] -translate-y-52 whitespace-nowrap bg-primary px-4 py-2 text-sm font-medium text-primary-foreground opacity-95 shadow transition hover:bg-primary/90 focus:translate-y-3 focus:transform focus-visible:ring-1 focus-visible:ring-ring`}
|
||||
href="#content">
|
||||
href="#content"
|
||||
>
|
||||
Skip to Main
|
||||
</a>
|
||||
);
|
||||
|
@ -10,17 +10,17 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function TeamSwitcher({
|
||||
teams
|
||||
teams,
|
||||
}: {
|
||||
teams: {
|
||||
name: string;
|
||||
@ -38,7 +38,8 @@ export function TeamSwitcher({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
@ -55,7 +56,8 @@ export function TeamSwitcher({
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}>
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Teams
|
||||
</DropdownMenuLabel>
|
||||
@ -63,7 +65,8 @@ export function TeamSwitcher({
|
||||
<DropdownMenuItem
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
className="gap-2 p-2">
|
||||
className="gap-2 p-2"
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||
<team.logo className="size-4 shrink-0" />
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeSwitch() {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef<
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
@ -27,7 +27,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef<
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef<
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -49,7 +49,7 @@ const AlertDialogHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -63,7 +63,7 @@ const AlertDialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -116,7 +116,7 @@ const AlertDialogCancel = React.forwardRef<
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -134,5 +134,5 @@ export {
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel
|
||||
AlertDialogCancel,
|
||||
};
|
||||
|
@ -9,13 +9,13 @@ const alertVariants = cva(
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
|
||||
}
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
|
@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -13,13 +13,13 @@ const badgeVariants = cva(
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground"
|
||||
}
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
|
@ -20,7 +20,7 @@ const BreadcrumbList = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -81,7 +81,8 @@ const BreadcrumbSeparator = ({
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
@ -95,7 +96,8 @@ const BreadcrumbEllipsis = ({
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
@ -109,5 +111,5 @@ export {
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
|
@ -17,20 +17,20 @@ const buttonVariants = cva(
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10"
|
||||
}
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
@ -49,7 +49,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||
)
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar };
|
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -78,5 +78,5 @@ export {
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent
|
||||
CardContent,
|
||||
};
|
||||
|
@ -11,11 +11,13 @@ const Checkbox = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}>
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
@ -19,7 +19,7 @@ const Command = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -54,7 +54,7 @@ const CommandInput = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -97,7 +97,7 @@ const CommandGroup = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -125,7 +125,7 @@ const CommandItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -141,7 +141,7 @@ const CommandShortcut = ({
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -158,5 +158,5 @@ export {
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator
|
||||
CommandSeparator,
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -38,9 +38,10 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
@ -58,7 +59,7 @@ const DialogHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -72,7 +73,7 @@ const DialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -87,7 +88,7 @@ const DialogTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -116,5 +117,5 @@ export {
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
DialogDescription,
|
||||
};
|
||||
|
@ -26,9 +26,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
@ -44,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -63,7 +64,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -82,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -97,10 +98,11 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
@ -120,9 +122,10 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
@ -144,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -191,5 +194,5 @@ export {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
@ -16,18 +16,18 @@ const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
@ -57,7 +57,7 @@ const useFormField = () => {
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
@ -66,7 +66,7 @@ type FormItemContextValue = {
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
@ -156,7 +156,8 @@ const FormMessage = React.forwardRef<
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
@ -171,5 +172,5 @@ export {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField
|
||||
FormField,
|
||||
};
|
||||
|
@ -8,13 +8,13 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -53,7 +53,7 @@ const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
PhoneInput.displayName = "PhoneInput";
|
||||
|
||||
@ -82,7 +82,7 @@ const CountrySelect = ({
|
||||
disabled,
|
||||
value: selectedCountry,
|
||||
options: countryList,
|
||||
onChange
|
||||
onChange,
|
||||
}: CountrySelectProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
@ -91,7 +91,8 @@ const CountrySelect = ({
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
|
||||
disabled={disabled}>
|
||||
disabled={disabled}
|
||||
>
|
||||
<FlagComponent
|
||||
country={selectedCountry}
|
||||
countryName={selectedCountry}
|
||||
@ -99,7 +100,7 @@ const CountrySelect = ({
|
||||
<ChevronsUpDown
|
||||
className={cn(
|
||||
"-mr-2 size-4 opacity-50",
|
||||
disabled ? "hidden" : "opacity-100"
|
||||
disabled ? "hidden" : "opacity-100",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
@ -120,7 +121,7 @@ const CountrySelect = ({
|
||||
selectedCountry={selectedCountry}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : null
|
||||
) : null,
|
||||
)}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
@ -140,7 +141,7 @@ const CountrySelectOption = ({
|
||||
country,
|
||||
countryName,
|
||||
selectedCountry,
|
||||
onChange
|
||||
onChange,
|
||||
}: CountrySelectOptionProps) => {
|
||||
return (
|
||||
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
|
||||
|
@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -26,9 +26,10 @@ const RadioGroupItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
|
@ -14,12 +14,14 @@ const ScrollArea = React.forwardRef<
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn(
|
||||
"h-full w-full rounded-[inherit]",
|
||||
orientation === "horizontal" && "!overflow-x-auto"
|
||||
)}>
|
||||
orientation === "horizontal" && "!overflow-x-auto",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar orientation={orientation} />
|
||||
@ -41,9 +43,10 @@ const ScrollBar = React.forwardRef<
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
|
@ -17,9 +17,10 @@ const SelectTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
@ -36,9 +37,10 @@ const SelectScrollUpButton = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
@ -52,9 +54,10 @@ const SelectScrollDownButton = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
@ -72,17 +75,19 @@ const SelectContent = React.forwardRef<
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}>
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
@ -111,9 +116,10 @@ const SelectItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
@ -146,5 +152,5 @@ export {
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ const Separator = React.forwardRef<
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
@ -17,11 +17,11 @@ const Separator = React.forwardRef<
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
|
@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
@ -39,13 +39,13 @@ const sheetVariants = cva(
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
|
||||
}
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right"
|
||||
}
|
||||
}
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
@ -61,7 +61,8 @@ const SheetContent = React.forwardRef<
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
@ -79,7 +80,7 @@ const SheetHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -93,7 +94,7 @@ const SheetFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -134,5 +135,5 @@ export {
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription
|
||||
SheetDescription,
|
||||
};
|
||||
|
@ -12,14 +12,14 @@ import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetTitle
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||
@ -68,7 +68,7 @@ const SidebarProvider = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
@ -89,7 +89,7 @@ const SidebarProvider = React.forwardRef<
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open]
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
@ -127,9 +127,17 @@ const SidebarProvider = React.forwardRef<
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
[
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -140,21 +148,22 @@ const SidebarProvider = React.forwardRef<
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
@ -175,7 +184,7 @@ const Sidebar = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
@ -184,10 +193,11 @@ const Sidebar = React.forwardRef<
|
||||
<div
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -205,10 +215,11 @@ const Sidebar = React.forwardRef<
|
||||
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}>
|
||||
side={side}
|
||||
>
|
||||
<VisuallyHidden asChild>
|
||||
<SheetDescription />
|
||||
</VisuallyHidden>
|
||||
@ -225,7 +236,8 @@ const Sidebar = React.forwardRef<
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}>
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
@ -234,7 +246,7 @@ const Sidebar = React.forwardRef<
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
@ -247,18 +259,20 @@ const Sidebar = React.forwardRef<
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow">
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
@ -279,7 +293,8 @@ const SidebarTrigger = React.forwardRef<
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
@ -308,7 +323,7 @@ const SidebarRail = React.forwardRef<
|
||||
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -326,7 +341,7 @@ const SidebarInset = React.forwardRef<
|
||||
className={cn(
|
||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -344,7 +359,7 @@ const SidebarInput = React.forwardRef<
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"focus-visible:ring-sidebar-ring h-8 w-full bg-background shadow-none focus-visible:ring-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -407,7 +422,7 @@ const SidebarContent = React.forwardRef<
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -443,7 +458,7 @@ const SidebarGroupLabel = React.forwardRef<
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -466,7 +481,7 @@ const SidebarGroupAction = React.forwardRef<
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -520,19 +535,19 @@ const sidebarMenuButtonVariants = cva(
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0"
|
||||
}
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
@ -553,7 +568,7 @@ const SidebarMenuButton = React.forwardRef<
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
@ -575,7 +590,7 @@ const SidebarMenuButton = React.forwardRef<
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
@ -590,7 +605,7 @@ const SidebarMenuButton = React.forwardRef<
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
@ -617,7 +632,7 @@ const SidebarMenuAction = React.forwardRef<
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -639,7 +654,7 @@ const SidebarMenuBadge = React.forwardRef<
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -662,7 +677,8 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
@ -674,7 +690,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
@ -693,7 +709,7 @@ const SidebarMenuSub = React.forwardRef<
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -728,7 +744,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -760,5 +776,5 @@ export {
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar
|
||||
useSidebar,
|
||||
};
|
||||
|
@ -9,13 +9,14 @@ const Switch = React.forwardRef<
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}>
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -88,7 +88,7 @@ const TableCell = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -115,5 +115,5 @@ export {
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption
|
||||
TableCaption,
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
|
@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -28,13 +28,13 @@ const toastVariants = cva(
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground"
|
||||
}
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
@ -60,7 +60,7 @@ const ToastAction = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -75,10 +75,11 @@ const ToastClose = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
@ -121,5 +122,5 @@ export {
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction
|
||||
ToastAction,
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
|
@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -1,2 +1,2 @@
|
||||
export const AppName = "SwagShop"
|
||||
export const AdminAppName = "SwagShop Admin"
|
||||
export const AppName = "SwagShop";
|
||||
export const AdminAppName = "SwagShop Admin";
|
||||
|
@ -11,7 +11,7 @@ interface FontContextType {
|
||||
const FontContext = createContext<FontContextType | undefined>(undefined);
|
||||
|
||||
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children
|
||||
children,
|
||||
}) => {
|
||||
const [font, _setFont] = useState<Font>(() => {
|
||||
const savedFont = localStorage.getItem("font");
|
||||
|
@ -15,7 +15,7 @@ type ThemeProviderState = {
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
@ -27,7 +27,7 @@ export function ThemeProvider({
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, _setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,7 +61,7 @@ export function ThemeProvider({
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme
|
||||
setTheme,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -4,17 +4,11 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { handleServerError } from "@/utils/handle-server-error";
|
||||
import { ApiError } from "@/errors/api-error";
|
||||
import {
|
||||
DashboardService,
|
||||
LoginService,
|
||||
ShopLoginAccessTokenData,
|
||||
UserPublic,
|
||||
UserRegister,
|
||||
UserService,
|
||||
UserUpdate
|
||||
} from "@/client";
|
||||
import { ShopLoginAccessTokenData, UserPublic } from "@/client";
|
||||
import { toast } from "./useToast";
|
||||
|
||||
import { authAPI } from "@/api/api";
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return localStorage.getItem("access_token") !== null;
|
||||
};
|
||||
@ -24,27 +18,22 @@ const useAuth = () => {
|
||||
const [loggedIn, setLoggedIn] = useState(isLoggedIn());
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: user } = useQuery<UserPublic | null, Error>({
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: DashboardService.userGetUser,
|
||||
enabled: loggedIn
|
||||
queryFn: authAPI.getCurrentUser,
|
||||
enabled: loggedIn,
|
||||
});
|
||||
|
||||
const signUpMutation = useMutation({
|
||||
mutationFn: (data: UserRegister) =>
|
||||
DashboardService.userRegister({ requestBody: data }),
|
||||
mutationFn: authAPI.registerUser,
|
||||
onSuccess: () => navigate({ to: "/sign-in" }),
|
||||
onError: (err: ApiError) => handleServerError(err),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] })
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
|
||||
});
|
||||
|
||||
const login = async (data: ShopLoginAccessTokenData) => {
|
||||
const response = await LoginService.dashboardLoginAccessToken({
|
||||
formData: {
|
||||
username: data.formData.username,
|
||||
password: data.formData.password
|
||||
}
|
||||
});
|
||||
const response = await authAPI.loginUser(data);
|
||||
localStorage.setItem("access_token", response.access_token);
|
||||
setLoggedIn(true);
|
||||
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||
@ -53,7 +42,7 @@ const useAuth = () => {
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: login,
|
||||
onSuccess: () => navigate({ to: "/" }),
|
||||
onError: (err: ApiError) => handleServerError(err)
|
||||
onError: (err: ApiError) => handleServerError(err),
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
@ -64,15 +53,26 @@ const useAuth = () => {
|
||||
};
|
||||
|
||||
const updateAccountMutation = useMutation({
|
||||
mutationFn: (data: UserUpdate) =>
|
||||
UserService.userUpdateUser({ requestBody: data }),
|
||||
mutationFn: authAPI.updateUser,
|
||||
onSuccess: () => {
|
||||
toast({ title: "Account updated successfully" });
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||
},
|
||||
onError: (err: ApiError) => handleServerError(err)
|
||||
onError: (err: ApiError) => handleServerError(err),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Checking whether the token is valid");
|
||||
const userLoggedInAndNull = loggedIn && user === null;
|
||||
const tokenExistsAndNull =
|
||||
Boolean(localStorage.getItem("access_token")) && user === null;
|
||||
|
||||
if (userLoggedInAndNull || tokenExistsAndNull) {
|
||||
console.warn("User data is null while logged in, logging out.");
|
||||
logout();
|
||||
}
|
||||
}, [loggedIn, user]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === "access_token") {
|
||||
@ -97,7 +97,7 @@ const useAuth = () => {
|
||||
logout,
|
||||
user,
|
||||
error,
|
||||
resetError: () => setError(null)
|
||||
resetError: () => setError(null),
|
||||
};
|
||||
};
|
||||
|
||||
|
39
frontend/src/hooks/useCoupon.ts
Normal file
39
frontend/src/hooks/useCoupon.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Coupon } from "@/api/mock/models";
|
||||
import { couponAPI } from "@/api/api";
|
||||
|
||||
export function useCoupon(couponId?: number) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const coupon = useQuery<Coupon | null>({
|
||||
queryKey: ["coupon", couponId],
|
||||
queryFn: () => couponAPI.getCouponById(couponId!),
|
||||
enabled: !!couponId,
|
||||
});
|
||||
|
||||
const createCoupon = useMutation({
|
||||
mutationFn: (data: Omit<Coupon, "id">) => couponAPI.createCoupon(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["coupons"] });
|
||||
},
|
||||
});
|
||||
|
||||
const updateCoupon = useMutation({
|
||||
mutationFn: (data: Partial<Omit<Coupon, "id">>) =>
|
||||
couponAPI.updateCoupon(couponId!, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["coupons"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["coupon", couponId] });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCoupon = useMutation({
|
||||
mutationFn: (id: number) => couponAPI.deleteCoupon(id),
|
||||
onSuccess: (_, id) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["coupons"] });
|
||||
queryClient.removeQueries({ queryKey: ["coupon", id] });
|
||||
},
|
||||
});
|
||||
|
||||
return { coupon, createCoupon, updateCoupon, deleteCoupon };
|
||||
}
|
10
frontend/src/hooks/useCoupons.ts
Normal file
10
frontend/src/hooks/useCoupons.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { couponAPI } from "@/api/api";
|
||||
import { Coupon } from "@/api/mock/models";
|
||||
|
||||
export function useCoupons() {
|
||||
return useQuery<Coupon[]>({
|
||||
queryKey: ["coupons"],
|
||||
queryFn: couponAPI.getAllCoupons,
|
||||
});
|
||||
}
|
@ -7,7 +7,7 @@ import { useState } from "react";
|
||||
* @example const [open, setOpen] = useDialogState<"approve" | "reject">()
|
||||
*/
|
||||
export default function useDialogState<T extends string | boolean>(
|
||||
initialState: T | null = null
|
||||
initialState: T | null = null,
|
||||
) {
|
||||
const [open, _setOpen] = useState<T | null>(initialState);
|
||||
|
||||
|
@ -4,7 +4,7 @@ const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
31
frontend/src/hooks/useProduct.ts
Normal file
31
frontend/src/hooks/useProduct.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ProductCreate, ProductWithDetails } from "@/api/mock/models";
|
||||
import { productsAPI } from "@/api/api";
|
||||
|
||||
export function useProduct(productId?: number) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const product = useQuery<ProductWithDetails | null>({
|
||||
queryKey: ["product", productId],
|
||||
queryFn: () => productsAPI.getProductById(productId!),
|
||||
enabled: !!productId,
|
||||
});
|
||||
|
||||
const createProduct = useMutation({
|
||||
mutationFn: (data: ProductCreate) => productsAPI.createProduct(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] }),
|
||||
});
|
||||
|
||||
const updateProduct = useMutation({
|
||||
mutationFn: (data: Partial<ProductCreate>) =>
|
||||
productsAPI.updateProduct(productId!, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] }),
|
||||
});
|
||||
|
||||
const deleteProduct = useMutation({
|
||||
mutationFn: () => productsAPI.deleteProduct(productId!),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] }),
|
||||
});
|
||||
|
||||
return { product, createProduct, updateProduct, deleteProduct };
|
||||
}
|
12
frontend/src/hooks/useProducts.ts
Normal file
12
frontend/src/hooks/useProducts.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { productsAPI } from "@/api/api";
|
||||
import { ProductWithDetails } from "@/api/mock/models";
|
||||
|
||||
export function useProducts() {
|
||||
const query = useQuery<ProductWithDetails[]>({
|
||||
queryKey: ["products"],
|
||||
queryFn: productsAPI.getProductsForShop,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user