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
|
# Swag Shop
|
||||||
Simple API (still WIP)
|
|
||||||
|
|
||||||
## Requires:
|
An e-commerence multitenant shop app.
|
||||||
1. Redis
|
|
||||||
Simple redis installation, no further configuration needed
|
|
||||||
|
|
||||||
2. MariaDB (or MySQL)
|
## Core idea
|
||||||
Make sure a proper database is set up with `shop.sql`
|
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.12
|
||||||
FROM python:3.13
|
|
||||||
|
|
||||||
RUN pip install poetry
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
# Copy dependency files first to leverage caching
|
# Install uv
|
||||||
COPY pyproject.toml poetry.lock /app/
|
# 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
|
COPY ./app /app/app
|
||||||
|
|
||||||
# Ensure dependencies are installed correctly
|
# Sync the project
|
||||||
RUN poetry install --no-interaction --no-ansi --without dev
|
# 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
|
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Command to run the app
|
|
||||||
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
|
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:
|
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:
|
db:
|
||||||
image: postgres:12
|
image: postgres:16
|
||||||
restart: no
|
container_name: swagshop-postgres
|
||||||
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: swagshop
|
||||||
volumes:
|
volumes:
|
||||||
- app-db-data:/var/lib/postgresql/data/pgdata
|
- pgdata:/var/lib/postgresql/data
|
||||||
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
|
|
||||||
networks:
|
networks:
|
||||||
- default
|
- internal
|
||||||
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
|
|
||||||
|
|
||||||
volumes:
|
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",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format:check": "prettier --check .",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"knip": "knip",
|
"knip": "knip",
|
||||||
"generate-client": "openapi-ts"
|
"generate-client": "openapi-ts",
|
||||||
|
"format:write": "biome format ./src/ --write",
|
||||||
|
"format:check": "biome check ./src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
@ -29,14 +29,14 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@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-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||||
"@tabler/icons-react": "^3.24.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-router": "^1.86.1",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
@ -45,7 +45,9 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"dexie": "^4.0.11",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^9.6.0",
|
"react-day-picker": "^9.6.0",
|
||||||
@ -55,10 +57,12 @@
|
|||||||
"recharts": "^2.14.1",
|
"recharts": "^2.14.1",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@eslint/js": "^9.16.0",
|
"@eslint/js": "^9.16.0",
|
||||||
"@faker-js/faker": "^9.3.0",
|
"@faker-js/faker": "^9.3.0",
|
||||||
"@hey-api/client-axios": "^0.6.2",
|
"@hey-api/client-axios": "^0.6.2",
|
||||||
@ -85,6 +89,7 @@
|
|||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.22.0",
|
"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 { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
import type { ApiResult } from './ApiResult';
|
import type { ApiResult } from "./ApiResult";
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
public readonly url: string;
|
public readonly url: string;
|
||||||
@ -8,14 +8,18 @@ export class ApiError extends Error {
|
|||||||
public readonly body: unknown;
|
public readonly body: unknown;
|
||||||
public readonly request: ApiRequestOptions;
|
public readonly request: ApiRequestOptions;
|
||||||
|
|
||||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
constructor(
|
||||||
|
request: ApiRequestOptions,
|
||||||
|
response: ApiResult,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
||||||
this.name = 'ApiError';
|
this.name = "ApiError";
|
||||||
this.url = response.url;
|
this.url = response.url;
|
||||||
this.status = response.status;
|
this.status = response.status;
|
||||||
this.statusText = response.statusText;
|
this.statusText = response.statusText;
|
||||||
this.body = response.body;
|
this.body = response.body;
|
||||||
this.request = request;
|
this.request = request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,16 @@ export type ApiRequestOptions<T = unknown> = {
|
|||||||
readonly headers?: Record<string, unknown>;
|
readonly headers?: Record<string, unknown>;
|
||||||
readonly mediaType?: string;
|
readonly mediaType?: string;
|
||||||
readonly method:
|
readonly method:
|
||||||
| 'DELETE'
|
| "DELETE"
|
||||||
| 'GET'
|
| "GET"
|
||||||
| 'HEAD'
|
| "HEAD"
|
||||||
| 'OPTIONS'
|
| "OPTIONS"
|
||||||
| 'PATCH'
|
| "PATCH"
|
||||||
| 'POST'
|
| "POST"
|
||||||
| 'PUT';
|
| "PUT";
|
||||||
readonly path?: Record<string, unknown>;
|
readonly path?: Record<string, unknown>;
|
||||||
readonly query?: Record<string, unknown>;
|
readonly query?: Record<string, unknown>;
|
||||||
readonly responseHeader?: string;
|
readonly responseHeader?: string;
|
||||||
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
};
|
};
|
||||||
|
@ -4,4 +4,4 @@ export type ApiResult<TData = any> = {
|
|||||||
readonly status: number;
|
readonly status: number;
|
||||||
readonly statusText: string;
|
readonly statusText: string;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export class CancelError extends Error {
|
export class CancelError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'CancelError';
|
this.name = "CancelError";
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isCancelled(): boolean {
|
public get isCancelled(): boolean {
|
||||||
@ -30,8 +30,8 @@ export class CancelablePromise<T> implements Promise<T> {
|
|||||||
executor: (
|
executor: (
|
||||||
resolve: (value: T | PromiseLike<T>) => void,
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
reject: (reason?: unknown) => void,
|
reject: (reason?: unknown) => void,
|
||||||
onCancel: OnCancel
|
onCancel: OnCancel,
|
||||||
) => void
|
) => void,
|
||||||
) {
|
) {
|
||||||
this._isResolved = false;
|
this._isResolved = false;
|
||||||
this._isRejected = false;
|
this._isRejected = false;
|
||||||
@ -64,15 +64,15 @@ export class CancelablePromise<T> implements Promise<T> {
|
|||||||
this.cancelHandlers.push(cancelHandler);
|
this.cancelHandlers.push(cancelHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperty(onCancel, 'isResolved', {
|
Object.defineProperty(onCancel, "isResolved", {
|
||||||
get: (): boolean => this._isResolved,
|
get: (): boolean => this._isResolved,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(onCancel, 'isRejected', {
|
Object.defineProperty(onCancel, "isRejected", {
|
||||||
get: (): boolean => this._isRejected,
|
get: (): boolean => this._isRejected,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(onCancel, 'isCancelled', {
|
Object.defineProperty(onCancel, "isCancelled", {
|
||||||
get: (): boolean => this._isCancelled,
|
get: (): boolean => this._isCancelled,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,13 +86,13 @@ export class CancelablePromise<T> implements Promise<T> {
|
|||||||
|
|
||||||
public then<TResult1 = T, TResult2 = never>(
|
public then<TResult1 = T, TResult2 = never>(
|
||||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
|
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||||
): Promise<TResult1 | TResult2> {
|
): Promise<TResult1 | TResult2> {
|
||||||
return this.promise.then(onFulfilled, onRejected);
|
return this.promise.then(onFulfilled, onRejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
public catch<TResult = never>(
|
public catch<TResult = never>(
|
||||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
|
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
||||||
): Promise<T | TResult> {
|
): Promise<T | TResult> {
|
||||||
return this.promise.catch(onRejected);
|
return this.promise.catch(onRejected);
|
||||||
}
|
}
|
||||||
@ -112,15 +112,15 @@ export class CancelablePromise<T> implements Promise<T> {
|
|||||||
cancelHandler();
|
cancelHandler();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Cancellation threw an error', error);
|
console.warn("Cancellation threw an error", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cancelHandlers.length = 0;
|
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 {
|
public get isCancelled(): boolean {
|
||||||
return this._isCancelled;
|
return this._isCancelled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
|
|
||||||
type Headers = Record<string, string>;
|
type Headers = Record<string, string>;
|
||||||
type Middleware<T> = (value: T) => T | Promise<T>;
|
type Middleware<T> = (value: T) => T | Promise<T>;
|
||||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||||
|
|
||||||
export class Interceptors<T> {
|
export class Interceptors<T> {
|
||||||
_fns: Middleware<T>[];
|
_fns: Middleware<T>[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._fns = [];
|
this._fns = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
eject(fn: Middleware<T>): void {
|
eject(fn: Middleware<T>): void {
|
||||||
const index = this._fns.indexOf(fn);
|
const index = this._fns.indexOf(fn);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
|
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use(fn: Middleware<T>): void {
|
use(fn: Middleware<T>): void {
|
||||||
this._fns = [...this._fns, fn];
|
this._fns = [...this._fns, fn];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenAPIConfig = {
|
export type OpenAPIConfig = {
|
||||||
BASE: string;
|
BASE: string;
|
||||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||||
PASSWORD?: string | Resolver<string> | undefined;
|
PASSWORD?: string | Resolver<string> | undefined;
|
||||||
@ -41,17 +41,17 @@ export type OpenAPIConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: '',
|
BASE: "",
|
||||||
CREDENTIALS: 'include',
|
CREDENTIALS: "include",
|
||||||
ENCODE_PATH: undefined,
|
ENCODE_PATH: undefined,
|
||||||
HEADERS: undefined,
|
HEADERS: undefined,
|
||||||
PASSWORD: undefined,
|
PASSWORD: undefined,
|
||||||
TOKEN: undefined,
|
TOKEN: undefined,
|
||||||
USERNAME: undefined,
|
USERNAME: undefined,
|
||||||
VERSION: '0.0.1',
|
VERSION: "0.0.1",
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: new Interceptors(),
|
request: new Interceptors(),
|
||||||
response: new Interceptors(),
|
response: new Interceptors(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosInstance,
|
||||||
|
} from "axios";
|
||||||
|
|
||||||
import { ApiError } from './ApiError';
|
import { ApiError } from "./ApiError";
|
||||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
import type { ApiResult } from './ApiResult';
|
import type { ApiResult } from "./ApiResult";
|
||||||
import { CancelablePromise } from './CancelablePromise';
|
import { CancelablePromise } from "./CancelablePromise";
|
||||||
import type { OnCancel } from './CancelablePromise';
|
import type { OnCancel } from "./CancelablePromise";
|
||||||
import type { OpenAPIConfig } from './OpenAPI';
|
import type { OpenAPIConfig } from "./OpenAPI";
|
||||||
|
|
||||||
export const isString = (value: unknown): value is string => {
|
export const isString = (value: unknown): value is string => {
|
||||||
return typeof value === 'string';
|
return typeof value === "string";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isStringWithValue = (value: unknown): value is 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 => {
|
export const isBlob = (value: any): value is Blob => {
|
||||||
@ -33,7 +38,7 @@ export const base64 = (str: string): string => {
|
|||||||
return btoa(str);
|
return btoa(str);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// @ts-ignore
|
// @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) {
|
if (value instanceof Date) {
|
||||||
append(key, value.toISOString());
|
append(key, value.toISOString());
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
value.forEach(v => encodePair(key, v));
|
value.forEach((v) => encodePair(key, v));
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === "object") {
|
||||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
||||||
} else {
|
} else {
|
||||||
append(key, value);
|
append(key, value);
|
||||||
@ -62,14 +67,14 @@ export const getQueryString = (params: Record<string, unknown>): string => {
|
|||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
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 getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||||
const encoder = config.ENCODE_PATH || encodeURI;
|
const encoder = config.ENCODE_PATH || encodeURI;
|
||||||
|
|
||||||
const path = options.url
|
const path = options.url
|
||||||
.replace('{api-version}', config.VERSION)
|
.replace("{api-version}", config.VERSION)
|
||||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||||
if (options.path?.hasOwnProperty(group)) {
|
if (options.path?.hasOwnProperty(group)) {
|
||||||
return encoder(String(options.path[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;
|
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) {
|
if (options.formData) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
@ -97,7 +104,7 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
|
|||||||
.filter(([, value]) => value !== undefined && value !== null)
|
.filter(([, value]) => value !== undefined && value !== null)
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(v => process(key, v));
|
value.forEach((v) => process(key, v));
|
||||||
} else {
|
} else {
|
||||||
process(key, value);
|
process(key, value);
|
||||||
}
|
}
|
||||||
@ -110,14 +117,20 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
|
|||||||
|
|
||||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||||
|
|
||||||
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
export const resolve = async <T>(
|
||||||
if (typeof resolver === 'function') {
|
options: ApiRequestOptions<T>,
|
||||||
|
resolver?: T | Resolver<T>,
|
||||||
|
): Promise<T | undefined> => {
|
||||||
|
if (typeof resolver === "function") {
|
||||||
return (resolver as Resolver<T>)(options);
|
return (resolver as Resolver<T>)(options);
|
||||||
}
|
}
|
||||||
return resolver;
|
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([
|
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
resolve(options, config.TOKEN),
|
resolve(options, config.TOKEN),
|
||||||
@ -130,38 +143,41 @@ export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOp
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const headers = Object.entries({
|
const headers = Object.entries({
|
||||||
Accept: 'application/json',
|
Accept: "application/json",
|
||||||
...additionalHeaders,
|
...additionalHeaders,
|
||||||
...options.headers,
|
...options.headers,
|
||||||
})
|
})
|
||||||
.filter(([, value]) => value !== undefined && value !== null)
|
.filter(([, value]) => value !== undefined && value !== null)
|
||||||
.reduce((headers, [key, value]) => ({
|
.reduce(
|
||||||
...headers,
|
(headers, [key, value]) => ({
|
||||||
[key]: String(value),
|
...headers,
|
||||||
}), {} as Record<string, string>);
|
[key]: String(value),
|
||||||
|
}),
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
if (isStringWithValue(token)) {
|
if (isStringWithValue(token)) {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||||
const credentials = base64(`${username}:${password}`);
|
const credentials = base64(`${username}:${password}`);
|
||||||
headers['Authorization'] = `Basic ${credentials}`;
|
headers["Authorization"] = `Basic ${credentials}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.body !== undefined) {
|
if (options.body !== undefined) {
|
||||||
if (options.mediaType) {
|
if (options.mediaType) {
|
||||||
headers['Content-Type'] = options.mediaType;
|
headers["Content-Type"] = options.mediaType;
|
||||||
} else if (isBlob(options.body)) {
|
} 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)) {
|
} else if (isString(options.body)) {
|
||||||
headers['Content-Type'] = 'text/plain';
|
headers["Content-Type"] = "text/plain";
|
||||||
} else if (!isFormData(options.body)) {
|
} else if (!isFormData(options.body)) {
|
||||||
headers['Content-Type'] = 'application/json';
|
headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
} else if (options.formData !== undefined) {
|
} else if (options.formData !== undefined) {
|
||||||
if (options.mediaType) {
|
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,
|
formData: FormData | undefined,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
onCancel: OnCancel,
|
onCancel: OnCancel,
|
||||||
axiosClient: AxiosInstance
|
axiosClient: AxiosInstance,
|
||||||
): Promise<AxiosResponse<T>> => {
|
): Promise<AxiosResponse<T>> => {
|
||||||
const controller = new AbortController();
|
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) {
|
if (responseHeader) {
|
||||||
const content = response.headers[responseHeader];
|
const content = response.headers[responseHeader];
|
||||||
if (isString(content)) {
|
if (isString(content)) {
|
||||||
@ -230,50 +249,53 @@ export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
export const catchErrorCodes = (
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
result: ApiResult,
|
||||||
|
): void => {
|
||||||
const errors: Record<number, string> = {
|
const errors: Record<number, string> = {
|
||||||
400: 'Bad Request',
|
400: "Bad Request",
|
||||||
401: 'Unauthorized',
|
401: "Unauthorized",
|
||||||
402: 'Payment Required',
|
402: "Payment Required",
|
||||||
403: 'Forbidden',
|
403: "Forbidden",
|
||||||
404: 'Not Found',
|
404: "Not Found",
|
||||||
405: 'Method Not Allowed',
|
405: "Method Not Allowed",
|
||||||
406: 'Not Acceptable',
|
406: "Not Acceptable",
|
||||||
407: 'Proxy Authentication Required',
|
407: "Proxy Authentication Required",
|
||||||
408: 'Request Timeout',
|
408: "Request Timeout",
|
||||||
409: 'Conflict',
|
409: "Conflict",
|
||||||
410: 'Gone',
|
410: "Gone",
|
||||||
411: 'Length Required',
|
411: "Length Required",
|
||||||
412: 'Precondition Failed',
|
412: "Precondition Failed",
|
||||||
413: 'Payload Too Large',
|
413: "Payload Too Large",
|
||||||
414: 'URI Too Long',
|
414: "URI Too Long",
|
||||||
415: 'Unsupported Media Type',
|
415: "Unsupported Media Type",
|
||||||
416: 'Range Not Satisfiable',
|
416: "Range Not Satisfiable",
|
||||||
417: 'Expectation Failed',
|
417: "Expectation Failed",
|
||||||
418: 'Im a teapot',
|
418: "Im a teapot",
|
||||||
421: 'Misdirected Request',
|
421: "Misdirected Request",
|
||||||
422: 'Unprocessable Content',
|
422: "Unprocessable Content",
|
||||||
423: 'Locked',
|
423: "Locked",
|
||||||
424: 'Failed Dependency',
|
424: "Failed Dependency",
|
||||||
425: 'Too Early',
|
425: "Too Early",
|
||||||
426: 'Upgrade Required',
|
426: "Upgrade Required",
|
||||||
428: 'Precondition Required',
|
428: "Precondition Required",
|
||||||
429: 'Too Many Requests',
|
429: "Too Many Requests",
|
||||||
431: 'Request Header Fields Too Large',
|
431: "Request Header Fields Too Large",
|
||||||
451: 'Unavailable For Legal Reasons',
|
451: "Unavailable For Legal Reasons",
|
||||||
500: 'Internal Server Error',
|
500: "Internal Server Error",
|
||||||
501: 'Not Implemented',
|
501: "Not Implemented",
|
||||||
502: 'Bad Gateway',
|
502: "Bad Gateway",
|
||||||
503: 'Service Unavailable',
|
503: "Service Unavailable",
|
||||||
504: 'Gateway Timeout',
|
504: "Gateway Timeout",
|
||||||
505: 'HTTP Version Not Supported',
|
505: "HTTP Version Not Supported",
|
||||||
506: 'Variant Also Negotiates',
|
506: "Variant Also Negotiates",
|
||||||
507: 'Insufficient Storage',
|
507: "Insufficient Storage",
|
||||||
508: 'Loop Detected',
|
508: "Loop Detected",
|
||||||
510: 'Not Extended',
|
510: "Not Extended",
|
||||||
511: 'Network Authentication Required',
|
511: "Network Authentication Required",
|
||||||
...options.errors,
|
...options.errors,
|
||||||
}
|
};
|
||||||
|
|
||||||
const error = errors[result.status];
|
const error = errors[result.status];
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -281,8 +303,8 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
const errorStatus = result.status ?? 'unknown';
|
const errorStatus = result.status ?? "unknown";
|
||||||
const errorStatusText = result.statusText ?? 'unknown';
|
const errorStatusText = result.statusText ?? "unknown";
|
||||||
const errorBody = (() => {
|
const errorBody = (() => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(result.body, null, 2);
|
return JSON.stringify(result.body, null, 2);
|
||||||
@ -291,8 +313,10 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
throw new ApiError(options, result,
|
throw new ApiError(
|
||||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
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>
|
* @returns CancelablePromise<T>
|
||||||
* @throws ApiError
|
* @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) => {
|
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||||
try {
|
try {
|
||||||
const url = getUrl(config, options);
|
const url = getUrl(config, options);
|
||||||
@ -314,18 +342,30 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>,
|
|||||||
const headers = await getHeaders(config, options);
|
const headers = await getHeaders(config, options);
|
||||||
|
|
||||||
if (!onCancel.isCancelled) {
|
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) {
|
for (const fn of config.interceptors.response._fns) {
|
||||||
response = await fn(response);
|
response = await fn(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = getResponseBody(response);
|
const responseBody = getResponseBody(response);
|
||||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
const responseHeader = getResponseHeader(
|
||||||
|
response,
|
||||||
|
options.responseHeader,
|
||||||
|
);
|
||||||
|
|
||||||
let transformedBody = responseBody;
|
let transformedBody = responseBody;
|
||||||
if (options.responseTransformer && isSuccess(response.status)) {
|
if (options.responseTransformer && isSuccess(response.status)) {
|
||||||
transformedBody = await options.responseTransformer(responseBody)
|
transformedBody = await options.responseTransformer(responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: ApiResult = {
|
const result: ApiResult = {
|
||||||
@ -344,4 +384,4 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>,
|
|||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
export { ApiError } from './core/ApiError';
|
export { ApiError } from "./core/ApiError";
|
||||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
|
||||||
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
|
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI";
|
||||||
export * from './sdk.gen';
|
export * from "./sdk.gen";
|
||||||
export * from './types.gen';
|
export * from "./types.gen";
|
||||||
|
@ -1,438 +1,485 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { CancelablePromise } from './core/CancelablePromise';
|
import type { CancelablePromise } from "./core/CancelablePromise";
|
||||||
import { OpenAPI } from './core/OpenAPI';
|
import { OpenAPI } from "./core/OpenAPI";
|
||||||
import { request as __request } from './core/request';
|
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 {
|
||||||
|
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 {
|
export class DashboardService {
|
||||||
/**
|
/**
|
||||||
* Get information about currently logged in user
|
* Get information about currently logged in user
|
||||||
* @returns UserPublic Successful Response
|
* @returns UserPublic Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: '/user'
|
url: "/user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user details
|
* Update user details
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.requestBody
|
* @param data.requestBody
|
||||||
* @returns boolean Successful Response
|
* @returns boolean Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
|
public static userUpdateUser(
|
||||||
return __request(OpenAPI, {
|
data: UserUpdateUserData,
|
||||||
method: 'PUT',
|
): CancelablePromise<UserUpdateUserResponse> {
|
||||||
url: '/user',
|
return __request(OpenAPI, {
|
||||||
body: data.requestBody,
|
method: "PUT",
|
||||||
mediaType: 'application/json',
|
url: "/user",
|
||||||
errors: {
|
body: data.requestBody,
|
||||||
422: 'Validation Error'
|
mediaType: "application/json",
|
||||||
}
|
errors: {
|
||||||
});
|
422: "Validation Error",
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
}
|
||||||
* Register new user
|
|
||||||
* @param data The data for the request.
|
/**
|
||||||
* @param data.requestBody
|
* Register new user
|
||||||
* @returns boolean Successful Response
|
* @param data The data for the request.
|
||||||
* @throws ApiError
|
* @param data.requestBody
|
||||||
*/
|
* @returns boolean Successful Response
|
||||||
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
|
* @throws ApiError
|
||||||
return __request(OpenAPI, {
|
*/
|
||||||
method: 'POST',
|
public static userRegister(
|
||||||
url: '/user',
|
data: UserRegisterData,
|
||||||
body: data.requestBody,
|
): CancelablePromise<UserRegisterResponse> {
|
||||||
mediaType: 'application/json',
|
return __request(OpenAPI, {
|
||||||
errors: {
|
method: "POST",
|
||||||
422: 'Validation Error'
|
url: "/user",
|
||||||
}
|
body: data.requestBody,
|
||||||
});
|
mediaType: "application/json",
|
||||||
}
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
/**
|
},
|
||||||
* Delete user
|
});
|
||||||
* @returns boolean Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
/**
|
||||||
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
* Delete user
|
||||||
return __request(OpenAPI, {
|
* @returns boolean Successful Response
|
||||||
method: 'DELETE',
|
* @throws ApiError
|
||||||
url: '/user'
|
*/
|
||||||
});
|
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
||||||
}
|
return __request(OpenAPI, {
|
||||||
|
method: "DELETE",
|
||||||
/**
|
url: "/user",
|
||||||
* Login Access Token
|
});
|
||||||
* OAuth2 compatible token login for the dashboard.
|
}
|
||||||
*
|
|
||||||
* This endpoint generates an access token required for authenticating future
|
/**
|
||||||
* requests to the dashboard section of the application. The token is valid for
|
* Login Access Token
|
||||||
* a predefined expiration period.
|
* OAuth2 compatible token login for the dashboard.
|
||||||
*
|
*
|
||||||
* - **username**: User's email
|
* This endpoint generates an access token required for authenticating future
|
||||||
* - **password**: User's password
|
* requests to the dashboard section of the application. The token is valid for
|
||||||
*
|
* a predefined expiration period.
|
||||||
* **Note:** This login is restricted to dashboard access only and cannot be
|
*
|
||||||
* used for tenant accounts access to shops
|
* - **username**: User's email
|
||||||
* @param data The data for the request.
|
* - **password**: User's password
|
||||||
* @param data.formData
|
*
|
||||||
* @returns Token Successful Response
|
* **Note:** This login is restricted to dashboard access only and cannot be
|
||||||
* @throws ApiError
|
* used for tenant accounts access to shops
|
||||||
*/
|
* @param data The data for the request.
|
||||||
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
* @param data.formData
|
||||||
return __request(OpenAPI, {
|
* @returns Token Successful Response
|
||||||
method: 'POST',
|
* @throws ApiError
|
||||||
url: '/login/access-token',
|
*/
|
||||||
formData: data.formData,
|
public static dashboardLoginAccessToken(
|
||||||
mediaType: 'application/x-www-form-urlencoded',
|
data: DashboardLoginAccessTokenData,
|
||||||
errors: {
|
): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||||
422: 'Validation Error'
|
return __request(OpenAPI, {
|
||||||
}
|
method: "POST",
|
||||||
});
|
url: "/login/access-token",
|
||||||
}
|
formData: data.formData,
|
||||||
|
mediaType: "application/x-www-form-urlencoded",
|
||||||
/**
|
errors: {
|
||||||
* Register New Shop
|
422: "Validation Error",
|
||||||
* @param data The data for the request.
|
},
|
||||||
* @param data.requestBody
|
});
|
||||||
* @returns boolean Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
/**
|
||||||
public static dashboardRegisterNewShop(data: DashboardRegisterNewShopData): CancelablePromise<DashboardRegisterNewShopResponse> {
|
* Register New Shop
|
||||||
return __request(OpenAPI, {
|
* @param data The data for the request.
|
||||||
method: 'POST',
|
* @param data.requestBody
|
||||||
url: '/shop',
|
* @returns boolean Successful Response
|
||||||
body: data.requestBody,
|
* @throws ApiError
|
||||||
mediaType: 'application/json',
|
*/
|
||||||
errors: {
|
public static dashboardRegisterNewShop(
|
||||||
422: 'Validation Error'
|
data: DashboardRegisterNewShopData,
|
||||||
}
|
): CancelablePromise<DashboardRegisterNewShopResponse> {
|
||||||
});
|
return __request(OpenAPI, {
|
||||||
}
|
method: "POST",
|
||||||
|
url: "/shop",
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: "application/json",
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginService {
|
export class LoginService {
|
||||||
/**
|
/**
|
||||||
* Login Access Token
|
* Login Access Token
|
||||||
* OAuth2 compatible token login for the dashboard.
|
* OAuth2 compatible token login for the dashboard.
|
||||||
*
|
*
|
||||||
* This endpoint generates an access token required for authenticating future
|
* This endpoint generates an access token required for authenticating future
|
||||||
* requests to the dashboard section of the application. The token is valid for
|
* requests to the dashboard section of the application. The token is valid for
|
||||||
* a predefined expiration period.
|
* a predefined expiration period.
|
||||||
*
|
*
|
||||||
* - **username**: User's email
|
* - **username**: User's email
|
||||||
* - **password**: User's password
|
* - **password**: User's password
|
||||||
*
|
*
|
||||||
* **Note:** This login is restricted to dashboard access only and cannot be
|
* **Note:** This login is restricted to dashboard access only and cannot be
|
||||||
* used for tenant accounts access to shops
|
* used for tenant accounts access to shops
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.formData
|
* @param data.formData
|
||||||
* @returns Token Successful Response
|
* @returns Token Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
public static dashboardLoginAccessToken(
|
||||||
return __request(OpenAPI, {
|
data: DashboardLoginAccessTokenData,
|
||||||
method: 'POST',
|
): CancelablePromise<DashboardLoginAccessTokenResponse> {
|
||||||
url: '/login/access-token',
|
return __request(OpenAPI, {
|
||||||
formData: data.formData,
|
method: "POST",
|
||||||
mediaType: 'application/x-www-form-urlencoded',
|
url: "/login/access-token",
|
||||||
errors: {
|
formData: data.formData,
|
||||||
422: 'Validation Error'
|
mediaType: "application/x-www-form-urlencoded",
|
||||||
}
|
errors: {
|
||||||
});
|
422: "Validation Error",
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
}
|
||||||
* Login Access Token
|
|
||||||
* OAuth2 compatible token login, get an access token for future requests
|
/**
|
||||||
* @param data The data for the request.
|
* Login Access Token
|
||||||
* @param data.formData
|
* OAuth2 compatible token login, get an access token for future requests
|
||||||
* @returns Token Successful Response
|
* @param data The data for the request.
|
||||||
* @throws ApiError
|
* @param data.formData
|
||||||
*/
|
* @returns Token Successful Response
|
||||||
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
|
* @throws ApiError
|
||||||
return __request(OpenAPI, {
|
*/
|
||||||
method: 'POST',
|
public static shopLoginAccessToken(
|
||||||
url: '/shop/{shop_uuid}/login/access-token',
|
data: ShopLoginAccessTokenData,
|
||||||
formData: data.formData,
|
): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||||
mediaType: 'application/x-www-form-urlencoded',
|
return __request(OpenAPI, {
|
||||||
errors: {
|
method: "POST",
|
||||||
422: 'Validation Error'
|
url: "/shop/{shop_uuid}/login/access-token",
|
||||||
}
|
formData: data.formData,
|
||||||
});
|
mediaType: "application/x-www-form-urlencoded",
|
||||||
}
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShopService {
|
export class ShopService {
|
||||||
/**
|
/**
|
||||||
* Login Access Token
|
* Login Access Token
|
||||||
* OAuth2 compatible token login, get an access token for future requests
|
* OAuth2 compatible token login, get an access token for future requests
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.formData
|
* @param data.formData
|
||||||
* @returns Token Successful Response
|
* @returns Token Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
|
public static shopLoginAccessToken(
|
||||||
return __request(OpenAPI, {
|
data: ShopLoginAccessTokenData,
|
||||||
method: 'POST',
|
): CancelablePromise<ShopLoginAccessTokenResponse> {
|
||||||
url: '/shop/{shop_uuid}/login/access-token',
|
return __request(OpenAPI, {
|
||||||
formData: data.formData,
|
method: "POST",
|
||||||
mediaType: 'application/x-www-form-urlencoded',
|
url: "/shop/{shop_uuid}/login/access-token",
|
||||||
errors: {
|
formData: data.formData,
|
||||||
422: 'Validation Error'
|
mediaType: "application/x-www-form-urlencoded",
|
||||||
}
|
errors: {
|
||||||
});
|
422: "Validation Error",
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
}
|
||||||
* Delete user
|
|
||||||
* @param data The data for the request.
|
/**
|
||||||
* @param data.shopUuid
|
* Delete user
|
||||||
* @returns unknown Successful Response
|
* @param data The data for the request.
|
||||||
* @throws ApiError
|
* @param data.shopUuid
|
||||||
*/
|
* @returns unknown Successful Response
|
||||||
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
|
* @throws ApiError
|
||||||
return __request(OpenAPI, {
|
*/
|
||||||
method: 'DELETE',
|
public static shopDeleteUser(
|
||||||
url: '/shop/{shop_uuid}/user/delete',
|
data: ShopDeleteUserData,
|
||||||
path: {
|
): CancelablePromise<ShopDeleteUserResponse> {
|
||||||
shop_uuid: data.shopUuid
|
return __request(OpenAPI, {
|
||||||
},
|
method: "DELETE",
|
||||||
errors: {
|
url: "/shop/{shop_uuid}/user/delete",
|
||||||
422: 'Validation Error'
|
path: {
|
||||||
}
|
shop_uuid: data.shopUuid,
|
||||||
});
|
},
|
||||||
}
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
/**
|
},
|
||||||
* User logout
|
});
|
||||||
* @returns unknown Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
/**
|
||||||
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
* User logout
|
||||||
return __request(OpenAPI, {
|
* @returns unknown Successful Response
|
||||||
method: 'DELETE',
|
* @throws ApiError
|
||||||
url: '/shop/{shop_uuid}/user/logout'
|
*/
|
||||||
});
|
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
||||||
}
|
return __request(OpenAPI, {
|
||||||
|
method: "DELETE",
|
||||||
/**
|
url: "/shop/{shop_uuid}/user/logout",
|
||||||
* Register new user
|
});
|
||||||
* @param data The data for the request.
|
}
|
||||||
* @param data.shopUuid
|
|
||||||
* @param data.requestBody
|
/**
|
||||||
* @returns unknown Successful Response
|
* Register new user
|
||||||
* @throws ApiError
|
* @param data The data for the request.
|
||||||
*/
|
* @param data.shopUuid
|
||||||
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
|
* @param data.requestBody
|
||||||
return __request(OpenAPI, {
|
* @returns unknown Successful Response
|
||||||
method: 'POST',
|
* @throws ApiError
|
||||||
url: '/shop/{shop_uuid}/user/register',
|
*/
|
||||||
path: {
|
public static shopRegister(
|
||||||
shop_uuid: data.shopUuid
|
data: ShopRegisterData,
|
||||||
},
|
): CancelablePromise<ShopRegisterResponse> {
|
||||||
body: data.requestBody,
|
return __request(OpenAPI, {
|
||||||
mediaType: 'application/json',
|
method: "POST",
|
||||||
errors: {
|
url: "/shop/{shop_uuid}/user/register",
|
||||||
422: 'Validation Error'
|
path: {
|
||||||
}
|
shop_uuid: data.shopUuid,
|
||||||
});
|
},
|
||||||
}
|
body: data.requestBody,
|
||||||
|
mediaType: "application/json",
|
||||||
/**
|
errors: {
|
||||||
* Update user details
|
422: "Validation Error",
|
||||||
* @param data The data for the request.
|
},
|
||||||
* @param data.requestBody
|
});
|
||||||
* @returns unknown Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
/**
|
||||||
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
|
* Update user details
|
||||||
return __request(OpenAPI, {
|
* @param data The data for the request.
|
||||||
method: 'PUT',
|
* @param data.requestBody
|
||||||
url: '/shop/{shop_uuid}/user/update',
|
* @returns unknown Successful Response
|
||||||
body: data.requestBody,
|
* @throws ApiError
|
||||||
mediaType: 'application/json',
|
*/
|
||||||
errors: {
|
public static shopUpdateUser(
|
||||||
422: 'Validation Error'
|
data: ShopUpdateUserData,
|
||||||
}
|
): CancelablePromise<ShopUpdateUserResponse> {
|
||||||
});
|
return __request(OpenAPI, {
|
||||||
}
|
method: "PUT",
|
||||||
|
url: "/shop/{shop_uuid}/user/update",
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: "application/json",
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
/**
|
/**
|
||||||
* Get information about currently logged in user
|
* Get information about currently logged in user
|
||||||
* @returns UserPublic Successful Response
|
* @returns UserPublic Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: '/user'
|
url: "/user",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update user details
|
* Update user details
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
* @param data.requestBody
|
* @param data.requestBody
|
||||||
* @returns boolean Successful Response
|
* @returns boolean Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
|
public static userUpdateUser(
|
||||||
return __request(OpenAPI, {
|
data: UserUpdateUserData,
|
||||||
method: 'PUT',
|
): CancelablePromise<UserUpdateUserResponse> {
|
||||||
url: '/user',
|
return __request(OpenAPI, {
|
||||||
body: data.requestBody,
|
method: "PUT",
|
||||||
mediaType: 'application/json',
|
url: "/user",
|
||||||
errors: {
|
body: data.requestBody,
|
||||||
422: 'Validation Error'
|
mediaType: "application/json",
|
||||||
}
|
errors: {
|
||||||
});
|
422: "Validation Error",
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
}
|
||||||
* Register new user
|
|
||||||
* @param data The data for the request.
|
/**
|
||||||
* @param data.requestBody
|
* Register new user
|
||||||
* @returns boolean Successful Response
|
* @param data The data for the request.
|
||||||
* @throws ApiError
|
* @param data.requestBody
|
||||||
*/
|
* @returns boolean Successful Response
|
||||||
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
|
* @throws ApiError
|
||||||
return __request(OpenAPI, {
|
*/
|
||||||
method: 'POST',
|
public static userRegister(
|
||||||
url: '/user',
|
data: UserRegisterData,
|
||||||
body: data.requestBody,
|
): CancelablePromise<UserRegisterResponse> {
|
||||||
mediaType: 'application/json',
|
return __request(OpenAPI, {
|
||||||
errors: {
|
method: "POST",
|
||||||
422: 'Validation Error'
|
url: "/user",
|
||||||
}
|
body: data.requestBody,
|
||||||
});
|
mediaType: "application/json",
|
||||||
}
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
/**
|
},
|
||||||
* Delete user
|
});
|
||||||
* @returns boolean Successful Response
|
}
|
||||||
* @throws ApiError
|
|
||||||
*/
|
/**
|
||||||
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
* Delete user
|
||||||
return __request(OpenAPI, {
|
* @returns boolean Successful Response
|
||||||
method: 'DELETE',
|
* @throws ApiError
|
||||||
url: '/user'
|
*/
|
||||||
});
|
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
|
||||||
}
|
return __request(OpenAPI, {
|
||||||
|
method: "DELETE",
|
||||||
/**
|
url: "/user",
|
||||||
* Delete user
|
});
|
||||||
* @param data The data for the request.
|
}
|
||||||
* @param data.shopUuid
|
|
||||||
* @returns unknown Successful Response
|
/**
|
||||||
* @throws ApiError
|
* Delete user
|
||||||
*/
|
* @param data The data for the request.
|
||||||
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
|
* @param data.shopUuid
|
||||||
return __request(OpenAPI, {
|
* @returns unknown Successful Response
|
||||||
method: 'DELETE',
|
* @throws ApiError
|
||||||
url: '/shop/{shop_uuid}/user/delete',
|
*/
|
||||||
path: {
|
public static shopDeleteUser(
|
||||||
shop_uuid: data.shopUuid
|
data: ShopDeleteUserData,
|
||||||
},
|
): CancelablePromise<ShopDeleteUserResponse> {
|
||||||
errors: {
|
return __request(OpenAPI, {
|
||||||
422: 'Validation Error'
|
method: "DELETE",
|
||||||
}
|
url: "/shop/{shop_uuid}/user/delete",
|
||||||
});
|
path: {
|
||||||
}
|
shop_uuid: data.shopUuid,
|
||||||
|
},
|
||||||
/**
|
errors: {
|
||||||
* User logout
|
422: "Validation Error",
|
||||||
* @returns unknown Successful Response
|
},
|
||||||
* @throws ApiError
|
});
|
||||||
*/
|
}
|
||||||
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
|
||||||
return __request(OpenAPI, {
|
/**
|
||||||
method: 'DELETE',
|
* User logout
|
||||||
url: '/shop/{shop_uuid}/user/logout'
|
* @returns unknown Successful Response
|
||||||
});
|
* @throws ApiError
|
||||||
}
|
*/
|
||||||
|
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
|
||||||
/**
|
return __request(OpenAPI, {
|
||||||
* Register new user
|
method: "DELETE",
|
||||||
* @param data The data for the request.
|
url: "/shop/{shop_uuid}/user/logout",
|
||||||
* @param data.shopUuid
|
});
|
||||||
* @param data.requestBody
|
}
|
||||||
* @returns unknown Successful Response
|
|
||||||
* @throws ApiError
|
/**
|
||||||
*/
|
* Register new user
|
||||||
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
|
* @param data The data for the request.
|
||||||
return __request(OpenAPI, {
|
* @param data.shopUuid
|
||||||
method: 'POST',
|
* @param data.requestBody
|
||||||
url: '/shop/{shop_uuid}/user/register',
|
* @returns unknown Successful Response
|
||||||
path: {
|
* @throws ApiError
|
||||||
shop_uuid: data.shopUuid
|
*/
|
||||||
},
|
public static shopRegister(
|
||||||
body: data.requestBody,
|
data: ShopRegisterData,
|
||||||
mediaType: 'application/json',
|
): CancelablePromise<ShopRegisterResponse> {
|
||||||
errors: {
|
return __request(OpenAPI, {
|
||||||
422: 'Validation Error'
|
method: "POST",
|
||||||
}
|
url: "/shop/{shop_uuid}/user/register",
|
||||||
});
|
path: {
|
||||||
}
|
shop_uuid: data.shopUuid,
|
||||||
|
},
|
||||||
/**
|
body: data.requestBody,
|
||||||
* Update user details
|
mediaType: "application/json",
|
||||||
* @param data The data for the request.
|
errors: {
|
||||||
* @param data.requestBody
|
422: "Validation Error",
|
||||||
* @returns unknown Successful Response
|
},
|
||||||
* @throws ApiError
|
});
|
||||||
*/
|
}
|
||||||
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
|
|
||||||
return __request(OpenAPI, {
|
/**
|
||||||
method: 'PUT',
|
* Update user details
|
||||||
url: '/shop/{shop_uuid}/user/update',
|
* @param data The data for the request.
|
||||||
body: data.requestBody,
|
* @param data.requestBody
|
||||||
mediaType: 'application/json',
|
* @returns unknown Successful Response
|
||||||
errors: {
|
* @throws ApiError
|
||||||
422: 'Validation Error'
|
*/
|
||||||
}
|
public static shopUpdateUser(
|
||||||
});
|
data: ShopUpdateUserData,
|
||||||
}
|
): CancelablePromise<ShopUpdateUserResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: "PUT",
|
||||||
|
url: "/shop/{shop_uuid}/user/update",
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: "application/json",
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UtilsService {
|
export class UtilsService {
|
||||||
/**
|
/**
|
||||||
* Health Check
|
* Health Check
|
||||||
* Ping the API whether it's alive or not
|
* Ping the API whether it's alive or not
|
||||||
* @returns boolean Successful Response
|
* @returns boolean Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static utilsHealthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
|
public static utilsHealthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: '/utils/health-check/'
|
url: "/utils/health-check/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Db
|
* Test Db
|
||||||
* Ping database using select 1 to see if connection works
|
* Ping database using select 1 to see if connection works
|
||||||
* @returns boolean Successful Response
|
* @returns boolean Successful Response
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static utilsTestDb(): CancelablePromise<UtilsTestDbResponse> {
|
public static utilsTestDb(): CancelablePromise<UtilsTestDbResponse> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: '/utils/test-db/'
|
url: "/utils/test-db/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -1,142 +1,142 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export type Body_Dashboard_login_access_token = {
|
export type Body_Dashboard_login_access_token = {
|
||||||
grant_type?: (string | null);
|
grant_type?: string | null;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
client_id?: (string | null);
|
client_id?: string | null;
|
||||||
client_secret?: (string | null);
|
client_secret?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Body_Shop_login_access_token = {
|
export type Body_Shop_login_access_token = {
|
||||||
grant_type?: (string | null);
|
grant_type?: string | null;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
client_id?: (string | null);
|
client_id?: string | null;
|
||||||
client_secret?: (string | null);
|
client_secret?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HTTPValidationError = {
|
export type HTTPValidationError = {
|
||||||
detail?: Array<ValidationError>;
|
detail?: Array<ValidationError>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopAddress = {
|
export type ShopAddress = {
|
||||||
street: string;
|
street: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
postal_code: string;
|
postal_code: string;
|
||||||
country: string;
|
country: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopCreate = {
|
export type ShopCreate = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
contact_email: string;
|
contact_email: string;
|
||||||
contact_phone_number: string;
|
contact_phone_number: string;
|
||||||
address: ShopAddress;
|
address: ShopAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Token = {
|
export type Token = {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type?: string;
|
token_type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserPublic = {
|
export type UserPublic = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
first_name: (string | null);
|
first_name: string | null;
|
||||||
last_name: (string | null);
|
last_name: string | null;
|
||||||
phone_number: string;
|
phone_number: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserRegister = {
|
export type UserRegister = {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone_number: string;
|
phone_number: string;
|
||||||
/**
|
/**
|
||||||
* Password must conform to this regex:
|
* Password must conform to this regex:
|
||||||
* ```
|
* ```
|
||||||
* ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$
|
* ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserUpdate = {
|
export type UserUpdate = {
|
||||||
email: (string | null);
|
email: string | null;
|
||||||
phone_number: (string | null);
|
phone_number: string | null;
|
||||||
username: (string | null);
|
username: string | null;
|
||||||
first_name?: (string | null);
|
first_name?: string | null;
|
||||||
last_name?: (string | null);
|
last_name?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValidationError = {
|
export type ValidationError = {
|
||||||
loc: Array<(string | number)>;
|
loc: Array<string | number>;
|
||||||
msg: string;
|
msg: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserGetUserResponse = (UserPublic);
|
export type UserGetUserResponse = UserPublic;
|
||||||
|
|
||||||
export type UserUpdateUserData = {
|
export type UserUpdateUserData = {
|
||||||
requestBody: UserUpdate;
|
requestBody: UserUpdate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserUpdateUserResponse = (boolean);
|
export type UserUpdateUserResponse = boolean;
|
||||||
|
|
||||||
export type UserRegisterData = {
|
export type UserRegisterData = {
|
||||||
requestBody: UserRegister;
|
requestBody: UserRegister;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserRegisterResponse = (boolean);
|
export type UserRegisterResponse = boolean;
|
||||||
|
|
||||||
export type UserDeleteUserResponse = (boolean);
|
export type UserDeleteUserResponse = boolean;
|
||||||
|
|
||||||
export type DashboardLoginAccessTokenData = {
|
export type DashboardLoginAccessTokenData = {
|
||||||
formData: Body_Dashboard_login_access_token;
|
formData: Body_Dashboard_login_access_token;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardLoginAccessTokenResponse = (Token);
|
export type DashboardLoginAccessTokenResponse = Token;
|
||||||
|
|
||||||
export type DashboardRegisterNewShopData = {
|
export type DashboardRegisterNewShopData = {
|
||||||
requestBody: ShopCreate;
|
requestBody: ShopCreate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardRegisterNewShopResponse = (boolean);
|
export type DashboardRegisterNewShopResponse = boolean;
|
||||||
|
|
||||||
export type ShopLoginAccessTokenData = {
|
export type ShopLoginAccessTokenData = {
|
||||||
formData: Body_Shop_login_access_token;
|
formData: Body_Shop_login_access_token;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopLoginAccessTokenResponse = (Token);
|
export type ShopLoginAccessTokenResponse = Token;
|
||||||
|
|
||||||
export type ShopDeleteUserData = {
|
export type ShopDeleteUserData = {
|
||||||
shopUuid: unknown;
|
shopUuid: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopDeleteUserResponse = (unknown);
|
export type ShopDeleteUserResponse = unknown;
|
||||||
|
|
||||||
export type ShopLogoutResponse = (unknown);
|
export type ShopLogoutResponse = unknown;
|
||||||
|
|
||||||
export type ShopRegisterData = {
|
export type ShopRegisterData = {
|
||||||
requestBody: UserRegister;
|
requestBody: UserRegister;
|
||||||
shopUuid: unknown;
|
shopUuid: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopRegisterResponse = (unknown);
|
export type ShopRegisterResponse = unknown;
|
||||||
|
|
||||||
export type ShopUpdateUserData = {
|
export type ShopUpdateUserData = {
|
||||||
requestBody: {
|
requestBody: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
IconArrowRightDashed,
|
||||||
IconDeviceLaptop,
|
IconDeviceLaptop,
|
||||||
IconMoon,
|
IconMoon,
|
||||||
IconSun
|
IconSun,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useSearch } from "@/context/search-context";
|
import { useSearch } from "@/context/search-context";
|
||||||
import { useTheme } from "@/context/theme-context";
|
import { useTheme } from "@/context/theme-context";
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { sidebarData } from "./layout/data/sidebar-data";
|
import { sidebarData } from "./layout/data/sidebar-data";
|
||||||
import { ScrollArea } from "./ui/scroll-area";
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
@ -30,7 +30,7 @@ export function CommandMenu() {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
command();
|
command();
|
||||||
},
|
},
|
||||||
[setOpen]
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -49,7 +49,8 @@ export function CommandMenu() {
|
|||||||
value={navItem.title}
|
value={navItem.title}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
runCommand(() => navigate({ to: navItem.url }));
|
runCommand(() => navigate({ to: navItem.url }));
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||||
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +64,8 @@ export function CommandMenu() {
|
|||||||
value={subItem.title}
|
value={subItem.title}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
runCommand(() => navigate({ to: subItem.url }));
|
runCommand(() => navigate({ to: subItem.url }));
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||||
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
@ -56,7 +56,8 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant={destructive ? "destructive" : "default"}
|
variant={destructive ? "destructive" : "default"}
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
disabled={disabled || isLoading}>
|
disabled={disabled || isLoading}
|
||||||
|
>
|
||||||
{confirmText ?? "Continue"}
|
{confirmText ?? "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</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 {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarHeader,
|
SidebarRail,
|
||||||
SidebarRail
|
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { NavGroup } from "@/components/layout/nav-group";
|
import { NavGroup } from "@/components/layout/nav-group";
|
||||||
import { sidebarData } from "./data/sidebar-data";
|
import { sidebarData } from "./data/sidebar-data";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" variant="floating" {...props}>
|
<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>
|
<SidebarContent>
|
||||||
{sidebarData.navGroups.map((props) => (
|
{sidebarData.navGroups.map((props) => (
|
||||||
<NavGroup key={props.title} {...props} />
|
<NavGroup key={props.title} {...props} />
|
||||||
))}
|
))}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
{/* <SidebarFooter>
|
|
||||||
<NavUser user={sidebarData.user} />
|
|
||||||
</SidebarFooter> */}
|
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,14 @@
|
|||||||
import {
|
import
|
||||||
IconBrowserCheck,
|
{
|
||||||
IconBuildingStore,
|
IconBuildingStore,
|
||||||
IconCoin,
|
IconClipboardCheckFilled,
|
||||||
IconForklift,
|
IconCoin, IconLayoutDashboard, IconPackage,
|
||||||
IconHelp,
|
IconPalette,
|
||||||
IconLayoutDashboard,
|
IconSettings,
|
||||||
IconNotification,
|
IconTag,
|
||||||
IconPackage,
|
IconUserCog,
|
||||||
IconPalette,
|
IconUsers
|
||||||
IconPercentage,
|
} from "@tabler/icons-react";
|
||||||
IconSettings,
|
|
||||||
IconTag,
|
|
||||||
IconTool,
|
|
||||||
IconUserCog,
|
|
||||||
IconUsers
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
|
|
||||||
import { type SidebarData } from "../types";
|
import { type SidebarData } from "../types";
|
||||||
|
|
||||||
export const sidebarData: SidebarData = {
|
export const sidebarData: SidebarData = {
|
||||||
@ -26,45 +19,40 @@ export const sidebarData: SidebarData = {
|
|||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "/dashboard",
|
url: "/dashboard",
|
||||||
icon: IconLayoutDashboard
|
icon: IconLayoutDashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Shop",
|
title: "Shop",
|
||||||
url: "/dashboard/shop",
|
url: "/dashboard/shop",
|
||||||
icon: IconBuildingStore
|
icon: IconBuildingStore,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Products",
|
title: "Products",
|
||||||
url: "/dashboard/products",
|
url: "/dashboard/products",
|
||||||
icon: IconPackage
|
icon: IconPackage,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Inventory",
|
|
||||||
url: "/dashboard/tasks",
|
|
||||||
icon: IconForklift
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sales",
|
title: "Sales",
|
||||||
icon: IconCoin,
|
icon: IconCoin,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Discounts",
|
title: "Recent sales",
|
||||||
url: "/dashboard/sales/discounts",
|
url: "/dashboard/sales/recent-sales",
|
||||||
icon: IconPercentage
|
icon: IconClipboardCheckFilled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Coupons",
|
title: "Coupons",
|
||||||
url: "/dashboard/sales/coupons",
|
url: "/dashboard/sales/coupons",
|
||||||
icon: IconTag
|
icon: IconTag,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Customers",
|
title: "Customers",
|
||||||
url: "/dashboard/users",
|
url: "/dashboard/users",
|
||||||
icon: IconUsers
|
icon: IconUsers,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Other",
|
title: "Other",
|
||||||
@ -76,36 +64,17 @@ export const sidebarData: SidebarData = {
|
|||||||
{
|
{
|
||||||
title: "Profile",
|
title: "Profile",
|
||||||
url: "/dashboard/settings",
|
url: "/dashboard/settings",
|
||||||
icon: IconUserCog
|
icon: IconUserCog,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Account",
|
|
||||||
url: "/dashboard/settings/account",
|
|
||||||
icon: IconTool
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Appearance",
|
title: "Appearance",
|
||||||
url: "/dashboard/settings/appearance",
|
url: "/dashboard/settings/appearance",
|
||||||
icon: IconPalette
|
icon: IconPalette,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Notifications",
|
|
||||||
url: "/dashboard/settings/notifications",
|
|
||||||
icon: IconNotification
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Display",
|
|
||||||
url: "/dashboard/settings/display",
|
|
||||||
icon: IconBrowserCheck
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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",
|
"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",
|
fixed && "header-fixed peer/header fixed z-50 w-[inherit] rounded-md",
|
||||||
offset > 10 && fixed ? "shadow" : "shadow-none",
|
offset > 10 && fixed ? "shadow" : "shadow-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
|
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
|
||||||
<Separator orientation="vertical" className="h-6" />
|
<Separator orientation="vertical" className="h-6" />
|
||||||
{children}
|
{children}
|
||||||
|
@ -12,7 +12,7 @@ export const Main = ({ fixed, ...props }: MainProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"peer-[.header-fixed]/header:mt-16",
|
"peer-[.header-fixed]/header:mt-16",
|
||||||
"px-4 py-6",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -4,7 +4,7 @@ import { ChevronRight } from "lucide-react";
|
|||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
useSidebar
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import {
|
import {
|
||||||
@ -24,7 +24,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
|
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
|
||||||
|
|
||||||
@ -64,7 +64,8 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
isActive={checkIsActive(href, item)}
|
isActive={checkIsActive(href, item)}
|
||||||
tooltip={item.title}>
|
tooltip={item.title}
|
||||||
|
>
|
||||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||||
{item.icon && <item.icon />}
|
{item.icon && <item.icon />}
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
@ -77,7 +78,7 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
|
|||||||
|
|
||||||
const SidebarMenuCollapsible = ({
|
const SidebarMenuCollapsible = ({
|
||||||
item,
|
item,
|
||||||
href
|
href,
|
||||||
}: {
|
}: {
|
||||||
item: NavCollapsible;
|
item: NavCollapsible;
|
||||||
href: string;
|
href: string;
|
||||||
@ -87,7 +88,8 @@ const SidebarMenuCollapsible = ({
|
|||||||
<Collapsible
|
<Collapsible
|
||||||
asChild
|
asChild
|
||||||
defaultOpen={checkIsActive(href, item, true)}
|
defaultOpen={checkIsActive(href, item, true)}
|
||||||
className="group/collapsible">
|
className="group/collapsible"
|
||||||
|
>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
@ -103,7 +105,8 @@ const SidebarMenuCollapsible = ({
|
|||||||
<SidebarMenuSubItem key={subItem.title}>
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
asChild
|
asChild
|
||||||
isActive={checkIsActive(href, subItem)}>
|
isActive={checkIsActive(href, subItem)}
|
||||||
|
>
|
||||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||||
{subItem.icon && <subItem.icon />}
|
{subItem.icon && <subItem.icon />}
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
@ -121,7 +124,7 @@ const SidebarMenuCollapsible = ({
|
|||||||
|
|
||||||
const SidebarMenuCollapsedDropdown = ({
|
const SidebarMenuCollapsedDropdown = ({
|
||||||
item,
|
item,
|
||||||
href
|
href,
|
||||||
}: {
|
}: {
|
||||||
item: NavCollapsible;
|
item: NavCollapsible;
|
||||||
href: string;
|
href: string;
|
||||||
@ -132,7 +135,8 @@ const SidebarMenuCollapsedDropdown = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
tooltip={item.title}
|
tooltip={item.title}
|
||||||
isActive={checkIsActive(href, item)}>
|
isActive={checkIsActive(href, item)}
|
||||||
|
>
|
||||||
{item.icon && <item.icon />}
|
{item.icon && <item.icon />}
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||||
@ -148,7 +152,8 @@ const SidebarMenuCollapsedDropdown = ({
|
|||||||
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
||||||
<Link
|
<Link
|
||||||
to={sub.url}
|
to={sub.url}
|
||||||
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}>
|
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}
|
||||||
|
>
|
||||||
{sub.icon && <sub.icon />}
|
{sub.icon && <sub.icon />}
|
||||||
<span className="max-w-52 text-wrap">{sub.title}</span>
|
<span className="max-w-52 text-wrap">{sub.title}</span>
|
||||||
{sub.badge && (
|
{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,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function TeamSwitcher({
|
export function TeamSwitcher({
|
||||||
teams
|
teams,
|
||||||
}: {
|
}: {
|
||||||
teams: {
|
teams: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -35,7 +35,8 @@ export function TeamSwitcher({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
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">
|
<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" />
|
<activeTeam.logo className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
@ -52,7 +53,8 @@ export function TeamSwitcher({
|
|||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
align="start"
|
align="start"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
sideOffset={4}>
|
sideOffset={4}
|
||||||
|
>
|
||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Teams
|
Teams
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@ -60,7 +62,8 @@ export function TeamSwitcher({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={team.name}
|
key={team.name}
|
||||||
onClick={() => setActiveTeam(team)}
|
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">
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
<team.logo className="size-4 shrink-0" />
|
<team.logo className="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
@ -34,7 +34,8 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
|
|||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
className={!isActive ? "text-muted-foreground" : ""}
|
className={!isActive ? "text-muted-foreground" : ""}
|
||||||
disabled={disabled}>
|
disabled={disabled}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -46,15 +47,17 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
|
|||||||
<nav
|
<nav
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden items-center space-x-4 md:flex lg:space-x-6",
|
"hidden items-center space-x-4 md:flex lg:space-x-6",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{links.map(({ title, href, isActive, disabled }) => (
|
{links.map(({ title, href, isActive, disabled }) => (
|
||||||
<Link
|
<Link
|
||||||
key={`${title}-${href}`}
|
key={`${title}-${href}`}
|
||||||
to={href}
|
to={href}
|
||||||
disabled={disabled}
|
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}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
@ -3,13 +3,13 @@ import { cn } from "@/lib/utils";
|
|||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -21,7 +21,7 @@ interface Props {
|
|||||||
export default function LongText({
|
export default function LongText({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
contentClassName = ""
|
contentClassName = "",
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isOverflown, setIsOverflown] = useState(false);
|
const [isOverflown, setIsOverflown] = useState(false);
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Menu } from "lucide-react";
|
import { Menu } from "lucide-react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
@ -18,9 +18,9 @@ const MainNavbar = () => {
|
|||||||
<li className="font-medium text-primary">
|
<li className="font-medium text-primary">
|
||||||
<a href="#home">Home</a>
|
<a href="#home">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{/* <li>
|
||||||
<a href="#pricing">Pricing</a>
|
<a href="#pricing">Pricing</a>
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
<li>
|
||||||
<a href="#faqs">FAQs</a>
|
<a href="#faqs">FAQs</a>
|
||||||
</li>
|
</li>
|
||||||
@ -29,7 +29,6 @@ const MainNavbar = () => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DynamicLoginButton />
|
<DynamicLoginButton />
|
||||||
<div className="mr-2 flex items-center gap-2 md:hidden">
|
<div className="mr-2 flex items-center gap-2 md:hidden">
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
@ -44,9 +43,9 @@ const MainNavbar = () => {
|
|||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a href="#features">Features</a>
|
<a href="#features">Features</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
{/* <DropdownMenuItem>
|
||||||
<a href="#pricing">Pricing</a>
|
<a href="#pricing">Pricing</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem> */}
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a href="#faqs">FAQs</a>
|
<a href="#faqs">FAQs</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -5,7 +5,7 @@ import { ChevronRight, type LucideIcon } from "lucide-react";
|
|||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
@ -15,11 +15,11 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem
|
SidebarMenuSubItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavMain({
|
export function NavMain({
|
||||||
items
|
items,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string;
|
title: string;
|
||||||
@ -41,7 +41,8 @@ export function NavMain({
|
|||||||
key={item.title}
|
key={item.title}
|
||||||
asChild
|
asChild
|
||||||
defaultOpen={item.isActive}
|
defaultOpen={item.isActive}
|
||||||
className="group/collapsible">
|
className="group/collapsible"
|
||||||
|
>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
Forward,
|
Forward,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Trash2,
|
Trash2,
|
||||||
type LucideIcon
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
@ -22,11 +22,11 @@ import {
|
|||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavProjects({
|
export function NavProjects({
|
||||||
projects
|
projects,
|
||||||
}: {
|
}: {
|
||||||
projects: {
|
projects: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -58,7 +58,8 @@ export function NavProjects({
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-48 rounded-lg"
|
className="w-48 rounded-lg"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
align={isMobile ? "end" : "start"}>
|
align={isMobile ? "end" : "start"}
|
||||||
|
>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Folder className="text-muted-foreground" />
|
<Folder className="text-muted-foreground" />
|
||||||
<span>View Project</span>
|
<span>View Project</span>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
LogOut,
|
LogOut,
|
||||||
Sparkles
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
@ -17,17 +17,17 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user
|
user,
|
||||||
}: {
|
}: {
|
||||||
user: {
|
user: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -44,7 +44,8 @@ export function NavUser({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
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">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<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"
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={4}>
|
sideOffset={4}
|
||||||
|
>
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
|
@ -26,12 +26,13 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground"
|
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} />}
|
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
PasswordInput.displayName = "PasswordInput";
|
PasswordInput.displayName = "PasswordInput";
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
|||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
length,
|
length,
|
||||||
readOnly
|
readOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* call onChange func if pinValue changes */
|
/* call onChange func if pinValue changes */
|
||||||
@ -171,7 +171,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
|||||||
} else {
|
} else {
|
||||||
refMap?.delete(pinIndex);
|
refMap?.delete(pinIndex);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
skipRef.current = skipRef.current + 1;
|
skipRef.current = skipRef.current + 1;
|
||||||
@ -220,7 +220,7 @@ const PinInputField = <T extends React.ElementType = "input">({
|
|||||||
const isInsidePinInput = React.useContext(PinInputContext);
|
const isInsidePinInput = React.useContext(PinInputContext);
|
||||||
if (!isInsidePinInput) {
|
if (!isInsidePinInput) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`PinInputField must be used within ${PinInput.displayName}.`
|
`PinInputField must be used within ${PinInput.displayName}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +254,7 @@ const usePinInput = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
length,
|
length,
|
||||||
readOnly
|
readOnly,
|
||||||
}: UsePinInputProps) => {
|
}: UsePinInputProps) => {
|
||||||
const pinInputs = React.useMemo(
|
const pinInputs = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -263,9 +263,9 @@ const usePinInput = ({
|
|||||||
? defaultValue.charAt(index)
|
? defaultValue.charAt(index)
|
||||||
: value
|
: value
|
||||||
? value.charAt(index)
|
? value.charAt(index)
|
||||||
: ""
|
: "",
|
||||||
),
|
),
|
||||||
[defaultValue, length, value]
|
[defaultValue, length, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [pins, setPins] = React.useState(pinInputs);
|
const [pins, setPins] = React.useState(pinInputs);
|
||||||
@ -305,7 +305,7 @@ const usePinInput = ({
|
|||||||
|
|
||||||
function handleFocus(
|
function handleFocus(
|
||||||
event: React.FocusEvent<HTMLInputElement>,
|
event: React.FocusEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number,
|
||||||
) {
|
) {
|
||||||
event.target.select();
|
event.target.select();
|
||||||
focusInput(index);
|
focusInput(index);
|
||||||
@ -332,7 +332,7 @@ const usePinInput = ({
|
|||||||
} else {
|
} else {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +384,7 @@ const usePinInput = ({
|
|||||||
|
|
||||||
function handleKeyDown(
|
function handleKeyDown(
|
||||||
event: React.KeyboardEvent<HTMLInputElement>,
|
event: React.KeyboardEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number,
|
||||||
) {
|
) {
|
||||||
const { ctrlKey, key, shiftKey, metaKey } = event;
|
const { ctrlKey, key, shiftKey, metaKey } = event;
|
||||||
|
|
||||||
@ -429,7 +429,7 @@ const usePinInput = ({
|
|||||||
handleBlur,
|
handleBlur,
|
||||||
handleChange,
|
handleChange,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleKeyDown
|
handleKeyDown,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
|
|
||||||
@ -45,12 +45,6 @@ export function ProfileDropdown() {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to="/dashboard/settings">
|
|
||||||
Profile
|
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/dashboard/settings">
|
<Link to="/dashboard/settings">
|
||||||
Settings
|
Settings
|
||||||
|
@ -16,9 +16,10 @@ export function Search({ className = "", placeholder = "Search" }: Props) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
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",
|
"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
|
<IconSearch
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute left-1.5 top-1/2 -translate-y-1/2"
|
className="absolute left-1.5 top-1/2 -translate-y-1/2"
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
interface SelectDropdownProps {
|
interface SelectDropdownProps {
|
||||||
@ -28,7 +28,7 @@ export function SelectDropdown({
|
|||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
className = "",
|
className = "",
|
||||||
isControlled = false
|
isControlled = false,
|
||||||
}: SelectDropdownProps) {
|
}: SelectDropdownProps) {
|
||||||
const defaultState = isControlled
|
const defaultState = isControlled
|
||||||
? { value: defaultValue, onValueChange }
|
? { value: defaultValue, onValueChange }
|
||||||
|
@ -2,7 +2,8 @@ const SkipToMain = () => {
|
|||||||
return (
|
return (
|
||||||
<a
|
<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`}
|
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
|
Skip to Main
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -10,17 +10,17 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function TeamSwitcher({
|
export function TeamSwitcher({
|
||||||
teams
|
teams,
|
||||||
}: {
|
}: {
|
||||||
teams: {
|
teams: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -38,7 +38,8 @@ export function TeamSwitcher({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
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">
|
<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" />
|
<activeTeam.logo className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +56,8 @@ export function TeamSwitcher({
|
|||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
align="start"
|
align="start"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
sideOffset={4}>
|
sideOffset={4}
|
||||||
|
>
|
||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Teams
|
Teams
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@ -63,7 +65,8 @@ export function TeamSwitcher({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={team.name}
|
key={team.name}
|
||||||
onClick={() => setActiveTeam(team)}
|
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">
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
<team.logo className="size-4 shrink-0" />
|
<team.logo className="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
export function ThemeSwitch() {
|
export function ThemeSwitch() {
|
||||||
|
@ -1,56 +1,56 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import { ChevronDown } from "lucide-react"
|
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<
|
const AccordionItem = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("border-b", className)}
|
className={cn("border-b", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AccordionItem.displayName = "AccordionItem"
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
const AccordionTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
))
|
));
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
const AccordionContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</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
|
<AlertDialogPrimitive.Overlay
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
||||||
/>
|
/>
|
||||||
@ -49,7 +49,7 @@ const AlertDialogHeader = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -63,7 +63,7 @@ const AlertDialogFooter = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -116,7 +116,7 @@ const AlertDialogCancel = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"mt-2 sm:mt-0",
|
"mt-2 sm:mt-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -134,5 +134,5 @@ export {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel
|
AlertDialogCancel,
|
||||||
};
|
};
|
||||||
|
@ -9,13 +9,13 @@ const alertVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground",
|
default: "bg-background text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default"
|
variant: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
|
@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -13,13 +13,13 @@ const badgeVariants = cva(
|
|||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||||
outline: "text-foreground"
|
outline: "text-foreground",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default"
|
variant: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
|
@ -20,7 +20,7 @@ const BreadcrumbList = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -81,7 +81,8 @@ const BreadcrumbSeparator = ({
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <ChevronRight />}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -95,7 +96,8 @@ const BreadcrumbEllipsis = ({
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
@ -109,5 +111,5 @@ export {
|
|||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis
|
BreadcrumbEllipsis,
|
||||||
};
|
};
|
||||||
|
@ -17,20 +17,20 @@ const buttonVariants = cva(
|
|||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
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: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-9 rounded-md px-3",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-11 rounded-md px-8",
|
||||||
icon: "h-10 w-10"
|
icon: "h-10 w-10",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default"
|
size: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
@ -49,7 +49,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
Button.displayName = "Button";
|
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}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border bg-card text-card-foreground shadow",
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -78,5 +78,5 @@ export {
|
|||||||
CardFooter,
|
CardFooter,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent
|
CardContent,
|
||||||
};
|
};
|
||||||
|
@ -11,11 +11,13 @@ const Checkbox = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
<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" />
|
<Check className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogTitle
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
@ -19,7 +19,7 @@ const Command = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -54,7 +54,7 @@ const CommandInput = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -97,7 +97,7 @@ const CommandGroup = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -125,7 +125,7 @@ const CommandItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -141,7 +141,7 @@ const CommandShortcut = ({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -158,5 +158,5 @@ export {
|
|||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator
|
CommandSeparator,
|
||||||
};
|
};
|
||||||
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -38,9 +38,10 @@ const DialogContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{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">
|
<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" />
|
<X className="h-4 w-4" />
|
||||||
@ -58,7 +59,7 @@ const DialogHeader = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -72,7 +73,7 @@ const DialogFooter = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -87,7 +88,7 @@ const DialogTitle = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -116,5 +117,5 @@ export {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription
|
DialogDescription,
|
||||||
};
|
};
|
||||||
|
@ -26,9 +26,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
className={cn(
|
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",
|
"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",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto" />
|
<ChevronRight className="ml-auto" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
@ -44,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -63,7 +64,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
"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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -82,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
className={cn(
|
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",
|
"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",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -97,10 +98,11 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
checked={checked}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
@ -120,9 +122,10 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
@ -144,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -191,5 +194,5 @@ export {
|
|||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup
|
DropdownMenuRadioGroup,
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
FieldPath,
|
FieldPath,
|
||||||
FieldValues,
|
FieldValues,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext
|
useFormContext,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
@ -16,18 +16,18 @@ const Form = FormProvider;
|
|||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = {
|
> = {
|
||||||
name: TName;
|
name: TName;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue
|
{} as FormFieldContextValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
@ -57,7 +57,7 @@ const useFormField = () => {
|
|||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState
|
...fieldState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ type FormItemContextValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue
|
{} as FormItemContextValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<
|
||||||
@ -156,7 +156,8 @@ const FormMessage = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@ -171,5 +172,5 @@ export {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField
|
FormField,
|
||||||
};
|
};
|
||||||
|
@ -8,13 +8,13 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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",
|
"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}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const labelVariants = cva(
|
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<
|
const Label = React.forwardRef<
|
||||||
|
@ -10,13 +10,13 @@ import {
|
|||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -53,7 +53,7 @@ const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
PhoneInput.displayName = "PhoneInput";
|
PhoneInput.displayName = "PhoneInput";
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ const CountrySelect = ({
|
|||||||
disabled,
|
disabled,
|
||||||
value: selectedCountry,
|
value: selectedCountry,
|
||||||
options: countryList,
|
options: countryList,
|
||||||
onChange
|
onChange,
|
||||||
}: CountrySelectProps) => {
|
}: CountrySelectProps) => {
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
@ -91,7 +91,8 @@ const CountrySelect = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
|
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
|
||||||
disabled={disabled}>
|
disabled={disabled}
|
||||||
|
>
|
||||||
<FlagComponent
|
<FlagComponent
|
||||||
country={selectedCountry}
|
country={selectedCountry}
|
||||||
countryName={selectedCountry}
|
countryName={selectedCountry}
|
||||||
@ -99,7 +100,7 @@ const CountrySelect = ({
|
|||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mr-2 size-4 opacity-50",
|
"-mr-2 size-4 opacity-50",
|
||||||
disabled ? "hidden" : "opacity-100"
|
disabled ? "hidden" : "opacity-100",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -120,7 +121,7 @@ const CountrySelect = ({
|
|||||||
selectedCountry={selectedCountry}
|
selectedCountry={selectedCountry}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null,
|
||||||
)}
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@ -140,7 +141,7 @@ const CountrySelectOption = ({
|
|||||||
country,
|
country,
|
||||||
countryName,
|
countryName,
|
||||||
selectedCountry,
|
selectedCountry,
|
||||||
onChange
|
onChange,
|
||||||
}: CountrySelectOptionProps) => {
|
}: CountrySelectOptionProps) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
|
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
|
||||||
|
@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -26,9 +26,10 @@ const RadioGroupItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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">
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
@ -14,12 +14,14 @@ const ScrollArea = React.forwardRef<
|
|||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("relative overflow-hidden", className)}
|
className={cn("relative overflow-hidden", className)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full rounded-[inherit]",
|
"h-full w-full rounded-[inherit]",
|
||||||
orientation === "horizontal" && "!overflow-x-auto"
|
orientation === "horizontal" && "!overflow-x-auto",
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar orientation={orientation} />
|
<ScrollBar orientation={orientation} />
|
||||||
@ -41,9 +43,10 @@ const ScrollBar = React.forwardRef<
|
|||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
orientation === "horizontal" &&
|
orientation === "horizontal" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
"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.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
));
|
));
|
||||||
|
@ -17,9 +17,10 @@ const SelectTrigger = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
@ -36,9 +37,10 @@ const SelectScrollUpButton = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
));
|
));
|
||||||
@ -52,9 +54,10 @@ const SelectScrollDownButton = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</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",
|
"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" &&
|
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",
|
"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}
|
position={position}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
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}
|
{children}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
@ -111,9 +116,10 @@ const SelectItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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">
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
@ -146,5 +152,5 @@ export {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton
|
SelectScrollDownButton,
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ const Separator = React.forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
ref
|
ref,
|
||||||
) => (
|
) => (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -17,11 +17,11 @@ const Separator = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"shrink-0 bg-border",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
|||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
ref={ref}
|
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",
|
"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",
|
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:
|
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: {
|
defaultVariants: {
|
||||||
side: "right"
|
side: "right",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
@ -61,7 +61,8 @@ const SheetContent = React.forwardRef<
|
|||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(sheetVariants({ side }), className)}
|
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">
|
<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" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
@ -79,7 +80,7 @@ const SheetHeader = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -93,7 +94,7 @@ const SheetFooter = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -134,5 +135,5 @@ export {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription
|
SheetDescription,
|
||||||
};
|
};
|
||||||
|
@ -12,14 +12,14 @@ import {
|
|||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
SheetTitle
|
SheetTitle,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||||
@ -68,7 +68,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [openMobile, setOpenMobile] = React.useState(false);
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
@ -89,7 +89,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
// This sets the cookie to keep the sidebar state.
|
// This sets the cookie to keep the sidebar state.
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
},
|
},
|
||||||
[setOpenProp, open]
|
[setOpenProp, open],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
// Helper to toggle the sidebar.
|
||||||
@ -127,9 +127,17 @@ const SidebarProvider = React.forwardRef<
|
|||||||
isMobile,
|
isMobile,
|
||||||
openMobile,
|
openMobile,
|
||||||
setOpenMobile,
|
setOpenMobile,
|
||||||
toggleSidebar
|
toggleSidebar,
|
||||||
}),
|
}),
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
[
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -140,21 +148,22 @@ const SidebarProvider = React.forwardRef<
|
|||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
"--sidebar-width": SIDEBAR_WIDTH,
|
||||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||||
...style
|
...style,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
|
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
SidebarProvider.displayName = "SidebarProvider";
|
SidebarProvider.displayName = "SidebarProvider";
|
||||||
|
|
||||||
@ -175,7 +184,7 @@ const Sidebar = React.forwardRef<
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
@ -184,10 +193,11 @@ const Sidebar = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
|
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -205,10 +215,11 @@ const Sidebar = React.forwardRef<
|
|||||||
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
|
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
side={side}>
|
side={side}
|
||||||
|
>
|
||||||
<VisuallyHidden asChild>
|
<VisuallyHidden asChild>
|
||||||
<SheetDescription />
|
<SheetDescription />
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
@ -225,7 +236,8 @@ const Sidebar = React.forwardRef<
|
|||||||
data-state={state}
|
data-state={state}
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-side={side}>
|
data-side={side}
|
||||||
|
>
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -234,7 +246,7 @@ const Sidebar = React.forwardRef<
|
|||||||
"group-data-[side=right]:rotate-180",
|
"group-data-[side=right]:rotate-180",
|
||||||
variant === "floating" || variant === "inset"
|
variant === "floating" || variant === "inset"
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
? "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
|
<div
|
||||||
@ -247,18 +259,20 @@ const Sidebar = React.forwardRef<
|
|||||||
variant === "floating" || variant === "inset"
|
variant === "floating" || variant === "inset"
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
? "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",
|
: "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
|
<div
|
||||||
data-sidebar="sidebar"
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
Sidebar.displayName = "Sidebar";
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
@ -279,7 +293,8 @@ const SidebarTrigger = React.forwardRef<
|
|||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
}}
|
}}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</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",
|
"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=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -326,7 +341,7 @@ const SidebarInset = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
"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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -344,7 +359,7 @@ const SidebarInput = React.forwardRef<
|
|||||||
data-sidebar="input"
|
data-sidebar="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus-visible:ring-sidebar-ring h-8 w-full bg-background shadow-none focus-visible:ring-2",
|
"focus-visible:ring-sidebar-ring h-8 w-full bg-background shadow-none focus-visible:ring-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -407,7 +422,7 @@ const SidebarContent = React.forwardRef<
|
|||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -443,7 +458,7 @@ const SidebarGroupLabel = React.forwardRef<
|
|||||||
className={cn(
|
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",
|
"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",
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -466,7 +481,7 @@ const SidebarGroupAction = React.forwardRef<
|
|||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -520,19 +535,19 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
outline:
|
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: {
|
size: {
|
||||||
default: "h-8 text-sm",
|
default: "h-8 text-sm",
|
||||||
sm: "h-7 text-xs",
|
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: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default"
|
size: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const SidebarMenuButton = React.forwardRef<
|
const SidebarMenuButton = React.forwardRef<
|
||||||
@ -553,7 +568,7 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
const { isMobile, state } = useSidebar();
|
const { isMobile, state } = useSidebar();
|
||||||
@ -575,7 +590,7 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
|
|
||||||
if (typeof tooltip === "string") {
|
if (typeof tooltip === "string") {
|
||||||
tooltip = {
|
tooltip = {
|
||||||
children: tooltip
|
children: tooltip,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,7 +605,7 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||||
|
|
||||||
@ -617,7 +632,7 @@ const SidebarMenuAction = React.forwardRef<
|
|||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
showOnHover &&
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -639,7 +654,7 @@ const SidebarMenuBadge = React.forwardRef<
|
|||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -662,7 +677,8 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="menu-skeleton"
|
data-sidebar="menu-skeleton"
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{showIcon && (
|
{showIcon && (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
className="size-4 rounded-md"
|
className="size-4 rounded-md"
|
||||||
@ -674,7 +690,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
data-sidebar="menu-skeleton-text"
|
data-sidebar="menu-skeleton-text"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--skeleton-width": width
|
"--skeleton-width": width,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -693,7 +709,7 @@ const SidebarMenuSub = React.forwardRef<
|
|||||||
className={cn(
|
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",
|
"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",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -728,7 +744,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
size === "sm" && "text-xs",
|
size === "sm" && "text-xs",
|
||||||
size === "md" && "text-sm",
|
size === "md" && "text-sm",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -760,5 +776,5 @@ export {
|
|||||||
SidebarRail,
|
SidebarRail,
|
||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar
|
useSidebar,
|
||||||
};
|
};
|
||||||
|
@ -9,13 +9,14 @@ const Switch = React.forwardRef<
|
|||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
ref={ref}>
|
ref={ref}
|
||||||
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
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>
|
</SwitchPrimitives.Root>
|
||||||
|
@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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]",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -88,7 +88,7 @@ const TableCell = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -115,5 +115,5 @@ export {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCaption
|
TableCaption,
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
|||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
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",
|
"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}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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]",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -28,13 +28,13 @@ const toastVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "border bg-background text-foreground",
|
default: "border bg-background text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground"
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default"
|
variant: "default",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
@ -60,7 +60,7 @@ const ToastAction = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -75,10 +75,11 @@ const ToastClose = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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=""
|
toast-close=""
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
));
|
));
|
||||||
@ -121,5 +122,5 @@ export {
|
|||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction
|
ToastAction,
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport
|
ToastViewport,
|
||||||
} from "@/components/ui/toast";
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
|
@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
|
|||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export const AppName = "SwagShop"
|
export const AppName = "SwagShop";
|
||||||
export const AdminAppName = "SwagShop Admin"
|
export const AdminAppName = "SwagShop Admin";
|
||||||
|
@ -11,7 +11,7 @@ interface FontContextType {
|
|||||||
const FontContext = createContext<FontContextType | undefined>(undefined);
|
const FontContext = createContext<FontContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [font, _setFont] = useState<Font>(() => {
|
const [font, _setFont] = useState<Font>(() => {
|
||||||
const savedFont = localStorage.getItem("font");
|
const savedFont = localStorage.getItem("font");
|
||||||
|
@ -15,7 +15,7 @@ type ThemeProviderState = {
|
|||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
setTheme: () => null
|
setTheme: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
@ -27,7 +27,7 @@ export function ThemeProvider({
|
|||||||
...props
|
...props
|
||||||
}: ThemeProviderProps) {
|
}: ThemeProviderProps) {
|
||||||
const [theme, _setTheme] = useState<Theme>(
|
const [theme, _setTheme] = useState<Theme>(
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -61,7 +61,7 @@ export function ThemeProvider({
|
|||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
setTheme
|
setTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,17 +4,11 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { handleServerError } from "@/utils/handle-server-error";
|
import { handleServerError } from "@/utils/handle-server-error";
|
||||||
import { ApiError } from "@/errors/api-error";
|
import { ApiError } from "@/errors/api-error";
|
||||||
import {
|
import { ShopLoginAccessTokenData, UserPublic } from "@/client";
|
||||||
DashboardService,
|
|
||||||
LoginService,
|
|
||||||
ShopLoginAccessTokenData,
|
|
||||||
UserPublic,
|
|
||||||
UserRegister,
|
|
||||||
UserService,
|
|
||||||
UserUpdate
|
|
||||||
} from "@/client";
|
|
||||||
import { toast } from "./useToast";
|
import { toast } from "./useToast";
|
||||||
|
|
||||||
|
import { authAPI } from "@/api/api";
|
||||||
|
|
||||||
const isLoggedIn = () => {
|
const isLoggedIn = () => {
|
||||||
return localStorage.getItem("access_token") !== null;
|
return localStorage.getItem("access_token") !== null;
|
||||||
};
|
};
|
||||||
@ -24,27 +18,22 @@ const useAuth = () => {
|
|||||||
const [loggedIn, setLoggedIn] = useState(isLoggedIn());
|
const [loggedIn, setLoggedIn] = useState(isLoggedIn());
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: user } = useQuery<UserPublic | null, Error>({
|
const { data: user } = useQuery<UserPublic | null, Error>({
|
||||||
queryKey: ["currentUser"],
|
queryKey: ["currentUser"],
|
||||||
queryFn: DashboardService.userGetUser,
|
queryFn: authAPI.getCurrentUser,
|
||||||
enabled: loggedIn
|
enabled: loggedIn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const signUpMutation = useMutation({
|
const signUpMutation = useMutation({
|
||||||
mutationFn: (data: UserRegister) =>
|
mutationFn: authAPI.registerUser,
|
||||||
DashboardService.userRegister({ requestBody: data }),
|
|
||||||
onSuccess: () => navigate({ to: "/sign-in" }),
|
onSuccess: () => navigate({ to: "/sign-in" }),
|
||||||
onError: (err: ApiError) => handleServerError(err),
|
onError: (err: ApiError) => handleServerError(err),
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] })
|
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const login = async (data: ShopLoginAccessTokenData) => {
|
const login = async (data: ShopLoginAccessTokenData) => {
|
||||||
const response = await LoginService.dashboardLoginAccessToken({
|
const response = await authAPI.loginUser(data);
|
||||||
formData: {
|
|
||||||
username: data.formData.username,
|
|
||||||
password: data.formData.password
|
|
||||||
}
|
|
||||||
});
|
|
||||||
localStorage.setItem("access_token", response.access_token);
|
localStorage.setItem("access_token", response.access_token);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||||
@ -53,7 +42,7 @@ const useAuth = () => {
|
|||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: login,
|
mutationFn: login,
|
||||||
onSuccess: () => navigate({ to: "/" }),
|
onSuccess: () => navigate({ to: "/" }),
|
||||||
onError: (err: ApiError) => handleServerError(err)
|
onError: (err: ApiError) => handleServerError(err),
|
||||||
});
|
});
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
@ -64,15 +53,26 @@ const useAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAccountMutation = useMutation({
|
const updateAccountMutation = useMutation({
|
||||||
mutationFn: (data: UserUpdate) =>
|
mutationFn: authAPI.updateUser,
|
||||||
UserService.userUpdateUser({ requestBody: data }),
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({ title: "Account updated successfully" });
|
toast({ title: "Account updated successfully" });
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
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(() => {
|
useEffect(() => {
|
||||||
const handleStorageChange = (event: StorageEvent) => {
|
const handleStorageChange = (event: StorageEvent) => {
|
||||||
if (event.key === "access_token") {
|
if (event.key === "access_token") {
|
||||||
@ -97,7 +97,7 @@ const useAuth = () => {
|
|||||||
logout,
|
logout,
|
||||||
user,
|
user,
|
||||||
error,
|
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">()
|
* @example const [open, setOpen] = useDialogState<"approve" | "reject">()
|
||||||
*/
|
*/
|
||||||
export default function useDialogState<T extends string | boolean>(
|
export default function useDialogState<T extends string | boolean>(
|
||||||
initialState: T | null = null
|
initialState: T | null = null,
|
||||||
) {
|
) {
|
||||||
const [open, _setOpen] = useState<T | null>(initialState);
|
const [open, _setOpen] = useState<T | null>(initialState);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const MOBILE_BREAKPOINT = 768;
|
|||||||
|
|
||||||
export function useIsMobile() {
|
export function useIsMobile() {
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
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 };
|
||||||
|
}
|
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