Compare commits

...

10 Commits

259 changed files with 13049 additions and 5333 deletions

View File

@ -1,15 +1,55 @@
# SHOP API
Simple API (still WIP)
# Swag Shop
## Requires:
1. Redis
Simple redis installation, no further configuration needed
An e-commerence multitenant shop app.
2. MariaDB (or MySQL)
Make sure a proper database is set up with `shop.sql`
## Core idea
This app provides a dashboard for managing individual shop data like products and similar things. Then it's up to the tenant to craft their own frontend and make use of the API.
## Running
### Backend
1. Dependencies
Straight forward, you need to install dependencies via *[uv](https://docs.astral.sh/uv/)*
## Running:
Gunicorn is the simplest way to run this project
```sh
gunicorn -w 4 -b HOST:PORT main:app
```
cd backend
uv sync
```
2. Fill out `.env`
You can use `.env.example` as a reference
3. Run it!
```
fastapi dev backend/app/main.py
```
### Frontend
1. Dependencies
Using npm (or your favorite package manager)
```
cd frontend
npm install
```
2. Fill out `.env`
As with backend, there is `.env.example`
3. Run it!
There is a dev package script
```
npm run dev
```
<br>
- For deployment a `docker-compose.yml` is provided
## Tech Stack
- **PostgreSQL**
- **Backend** - *FastAPI* + *SQLModel*
- **Frontend** - *Vite* + *Typescript* + *ShadcnUI* + *Tanstack Query*
## Future plans
- True API integration
- Audit log
- Stripe integration

View File

@ -1,25 +1,41 @@
# Base Image
FROM python:3.13
FROM python:3.12
RUN pip install poetry
# Environment variables
ENV PYTHONUNBUFFERED=1
# Set working directory
WORKDIR /app/
# Copy dependency files first to leverage caching
COPY pyproject.toml poetry.lock /app/
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
# Place executables in the environment at the front of the path
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
ENV PATH="/app/.venv/bin:$PATH"
# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1
# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy
# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project
ENV PYTHONPATH=/app
COPY ./pyproject.toml ./uv.lock /app/
# Copy the rest of the application
COPY ./app /app/app
# Ensure dependencies are installed correctly
RUN poetry install --no-interaction --no-ansi --without dev
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync
# Expose port for FastAPI
EXPOSE 8000
# Command to run the app
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]

261
currency_names.txt Normal file
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:
backend:
build:
context: ./backend
container_name: swagshop-backend
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "8000:8000"
frontend:
build:
context: ./frontend/
container_name: swagshop-frontend
env_file:
- ./frontend/.env
ports:
- "5173:5173"
db:
image: postgres:12
restart: no
image: postgres:16
container_name: swagshop-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: swagshop
volumes:
- app-db-data:/var/lib/postgresql/data/pgdata
env_file:
- .env
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
adminer:
image: adminer
restart: no
- pgdata:/var/lib/postgresql/data
networks:
- default
depends_on:
- db
environment:
- ADMINER_DESIGN=pepa-linha-dark
backend:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
restart: no
networks:
- default
depends_on:
db:
condition: service_healthy
env_file:
- .env
environment:
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
- ENVIRONMENT=${ENVIRONMENT}
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
- SECRET_KEY=${SECRET_KEY?Variable not set}
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
- SMTP_HOST=${SMTP_HOST}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
- MYSQL_SERVER=db
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_DB=${MYSQL_DB}
- MYSQL_USER=${MYSQL_USER?Variable not set}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
interval: 10s
timeout: 5s
retries: 5
build:
context: ./backend
- internal
volumes:
app-db-data:
pgdata:
networks:
internal:
driver: bridge

2
frontend/.dockerignore Normal file
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",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"format:check": "prettier --check .",
"format": "prettier --write .",
"knip": "knip",
"generate-client": "openapi-ts"
"generate-client": "openapi-ts",
"format:write": "biome format ./src/ --write",
"format:check": "biome check ./src/"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
@ -29,14 +29,14 @@
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@tabler/icons-react": "^3.24.0",
"@tanstack/react-query": "^5.62.3",
"@tanstack/react-query": "^5.72.2",
"@tanstack/react-router": "^1.86.1",
"@tanstack/react-table": "^8.20.5",
"@vitejs/plugin-react": "^4.3.4",
@ -45,7 +45,9 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^3.6.0",
"dexie": "^4.0.11",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-day-picker": "^9.6.0",
@ -55,10 +57,12 @@
"recharts": "^2.14.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^9.3.0",
"@hey-api/client-axios": "^0.6.2",
@ -85,6 +89,7 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0"
"vite": "^6.1.0",
"vite-plugin-svgr": "^4.3.0"
}
}

6157
frontend/pnpm-lock.yaml generated Normal file

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 { ApiResult } from './ApiResult';
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
export class ApiError extends Error {
public readonly url: string;
@ -8,10 +8,14 @@ export class ApiError extends Error {
public readonly body: unknown;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message);
this.name = 'ApiError';
this.name = "ApiError";
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;

View File

@ -6,13 +6,13 @@ export type ApiRequestOptions<T = unknown> = {
readonly headers?: Record<string, unknown>;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT";
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;

View File

@ -1,7 +1,7 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
this.name = "CancelError";
}
public get isCancelled(): boolean {
@ -30,8 +30,8 @@ export class CancelablePromise<T> implements Promise<T> {
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel
) => void
onCancel: OnCancel,
) => void,
) {
this._isResolved = false;
this._isRejected = false;
@ -64,15 +64,15 @@ export class CancelablePromise<T> implements Promise<T> {
this.cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
Object.defineProperty(onCancel, "isResolved", {
get: (): boolean => this._isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
Object.defineProperty(onCancel, "isRejected", {
get: (): boolean => this._isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
Object.defineProperty(onCancel, "isCancelled", {
get: (): boolean => this._isCancelled,
});
@ -86,13 +86,13 @@ export class CancelablePromise<T> implements Promise<T> {
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}
@ -112,12 +112,12 @@ export class CancelablePromise<T> implements Promise<T> {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
console.warn("Cancellation threw an error", error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError('Request aborted'));
if (this._reject) this._reject(new CancelError("Request aborted"));
}
public get isCancelled(): boolean {

View File

@ -1,5 +1,5 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { AxiosRequestConfig, AxiosResponse } from "axios";
import type { ApiRequestOptions } from "./ApiRequestOptions";
type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
@ -26,7 +26,7 @@ export class Interceptors<T> {
export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
CREDENTIALS: "include" | "omit" | "same-origin";
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
@ -41,14 +41,14 @@ export type OpenAPIConfig = {
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
CREDENTIALS: 'include',
BASE: "",
CREDENTIALS: "include",
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: '0.0.1',
VERSION: "0.0.1",
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),

View File

@ -1,19 +1,24 @@
import axios from 'axios';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
import axios from "axios";
import type {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
AxiosInstance,
} from "axios";
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export const isString = (value: unknown): value is string => {
return typeof value === 'string';
return typeof value === "string";
};
export const isStringWithValue = (value: unknown): value is string => {
return isString(value) && value !== '';
return isString(value) && value !== "";
};
export const isBlob = (value: any): value is Blob => {
@ -33,7 +38,7 @@ export const base64 = (str: string): string => {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
return Buffer.from(str).toString("base64");
}
};
@ -52,8 +57,8 @@ export const getQueryString = (params: Record<string, unknown>): string => {
if (value instanceof Date) {
append(key, value.toISOString());
} else if (Array.isArray(value)) {
value.forEach(v => encodePair(key, v));
} else if (typeof value === 'object') {
value.forEach((v) => encodePair(key, v));
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
} else {
append(key, value);
@ -62,14 +67,14 @@ export const getQueryString = (params: Record<string, unknown>): string => {
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
return qs.length ? `?${qs.join('&')}` : '';
return qs.length ? `?${qs.join("&")}` : "";
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
@ -81,7 +86,9 @@ const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
return options.query ? url + getQueryString(options.query) : url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
export const getFormData = (
options: ApiRequestOptions,
): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
@ -97,7 +104,7 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
.filter(([, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
@ -110,14 +117,20 @@ export const getFormData = (options: ApiRequestOptions): FormData | undefined =>
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
export const resolve = async <T>(
options: ApiRequestOptions<T>,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): Promise<Record<string, string>> => {
export const getHeaders = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
): Promise<Record<string, string>> => {
const [token, username, password, additionalHeaders] = await Promise.all([
// @ts-ignore
resolve(options, config.TOKEN),
@ -130,38 +143,41 @@ export const getHeaders = async <T>(config: OpenAPIConfig, options: ApiRequestOp
]);
const headers = Object.entries({
Accept: 'application/json',
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([, value]) => value !== undefined && value !== null)
.reduce((headers, [key, value]) => ({
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
}),
{} as Record<string, string>,
);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
headers["Authorization"] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
headers["Authorization"] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
headers["Content-Type"] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
headers["Content-Type"] = options.body.type || "application/octet-stream";
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
headers["Content-Type"] = "text/plain";
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
headers["Content-Type"] = "application/json";
}
} else if (options.formData !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
headers["Content-Type"] = options.mediaType;
}
}
@ -183,7 +199,7 @@ export const sendRequest = async <T>(
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance
axiosClient: AxiosInstance,
): Promise<AxiosResponse<T>> => {
const controller = new AbortController();
@ -213,7 +229,10 @@ export const sendRequest = async <T>(
}
};
export const getResponseHeader = (response: AxiosResponse<unknown>, responseHeader?: string): string | undefined => {
export const getResponseHeader = (
response: AxiosResponse<unknown>,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
@ -230,50 +249,53 @@ export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
export const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult,
): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'Im a teapot',
421: 'Misdirected Request',
422: 'Unprocessable Content',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "Im a teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
...options.errors,
}
};
const error = errors[result.status];
if (error) {
@ -281,8 +303,8 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorStatus = result.status ?? "unknown";
const errorStatusText = result.statusText ?? "unknown";
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
@ -291,8 +313,10 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
);
}
};
@ -305,7 +329,11 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
axiosClient: AxiosInstance = axios,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
@ -314,18 +342,30 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>,
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
let response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
let response = await sendRequest<T>(
config,
options,
url,
body,
formData,
headers,
onCancel,
axiosClient,
);
for (const fn of config.interceptors.response._fns) {
response = await fn(response);
}
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const responseHeader = getResponseHeader(
response,
options.responseHeader,
);
let transformedBody = responseBody;
if (options.responseTransformer && isSuccess(response.status)) {
transformedBody = await options.responseTransformer(responseBody)
transformedBody = await options.responseTransformer(responseBody);
}
const result: ApiResult = {

View File

@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
export * from './sdk.gen';
export * from './types.gen';
export { ApiError } from "./core/ApiError";
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI";
export * from "./sdk.gen";
export * from "./types.gen";

View File

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

View File

@ -1,21 +1,21 @@
// This file is auto-generated by @hey-api/openapi-ts
export type Body_Dashboard_login_access_token = {
grant_type?: (string | null);
grant_type?: string | null;
username: string;
password: string;
scope?: string;
client_id?: (string | null);
client_secret?: (string | null);
client_id?: string | null;
client_secret?: string | null;
};
export type Body_Shop_login_access_token = {
grant_type?: (string | null);
grant_type?: string | null;
username: string;
password: string;
scope?: string;
client_id?: (string | null);
client_secret?: (string | null);
client_id?: string | null;
client_secret?: string | null;
};
export type HTTPValidationError = {
@ -48,8 +48,8 @@ export type UserPublic = {
uuid: string;
username: string;
email: string;
first_name: (string | null);
last_name: (string | null);
first_name: string | null;
last_name: string | null;
phone_number: string;
};
@ -67,67 +67,67 @@ export type UserRegister = {
};
export type UserUpdate = {
email: (string | null);
phone_number: (string | null);
username: (string | null);
first_name?: (string | null);
last_name?: (string | null);
email: string | null;
phone_number: string | null;
username: string | null;
first_name?: string | null;
last_name?: string | null;
};
export type ValidationError = {
loc: Array<(string | number)>;
loc: Array<string | number>;
msg: string;
type: string;
};
export type UserGetUserResponse = (UserPublic);
export type UserGetUserResponse = UserPublic;
export type UserUpdateUserData = {
requestBody: UserUpdate;
};
export type UserUpdateUserResponse = (boolean);
export type UserUpdateUserResponse = boolean;
export type UserRegisterData = {
requestBody: UserRegister;
};
export type UserRegisterResponse = (boolean);
export type UserRegisterResponse = boolean;
export type UserDeleteUserResponse = (boolean);
export type UserDeleteUserResponse = boolean;
export type DashboardLoginAccessTokenData = {
formData: Body_Dashboard_login_access_token;
};
export type DashboardLoginAccessTokenResponse = (Token);
export type DashboardLoginAccessTokenResponse = Token;
export type DashboardRegisterNewShopData = {
requestBody: ShopCreate;
};
export type DashboardRegisterNewShopResponse = (boolean);
export type DashboardRegisterNewShopResponse = boolean;
export type ShopLoginAccessTokenData = {
formData: Body_Shop_login_access_token;
};
export type ShopLoginAccessTokenResponse = (Token);
export type ShopLoginAccessTokenResponse = Token;
export type ShopDeleteUserData = {
shopUuid: unknown;
};
export type ShopDeleteUserResponse = (unknown);
export type ShopDeleteUserResponse = unknown;
export type ShopLogoutResponse = (unknown);
export type ShopLogoutResponse = unknown;
export type ShopRegisterData = {
requestBody: UserRegister;
shopUuid: unknown;
};
export type ShopRegisterResponse = (unknown);
export type ShopRegisterResponse = unknown;
export type ShopUpdateUserData = {
requestBody: {
@ -135,8 +135,8 @@ export type ShopUpdateUserData = {
};
};
export type ShopUpdateUserResponse = (unknown);
export type ShopUpdateUserResponse = unknown;
export type UtilsHealthCheckResponse = (boolean);
export type UtilsHealthCheckResponse = boolean;
export type UtilsTestDbResponse = (boolean);
export type UtilsTestDbResponse = boolean;

View File

@ -4,7 +4,7 @@ import {
IconArrowRightDashed,
IconDeviceLaptop,
IconMoon,
IconSun
IconSun,
} from "@tabler/icons-react";
import { useSearch } from "@/context/search-context";
import { useTheme } from "@/context/theme-context";
@ -15,7 +15,7 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandSeparator
CommandSeparator,
} from "@/components/ui/command";
import { sidebarData } from "./layout/data/sidebar-data";
import { ScrollArea } from "./ui/scroll-area";
@ -30,7 +30,7 @@ export function CommandMenu() {
setOpen(false);
command();
},
[setOpen]
[setOpen],
);
return (
@ -49,7 +49,8 @@ export function CommandMenu() {
value={navItem.title}
onSelect={() => {
runCommand(() => navigate({ to: navItem.url }));
}}>
}}
>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
</div>
@ -63,7 +64,8 @@ export function CommandMenu() {
value={subItem.title}
onSelect={() => {
runCommand(() => navigate({ to: subItem.url }));
}}>
}}
>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
</div>

View File

@ -6,7 +6,7 @@ import {
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
@ -56,7 +56,8 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
<Button
variant={destructive ? "destructive" : "default"}
onClick={handleConfirm}
disabled={disabled || isLoading}>
disabled={disabled || isLoading}
>
{confirmText ?? "Continue"}
</Button>
</AlertDialogFooter>

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 {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarRail
SidebarRail,
} from "@/components/ui/sidebar";
import { NavGroup } from "@/components/layout/nav-group";
import { sidebarData } from "./data/sidebar-data";
import { cn } from "@/lib/utils";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" variant="floating" {...props}>
<SidebarHeader>
<h1
className={cn(
"header-fixed peer/header flex h-16 w-[inherit] items-center gap-3 rounded-md bg-background p-4 text-xl font-bold sm:gap-4"
)}
{...props}>
SwagShop
</h1>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((props) => (
<NavGroup key={props.title} {...props} />
))}
</SidebarContent>
{/* <SidebarFooter>
<NavUser user={sidebarData.user} />
</SidebarFooter> */}
<SidebarRail />
</Sidebar>
);

View File

@ -1,21 +1,14 @@
import {
IconBrowserCheck,
import
{
IconBuildingStore,
IconCoin,
IconForklift,
IconHelp,
IconLayoutDashboard,
IconNotification,
IconPackage,
IconClipboardCheckFilled,
IconCoin, IconLayoutDashboard, IconPackage,
IconPalette,
IconPercentage,
IconSettings,
IconTag,
IconTool,
IconUserCog,
IconUsers
} from "@tabler/icons-react";
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
import { type SidebarData } from "../types";
export const sidebarData: SidebarData = {
@ -26,45 +19,40 @@ export const sidebarData: SidebarData = {
{
title: "Dashboard",
url: "/dashboard",
icon: IconLayoutDashboard
icon: IconLayoutDashboard,
},
{
title: "Shop",
url: "/dashboard/shop",
icon: IconBuildingStore
icon: IconBuildingStore,
},
{
title: "Products",
url: "/dashboard/products",
icon: IconPackage
},
{
title: "Inventory",
url: "/dashboard/tasks",
icon: IconForklift
icon: IconPackage,
},
{
title: "Sales",
icon: IconCoin,
items: [
{
title: "Discounts",
url: "/dashboard/sales/discounts",
icon: IconPercentage
title: "Recent sales",
url: "/dashboard/sales/recent-sales",
icon: IconClipboardCheckFilled,
},
{
title: "Coupons",
url: "/dashboard/sales/coupons",
icon: IconTag
}
]
icon: IconTag,
},
],
},
{
title: "Customers",
url: "/dashboard/users",
icon: IconUsers
}
]
icon: IconUsers,
},
],
},
{
title: "Other",
@ -76,36 +64,17 @@ export const sidebarData: SidebarData = {
{
title: "Profile",
url: "/dashboard/settings",
icon: IconUserCog
},
{
title: "Account",
url: "/dashboard/settings/account",
icon: IconTool
icon: IconUserCog,
},
{
title: "Appearance",
url: "/dashboard/settings/appearance",
icon: IconPalette
},
{
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: IconNotification
},
{
title: "Display",
url: "/dashboard/settings/display",
icon: IconBrowserCheck
icon: IconPalette,
}
]
],
},
{
title: "Help Center",
url: "/help-center",
icon: IconHelp
}
]
}
]
],
},
],
};

View File

@ -34,9 +34,10 @@ export const Header = ({
"flex h-16 items-center gap-3 bg-background p-4 sm:gap-4",
fixed && "header-fixed peer/header fixed z-50 w-[inherit] rounded-md",
offset > 10 && fixed ? "shadow" : "shadow-none",
className
className,
)}
{...props}>
{...props}
>
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
<Separator orientation="vertical" className="h-6" />
{children}

View File

@ -12,7 +12,7 @@ export const Main = ({ fixed, ...props }: MainProps) => {
className={cn(
"peer-[.header-fixed]/header:mt-16",
"px-4 py-6",
fixed && "fixed-main flex flex-grow flex-col overflow-hidden"
fixed && "fixed-main flex flex-grow flex-col overflow-hidden",
)}
{...props}
/>

View File

@ -4,7 +4,7 @@ import { ChevronRight } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarGroup,
@ -15,7 +15,7 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar
useSidebar,
} from "@/components/ui/sidebar";
import { Badge } from "../ui/badge";
import {
@ -24,7 +24,7 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
@ -64,7 +64,8 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
<SidebarMenuButton
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}>
tooltip={item.title}
>
<Link to={item.url} onClick={() => setOpenMobile(false)}>
{item.icon && <item.icon />}
<span>{item.title}</span>
@ -77,7 +78,7 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
const SidebarMenuCollapsible = ({
item,
href
href,
}: {
item: NavCollapsible;
href: string;
@ -87,7 +88,8 @@ const SidebarMenuCollapsible = ({
<Collapsible
asChild
defaultOpen={checkIsActive(href, item, true)}
className="group/collapsible">
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
@ -103,7 +105,8 @@ const SidebarMenuCollapsible = ({
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={checkIsActive(href, subItem)}>
isActive={checkIsActive(href, subItem)}
>
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
{subItem.icon && <subItem.icon />}
<span>{subItem.title}</span>
@ -121,7 +124,7 @@ const SidebarMenuCollapsible = ({
const SidebarMenuCollapsedDropdown = ({
item,
href
href,
}: {
item: NavCollapsible;
href: string;
@ -132,7 +135,8 @@ const SidebarMenuCollapsedDropdown = ({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={checkIsActive(href, item)}>
isActive={checkIsActive(href, item)}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
@ -148,7 +152,8 @@ const SidebarMenuCollapsedDropdown = ({
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
<Link
to={sub.url}
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}>
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}
>
{sub.icon && <sub.icon />}
<span className="max-w-52 text-wrap">{sub.title}</span>
{sub.badge && (

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,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
useSidebar,
} from "@/components/ui/sidebar";
export function TeamSwitcher({
teams
teams,
}: {
teams: {
name: string;
@ -35,7 +35,8 @@ export function TeamSwitcher({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
@ -52,7 +53,8 @@ export function TeamSwitcher({
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}>
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
@ -60,7 +62,8 @@ export function TeamSwitcher({
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2">
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>

View File

@ -6,7 +6,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
@ -34,7 +34,8 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
<Link
to={href}
className={!isActive ? "text-muted-foreground" : ""}
disabled={disabled}>
disabled={disabled}
>
{title}
</Link>
</DropdownMenuItem>
@ -46,15 +47,17 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
<nav
className={cn(
"hidden items-center space-x-4 md:flex lg:space-x-6",
className
className,
)}
{...props}>
{...props}
>
{links.map(({ title, href, isActive, disabled }) => (
<Link
key={`${title}-${href}`}
to={href}
disabled={disabled}
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}>
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}
>
{title}
</Link>
))}

View File

@ -3,13 +3,13 @@ import { cn } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
TooltipTrigger,
} from "@/components/ui/tooltip";
interface Props {
@ -21,7 +21,7 @@ interface Props {
export default function LongText({
children,
className = "",
contentClassName = ""
contentClassName = "",
}: Props) {
const ref = useRef<HTMLDivElement>(null);
const [isOverflown, setIsOverflown] = useState(false);

View File

@ -2,7 +2,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Menu } from "lucide-react";
import { Card } from "@/components/ui/card";
@ -18,9 +18,9 @@ const MainNavbar = () => {
<li className="font-medium text-primary">
<a href="#home">Home</a>
</li>
<li>
{/* <li>
<a href="#pricing">Pricing</a>
</li>
</li> */}
<li>
<a href="#faqs">FAQs</a>
</li>
@ -29,7 +29,6 @@ const MainNavbar = () => {
<div className="flex items-center">
<DynamicLoginButton />
<div className="mr-2 flex items-center gap-2 md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
@ -44,9 +43,9 @@ const MainNavbar = () => {
<DropdownMenuItem>
<a href="#features">Features</a>
</DropdownMenuItem>
<DropdownMenuItem>
{/* <DropdownMenuItem>
<a href="#pricing">Pricing</a>
</DropdownMenuItem>
</DropdownMenuItem> */}
<DropdownMenuItem>
<a href="#faqs">FAQs</a>
</DropdownMenuItem>

View File

@ -5,7 +5,7 @@ import { ChevronRight, type LucideIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarGroup,
@ -15,11 +15,11 @@ import {
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
export function NavMain({
items
items,
}: {
items: {
title: string;
@ -41,7 +41,8 @@ export function NavMain({
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible">
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>

View File

@ -5,7 +5,7 @@ import {
Forward,
MoreHorizontal,
Trash2,
type LucideIcon
type LucideIcon,
} from "lucide-react";
import {
@ -13,7 +13,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarGroup,
@ -22,11 +22,11 @@ import {
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
useSidebar,
} from "@/components/ui/sidebar";
export function NavProjects({
projects
projects,
}: {
projects: {
name: string;
@ -58,7 +58,8 @@ export function NavProjects({
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}>
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>

View File

@ -6,7 +6,7 @@ import {
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles
Sparkles,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@ -17,17 +17,17 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
useSidebar,
} from "@/components/ui/sidebar";
export function NavUser({
user
user,
}: {
user: {
name: string;
@ -44,7 +44,8 @@ export function NavUser({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
@ -60,7 +61,8 @@ export function NavUser({
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}>
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">

View File

@ -26,12 +26,13 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
variant="ghost"
disabled={disabled}
className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground"
onClick={() => setShowPassword((prev) => !prev)}>
onClick={() => setShowPassword((prev) => !prev)}
>
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
</Button>
</div>
);
}
},
);
PasswordInput.displayName = "PasswordInput";

View File

@ -106,7 +106,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
placeholder,
type,
length,
readOnly
readOnly,
});
/* call onChange func if pinValue changes */
@ -171,7 +171,7 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
} else {
refMap?.delete(pinIndex);
}
}
},
});
}
skipRef.current = skipRef.current + 1;
@ -220,7 +220,7 @@ const PinInputField = <T extends React.ElementType = "input">({
const isInsidePinInput = React.useContext(PinInputContext);
if (!isInsidePinInput) {
throw new Error(
`PinInputField must be used within ${PinInput.displayName}.`
`PinInputField must be used within ${PinInput.displayName}.`,
);
}
@ -254,7 +254,7 @@ const usePinInput = ({
placeholder,
type,
length,
readOnly
readOnly,
}: UsePinInputProps) => {
const pinInputs = React.useMemo(
() =>
@ -263,9 +263,9 @@ const usePinInput = ({
? defaultValue.charAt(index)
: value
? value.charAt(index)
: ""
: "",
),
[defaultValue, length, value]
[defaultValue, length, value],
);
const [pins, setPins] = React.useState(pinInputs);
@ -305,7 +305,7 @@ const usePinInput = ({
function handleFocus(
event: React.FocusEvent<HTMLInputElement>,
index: number
index: number,
) {
event.target.select();
focusInput(index);
@ -332,7 +332,7 @@ const usePinInput = ({
} else {
return p;
}
})
}),
);
}
@ -384,7 +384,7 @@ const usePinInput = ({
function handleKeyDown(
event: React.KeyboardEvent<HTMLInputElement>,
index: number
index: number,
) {
const { ctrlKey, key, shiftKey, metaKey } = event;
@ -429,7 +429,7 @@ const usePinInput = ({
handleBlur,
handleChange,
handlePaste,
handleKeyDown
handleKeyDown,
};
};

View File

@ -9,7 +9,7 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import useAuth from "@/hooks/useAuth";
@ -45,12 +45,6 @@ export function ProfileDropdown() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to="/dashboard/settings">
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/dashboard/settings">
Settings

View File

@ -16,9 +16,10 @@ export function Search({ className = "", placeholder = "Search" }: Props) {
variant="outline"
className={cn(
"relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64",
className
className,
)}
onClick={() => setOpen(true)}>
onClick={() => setOpen(true)}
>
<IconSearch
aria-hidden="true"
className="absolute left-1.5 top-1/2 -translate-y-1/2"

View File

@ -6,7 +6,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
SelectValue,
} from "@/components/ui/select";
interface SelectDropdownProps {
@ -28,7 +28,7 @@ export function SelectDropdown({
placeholder,
disabled,
className = "",
isControlled = false
isControlled = false,
}: SelectDropdownProps) {
const defaultState = isControlled
? { value: defaultValue, onValueChange }

View File

@ -2,7 +2,8 @@ const SkipToMain = () => {
return (
<a
className={`fixed left-44 z-[999] -translate-y-52 whitespace-nowrap bg-primary px-4 py-2 text-sm font-medium text-primary-foreground opacity-95 shadow transition hover:bg-primary/90 focus:translate-y-3 focus:transform focus-visible:ring-1 focus-visible:ring-ring`}
href="#content">
href="#content"
>
Skip to Main
</a>
);

View File

@ -10,17 +10,17 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
useSidebar,
} from "@/components/ui/sidebar";
export function TeamSwitcher({
teams
teams,
}: {
teams: {
name: string;
@ -38,7 +38,8 @@ export function TeamSwitcher({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
@ -55,7 +56,8 @@ export function TeamSwitcher({
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}>
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
@ -63,7 +65,8 @@ export function TeamSwitcher({
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2">
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>

View File

@ -7,7 +7,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeSwitch() {

View File

@ -1,10 +1,10 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef<
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
@ -27,7 +27,7 @@ const AccordionTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
className,
)}
{...props}
>
@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef<
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
ref={ref}
@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
className,
)}
{...props}
/>
@ -49,7 +49,7 @@ const AlertDialogHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
@ -63,7 +63,7 @@ const AlertDialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
@ -116,7 +116,7 @@ const AlertDialogCancel = React.forwardRef<
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
className,
)}
{...props}
/>
@ -134,5 +134,5 @@ export {
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel
AlertDialogCancel,
};

View File

@ -9,13 +9,13 @@ const alertVariants = cva(
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
}
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default"
}
}
variant: "default",
},
},
);
const Alert = React.forwardRef<

View File

@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
/>
@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
className,
)}
{...props}
/>

View File

@ -13,13 +13,13 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground"
}
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default"
}
}
variant: "default",
},
},
);
export interface BadgeProps

View File

@ -20,7 +20,7 @@ const BreadcrumbList = React.forwardRef<
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
className,
)}
{...props}
/>
@ -81,7 +81,8 @@ const BreadcrumbSeparator = ({
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}>
{...props}
>
{children ?? <ChevronRight />}
</li>
);
@ -95,7 +96,8 @@ const BreadcrumbEllipsis = ({
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}>
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
@ -109,5 +111,5 @@ export {
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
BreadcrumbEllipsis,
};

View File

@ -17,20 +17,20 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default"
}
}
size: "default",
},
},
);
export interface ButtonProps
@ -49,7 +49,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
/>
);
}
},
);
Button.displayName = "Button";

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}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
className,
)}
{...props}
/>
@ -78,5 +78,5 @@ export {
CardFooter,
CardTitle,
CardDescription,
CardContent
CardContent,
};

View File

@ -11,11 +11,13 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
className,
)}
{...props}>
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}>
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>

View File

@ -8,7 +8,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
const Command = React.forwardRef<
@ -19,7 +19,7 @@ const Command = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
className,
)}
{...props}
/>
@ -54,7 +54,7 @@ const CommandInput = React.forwardRef<
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
/>
@ -97,7 +97,7 @@ const CommandGroup = React.forwardRef<
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
className,
)}
{...props}
/>
@ -125,7 +125,7 @@ const CommandItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
className,
)}
{...props}
/>
@ -141,7 +141,7 @@ const CommandShortcut = ({
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
className,
)}
{...props}
/>
@ -158,5 +158,5 @@ export {
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator
CommandSeparator,
};

View File

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
/>
@ -38,9 +38,10 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
className,
)}
{...props}>
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
@ -58,7 +59,7 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
className,
)}
{...props}
/>
@ -72,7 +73,7 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
@ -87,7 +88,7 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
className,
)}
{...props}
/>
@ -116,5 +117,5 @@ export {
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription
DialogDescription,
};

View File

@ -26,9 +26,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
className,
)}
{...props}>
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
@ -44,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>
@ -63,7 +64,7 @@ const DropdownMenuContent = React.forwardRef<
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>
@ -82,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
className,
)}
{...props}
/>
@ -97,10 +98,11 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
checked={checked}
{...props}>
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
@ -120,9 +122,10 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}>
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
@ -144,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
className,
)}
{...props}
/>
@ -191,5 +194,5 @@ export {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
DropdownMenuRadioGroup,
};

View File

@ -5,7 +5,7 @@ import {
FieldPath,
FieldValues,
FormProvider,
useFormContext
useFormContext,
} from "react-hook-form";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
@ -16,18 +16,18 @@ const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@ -57,7 +57,7 @@ const useFormField = () => {
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
...fieldState,
};
};
@ -66,7 +66,7 @@ type FormItemContextValue = {
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
@ -156,7 +156,8 @@ const FormMessage = React.forwardRef<
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}>
{...props}
>
{body}
</p>
);
@ -171,5 +172,5 @@ export {
FormControl,
FormDescription,
FormMessage,
FormField
FormField,
};

View File

@ -8,13 +8,13 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
className,
)}
ref={ref}
{...props}
/>
);
}
},
);
Input.displayName = "Input";

View File

@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<

View File

@ -10,13 +10,13 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
@ -53,7 +53,7 @@ const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
{...props}
/>
);
}
},
);
PhoneInput.displayName = "PhoneInput";
@ -82,7 +82,7 @@ const CountrySelect = ({
disabled,
value: selectedCountry,
options: countryList,
onChange
onChange,
}: CountrySelectProps) => {
return (
<Popover>
@ -91,7 +91,8 @@ const CountrySelect = ({
type="button"
variant="outline"
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
disabled={disabled}>
disabled={disabled}
>
<FlagComponent
country={selectedCountry}
countryName={selectedCountry}
@ -99,7 +100,7 @@ const CountrySelect = ({
<ChevronsUpDown
className={cn(
"-mr-2 size-4 opacity-50",
disabled ? "hidden" : "opacity-100"
disabled ? "hidden" : "opacity-100",
)}
/>
</Button>
@ -120,7 +121,7 @@ const CountrySelect = ({
selectedCountry={selectedCountry}
onChange={onChange}
/>
) : null
) : null,
)}
</CommandGroup>
</ScrollArea>
@ -140,7 +141,7 @@ const CountrySelectOption = ({
country,
countryName,
selectedCountry,
onChange
onChange,
}: CountrySelectOptionProps) => {
return (
<CommandItem className="gap-2" onSelect={() => onChange(country)}>

View File

@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>

View File

@ -26,9 +26,10 @@ const RadioGroupItem = React.forwardRef<
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}>
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>

View File

@ -14,12 +14,14 @@ const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}>
{...props}
>
<ScrollAreaPrimitive.Viewport
className={cn(
"h-full w-full rounded-[inherit]",
orientation === "horizontal" && "!overflow-x-auto"
)}>
orientation === "horizontal" && "!overflow-x-auto",
)}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar orientation={orientation} />
@ -41,9 +43,10 @@ const ScrollBar = React.forwardRef<
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
className,
)}
{...props}>
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));

View File

@ -17,9 +17,10 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
className,
)}
{...props}>
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
@ -36,9 +37,10 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}>
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
@ -52,9 +54,10 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}>
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
@ -72,17 +75,19 @@ const SelectContent = React.forwardRef<
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}>
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}>
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
@ -111,9 +116,10 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}>
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
@ -146,5 +152,5 @@ export {
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton
SelectScrollDownButton,
};

View File

@ -8,7 +8,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
@ -17,11 +17,11 @@ const Separator = React.forwardRef<
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
className,
)}
{...props}
/>
)
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;

View File

@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
ref={ref}
@ -39,13 +39,13 @@ const sheetVariants = cva(
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
}
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right"
}
}
side: "right",
},
},
);
interface SheetContentProps
@ -61,7 +61,8 @@ const SheetContent = React.forwardRef<
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}>
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
@ -79,7 +80,7 @@ const SheetHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
@ -93,7 +94,7 @@ const SheetFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
@ -134,5 +135,5 @@ export {
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
SheetDescription,
};

View File

@ -12,14 +12,14 @@ import {
Sheet,
SheetContent,
SheetDescription,
SheetTitle
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
TooltipTrigger,
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
@ -68,7 +68,7 @@ const SidebarProvider = React.forwardRef<
children,
...props
},
ref
ref,
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
@ -89,7 +89,7 @@ const SidebarProvider = React.forwardRef<
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
[setOpenProp, open],
);
// Helper to toggle the sidebar.
@ -127,9 +127,17 @@ const SidebarProvider = React.forwardRef<
isMobile,
openMobile,
setOpenMobile,
toggleSidebar
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
);
return (
@ -140,21 +148,22 @@ const SidebarProvider = React.forwardRef<
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
className
className,
)}
ref={ref}
{...props}>
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
},
);
SidebarProvider.displayName = "SidebarProvider";
@ -175,7 +184,7 @@ const Sidebar = React.forwardRef<
children,
...props
},
ref
ref,
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
@ -184,10 +193,11 @@ const Sidebar = React.forwardRef<
<div
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
className
className,
)}
ref={ref}
{...props}>
{...props}
>
{children}
</div>
);
@ -205,10 +215,11 @@ const Sidebar = React.forwardRef<
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}>
side={side}
>
<VisuallyHidden asChild>
<SheetDescription />
</VisuallyHidden>
@ -225,7 +236,8 @@ const Sidebar = React.forwardRef<
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}>
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
@ -234,7 +246,7 @@ const Sidebar = React.forwardRef<
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
@ -247,18 +259,20 @@ const Sidebar = React.forwardRef<
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
className,
)}
{...props}>
{...props}
>
<div
data-sidebar="sidebar"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow">
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
}
},
);
Sidebar.displayName = "Sidebar";
@ -279,7 +293,8 @@ const SidebarTrigger = React.forwardRef<
onClick?.(event);
toggleSidebar();
}}
{...props}>
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
@ -308,7 +323,7 @@ const SidebarRail = React.forwardRef<
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
className,
)}
{...props}
/>
@ -326,7 +341,7 @@ const SidebarInset = React.forwardRef<
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
className,
)}
{...props}
/>
@ -344,7 +359,7 @@ const SidebarInput = React.forwardRef<
data-sidebar="input"
className={cn(
"focus-visible:ring-sidebar-ring h-8 w-full bg-background shadow-none focus-visible:ring-2",
className
className,
)}
{...props}
/>
@ -407,7 +422,7 @@ const SidebarContent = React.forwardRef<
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
className,
)}
{...props}
/>
@ -443,7 +458,7 @@ const SidebarGroupLabel = React.forwardRef<
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
className,
)}
{...props}
/>
@ -466,7 +481,7 @@ const SidebarGroupAction = React.forwardRef<
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
@ -520,19 +535,19 @@ const sidebarMenuButtonVariants = cva(
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0"
}
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default"
}
}
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
@ -553,7 +568,7 @@ const SidebarMenuButton = React.forwardRef<
className,
...props
},
ref
ref,
) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
@ -575,7 +590,7 @@ const SidebarMenuButton = React.forwardRef<
if (typeof tooltip === "string") {
tooltip = {
children: tooltip
children: tooltip,
};
}
@ -590,7 +605,7 @@ const SidebarMenuButton = React.forwardRef<
/>
</Tooltip>
);
}
},
);
SidebarMenuButton.displayName = "SidebarMenuButton";
@ -617,7 +632,7 @@ const SidebarMenuAction = React.forwardRef<
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
className,
)}
{...props}
/>
@ -639,7 +654,7 @@ const SidebarMenuBadge = React.forwardRef<
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
@ -662,7 +677,8 @@ const SidebarMenuSkeleton = React.forwardRef<
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}>
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
@ -674,7 +690,7 @@ const SidebarMenuSkeleton = React.forwardRef<
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width
"--skeleton-width": width,
} as React.CSSProperties
}
/>
@ -693,7 +709,7 @@ const SidebarMenuSub = React.forwardRef<
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
@ -728,7 +744,7 @@ const SidebarMenuSubButton = React.forwardRef<
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
@ -760,5 +776,5 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar
useSidebar,
};

View File

@ -9,13 +9,14 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
className,
)}
{...props}
ref={ref}>
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>

View File

@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
className,
)}
{...props}
/>
@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
className,
)}
{...props}
/>
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
className,
)}
{...props}
/>
@ -88,7 +88,7 @@ const TableCell = React.forwardRef<
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
className,
)}
{...props}
/>
@ -115,5 +115,5 @@ export {
TableHead,
TableRow,
TableCell,
TableCaption
TableCaption,
};

View File

@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
className,
)}
{...props}
/>
@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
className,
)}
{...props}
/>
@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
className,
)}
{...props}
/>

View File

@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
className,
)}
ref={ref}
{...props}

View File

@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
className,
)}
{...props}
/>
@ -28,13 +28,13 @@ const toastVariants = cva(
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground"
}
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default"
}
}
variant: "default",
},
},
);
const Toast = React.forwardRef<
@ -60,7 +60,7 @@ const ToastAction = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
className,
)}
{...props}
/>
@ -75,10 +75,11 @@ const ToastClose = React.forwardRef<
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
className,
)}
toast-close=""
{...props}>
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
@ -121,5 +122,5 @@ export {
ToastTitle,
ToastDescription,
ToastClose,
ToastAction
ToastAction,
};

View File

@ -5,7 +5,7 @@ import {
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport
ToastViewport,
} from "@/components/ui/toast";
export function Toaster() {

View File

@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>

View File

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

View File

@ -11,7 +11,7 @@ interface FontContextType {
const FontContext = createContext<FontContextType | undefined>(undefined);
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
children
children,
}) => {
const [font, _setFont] = useState<Font>(() => {
const savedFont = localStorage.getItem("font");

View File

@ -15,7 +15,7 @@ type ThemeProviderState = {
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
@ -27,7 +27,7 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, _setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
useEffect(() => {
@ -61,7 +61,7 @@ export function ThemeProvider({
const value = {
theme,
setTheme
setTheme,
};
return (

View File

@ -4,17 +4,11 @@ import { useEffect, useState } from "react";
import { handleServerError } from "@/utils/handle-server-error";
import { ApiError } from "@/errors/api-error";
import {
DashboardService,
LoginService,
ShopLoginAccessTokenData,
UserPublic,
UserRegister,
UserService,
UserUpdate
} from "@/client";
import { ShopLoginAccessTokenData, UserPublic } from "@/client";
import { toast } from "./useToast";
import { authAPI } from "@/api/api";
const isLoggedIn = () => {
return localStorage.getItem("access_token") !== null;
};
@ -24,27 +18,22 @@ const useAuth = () => {
const [loggedIn, setLoggedIn] = useState(isLoggedIn());
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: user } = useQuery<UserPublic | null, Error>({
queryKey: ["currentUser"],
queryFn: DashboardService.userGetUser,
enabled: loggedIn
queryFn: authAPI.getCurrentUser,
enabled: loggedIn,
});
const signUpMutation = useMutation({
mutationFn: (data: UserRegister) =>
DashboardService.userRegister({ requestBody: data }),
mutationFn: authAPI.registerUser,
onSuccess: () => navigate({ to: "/sign-in" }),
onError: (err: ApiError) => handleServerError(err),
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] })
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
});
const login = async (data: ShopLoginAccessTokenData) => {
const response = await LoginService.dashboardLoginAccessToken({
formData: {
username: data.formData.username,
password: data.formData.password
}
});
const response = await authAPI.loginUser(data);
localStorage.setItem("access_token", response.access_token);
setLoggedIn(true);
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
@ -53,7 +42,7 @@ const useAuth = () => {
const loginMutation = useMutation({
mutationFn: login,
onSuccess: () => navigate({ to: "/" }),
onError: (err: ApiError) => handleServerError(err)
onError: (err: ApiError) => handleServerError(err),
});
const logout = () => {
@ -64,15 +53,26 @@ const useAuth = () => {
};
const updateAccountMutation = useMutation({
mutationFn: (data: UserUpdate) =>
UserService.userUpdateUser({ requestBody: data }),
mutationFn: authAPI.updateUser,
onSuccess: () => {
toast({ title: "Account updated successfully" });
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
},
onError: (err: ApiError) => handleServerError(err)
onError: (err: ApiError) => handleServerError(err),
});
useEffect(() => {
console.log("Checking whether the token is valid");
const userLoggedInAndNull = loggedIn && user === null;
const tokenExistsAndNull =
Boolean(localStorage.getItem("access_token")) && user === null;
if (userLoggedInAndNull || tokenExistsAndNull) {
console.warn("User data is null while logged in, logging out.");
logout();
}
}, [loggedIn, user]);
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "access_token") {
@ -97,7 +97,7 @@ const useAuth = () => {
logout,
user,
error,
resetError: () => setError(null)
resetError: () => setError(null),
};
};

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">()
*/
export default function useDialogState<T extends string | boolean>(
initialState: T | null = null
initialState: T | null = null,
) {
const [open, _setOpen] = useState<T | null>(initialState);

View File

@ -4,7 +4,7 @@ const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
undefined,
);
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