Compare commits

...

10 Commits

259 changed files with 13049 additions and 5333 deletions

View File

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

View File

@ -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
View 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
View 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",

View File

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

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -1 +1,2 @@
VITE_API_URL= VITE_API_URL=http://localhost:8000
VITE_USE_MOCK_API=true

14
frontend/Dockerfile Normal file
View 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
View 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
View 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",

View File

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

File diff suppressed because it is too large Load Diff

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

View 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,
});
},
};

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

View 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); // 011 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,
);
}

View 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();

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

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

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

View 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(),
});
},
};

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

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

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

View 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 });
},
};

View 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

View File

@ -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,10 +8,14 @@ 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;

View File

@ -6,13 +6,13 @@ 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;

View File

@ -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,12 +112,12 @@ 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 {

View File

@ -1,5 +1,5 @@
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>;
@ -26,7 +26,7 @@ export class Interceptors<T> {
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,14 +41,14 @@ 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(),

View File

@ -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, [key, value]) => ({
...headers, ...headers,
[key]: String(value), [key]: String(value),
}), {} as Record<string, string>); }),
{} 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 = {

View File

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

View File

@ -1,9 +1,31 @@
// 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 {
/** /**
@ -13,8 +35,8 @@ export class DashboardService {
*/ */
public static userGetUser(): CancelablePromise<UserGetUserResponse> { public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: "GET",
url: '/user' url: "/user",
}); });
} }
@ -25,15 +47,17 @@ export class DashboardService {
* @returns boolean Successful Response * @returns boolean Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> { public static userUpdateUser(
data: UserUpdateUserData,
): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: "PUT",
url: '/user', url: "/user",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -44,15 +68,17 @@ export class DashboardService {
* @returns boolean Successful Response * @returns boolean Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> { public static userRegister(
data: UserRegisterData,
): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/user', url: "/user",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -63,8 +89,8 @@ export class DashboardService {
*/ */
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> { public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/user' url: "/user",
}); });
} }
@ -86,15 +112,17 @@ export class DashboardService {
* @returns Token Successful Response * @returns Token Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> { public static dashboardLoginAccessToken(
data: DashboardLoginAccessTokenData,
): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/login/access-token', url: "/login/access-token",
formData: data.formData, formData: data.formData,
mediaType: 'application/x-www-form-urlencoded', mediaType: "application/x-www-form-urlencoded",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -105,18 +133,19 @@ export class DashboardService {
* @returns boolean Successful Response * @returns boolean Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static dashboardRegisterNewShop(data: DashboardRegisterNewShopData): CancelablePromise<DashboardRegisterNewShopResponse> { public static dashboardRegisterNewShop(
data: DashboardRegisterNewShopData,
): CancelablePromise<DashboardRegisterNewShopResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/shop', url: "/shop",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
} }
export class LoginService { export class LoginService {
@ -138,15 +167,17 @@ export class LoginService {
* @returns Token Successful Response * @returns Token Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> { public static dashboardLoginAccessToken(
data: DashboardLoginAccessTokenData,
): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/login/access-token', url: "/login/access-token",
formData: data.formData, formData: data.formData,
mediaType: 'application/x-www-form-urlencoded', mediaType: "application/x-www-form-urlencoded",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -158,18 +189,19 @@ export class LoginService {
* @returns Token Successful Response * @returns Token Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> { public static shopLoginAccessToken(
data: ShopLoginAccessTokenData,
): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/shop/{shop_uuid}/login/access-token', url: "/shop/{shop_uuid}/login/access-token",
formData: data.formData, formData: data.formData,
mediaType: 'application/x-www-form-urlencoded', mediaType: "application/x-www-form-urlencoded",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
} }
export class ShopService { export class ShopService {
@ -181,15 +213,17 @@ export class ShopService {
* @returns Token Successful Response * @returns Token Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> { public static shopLoginAccessToken(
data: ShopLoginAccessTokenData,
): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/shop/{shop_uuid}/login/access-token', url: "/shop/{shop_uuid}/login/access-token",
formData: data.formData, formData: data.formData,
mediaType: 'application/x-www-form-urlencoded', mediaType: "application/x-www-form-urlencoded",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -200,16 +234,18 @@ export class ShopService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> { public static shopDeleteUser(
data: ShopDeleteUserData,
): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/shop/{shop_uuid}/user/delete', url: "/shop/{shop_uuid}/user/delete",
path: { path: {
shop_uuid: data.shopUuid shop_uuid: data.shopUuid,
}, },
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -220,8 +256,8 @@ export class ShopService {
*/ */
public static shopLogout(): CancelablePromise<ShopLogoutResponse> { public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/shop/{shop_uuid}/user/logout' url: "/shop/{shop_uuid}/user/logout",
}); });
} }
@ -233,18 +269,20 @@ export class ShopService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> { public static shopRegister(
data: ShopRegisterData,
): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/shop/{shop_uuid}/user/register', url: "/shop/{shop_uuid}/user/register",
path: { path: {
shop_uuid: data.shopUuid shop_uuid: data.shopUuid,
}, },
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -255,18 +293,19 @@ export class ShopService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> { public static shopUpdateUser(
data: ShopUpdateUserData,
): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: "PUT",
url: '/shop/{shop_uuid}/user/update', url: "/shop/{shop_uuid}/user/update",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
} }
export class UserService { export class UserService {
@ -277,8 +316,8 @@ export class UserService {
*/ */
public static userGetUser(): CancelablePromise<UserGetUserResponse> { public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: "GET",
url: '/user' url: "/user",
}); });
} }
@ -289,15 +328,17 @@ export class UserService {
* @returns boolean Successful Response * @returns boolean Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> { public static userUpdateUser(
data: UserUpdateUserData,
): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: "PUT",
url: '/user', url: "/user",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -308,15 +349,17 @@ export class UserService {
* @returns boolean Successful Response * @returns boolean Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> { public static userRegister(
data: UserRegisterData,
): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/user', url: "/user",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -327,8 +370,8 @@ export class UserService {
*/ */
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> { public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/user' url: "/user",
}); });
} }
@ -339,16 +382,18 @@ export class UserService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> { public static shopDeleteUser(
data: ShopDeleteUserData,
): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/shop/{shop_uuid}/user/delete', url: "/shop/{shop_uuid}/user/delete",
path: { path: {
shop_uuid: data.shopUuid shop_uuid: data.shopUuid,
}, },
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -359,8 +404,8 @@ export class UserService {
*/ */
public static shopLogout(): CancelablePromise<ShopLogoutResponse> { public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: "DELETE",
url: '/shop/{shop_uuid}/user/logout' url: "/shop/{shop_uuid}/user/logout",
}); });
} }
@ -372,18 +417,20 @@ export class UserService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> { public static shopRegister(
data: ShopRegisterData,
): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: "POST",
url: '/shop/{shop_uuid}/user/register', url: "/shop/{shop_uuid}/user/register",
path: { path: {
shop_uuid: data.shopUuid shop_uuid: data.shopUuid,
}, },
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
@ -394,18 +441,19 @@ export class UserService {
* @returns unknown Successful Response * @returns unknown Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> { public static shopUpdateUser(
data: ShopUpdateUserData,
): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: "PUT",
url: '/shop/{shop_uuid}/user/update', url: "/shop/{shop_uuid}/user/update",
body: data.requestBody, body: data.requestBody,
mediaType: 'application/json', mediaType: "application/json",
errors: { errors: {
422: 'Validation Error' 422: "Validation Error",
} },
}); });
} }
} }
export class UtilsService { export class UtilsService {
@ -417,8 +465,8 @@ export class UtilsService {
*/ */
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/",
}); });
} }
@ -430,9 +478,8 @@ export class UtilsService {
*/ */
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/",
}); });
} }
} }

View File

@ -1,21 +1,21 @@
// 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 = {
@ -48,8 +48,8 @@ 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;
}; };
@ -67,67 +67,67 @@ export type UserRegister = {
}; };
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: {
@ -135,8 +135,8 @@ export type ShopUpdateUserData = {
}; };
}; };
export type ShopUpdateUserResponse = (unknown); export type ShopUpdateUserResponse = unknown;
export type UtilsHealthCheckResponse = (boolean); export type UtilsHealthCheckResponse = boolean;
export type UtilsTestDbResponse = (boolean); export type UtilsTestDbResponse = boolean;

View File

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

View File

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

View 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",
];

View File

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

View File

@ -1,21 +1,14 @@
import { import
IconBrowserCheck, {
IconBuildingStore, IconBuildingStore,
IconCoin, IconClipboardCheckFilled,
IconForklift, IconCoin, IconLayoutDashboard, IconPackage,
IconHelp,
IconLayoutDashboard,
IconNotification,
IconPackage,
IconPalette, IconPalette,
IconPercentage,
IconSettings, IconSettings,
IconTag, IconTag,
IconTool,
IconUserCog, IconUserCog,
IconUsers IconUsers
} from "@tabler/icons-react"; } 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 ],
}
]
}
]
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

@ -1,10 +1,10 @@
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>,
@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef<
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>,
@ -27,7 +27,7 @@ const AccordionTrigger = React.forwardRef<
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}
> >
@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef<
<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>,
@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef<
> >
<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 };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -1,2 +1,2 @@
export const AppName = "SwagShop" export const AppName = "SwagShop";
export const AdminAppName = "SwagShop Admin" export const AdminAppName = "SwagShop Admin";

View File

@ -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");

View File

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

View File

@ -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),
}; };
}; };

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

View 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,
});
}

View File

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

View File

@ -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(() => {

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

View File

@ -0,0 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { productsAPI } from "@/api/api";
import { ProductWithDetails } from "@/api/mock/models";
export function useProducts() {
const query = useQuery<ProductWithDetails[]>({
queryKey: ["products"],
queryFn: productsAPI.getProductsForShop,
});
return query;
}

Some files were not shown because too many files have changed in this diff Show More