Added customer list, coupon list and started working on purchase list

This commit is contained in:
Thastertyn 2025-04-21 19:29:01 +02:00
parent 2fe5dbcb21
commit f2af9dc566
223 changed files with 5027 additions and 3017 deletions

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

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",
@ -62,6 +62,7 @@
"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",

View File

@ -147,6 +147,9 @@ importers:
specifier: ^5.0.3
version: 5.0.3(@types/react@19.1.0)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
devDependencies:
'@biomejs/biome':
specifier: ^1.9.4
version: 1.9.4
'@eslint/js':
specifier: ^9.16.0
version: 9.24.0
@ -365,6 +368,59 @@ packages:
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
engines: {node: '>=6.9.0'}
'@biomejs/biome@1.9.4':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
@ -3379,6 +3435,41 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@biomejs/biome@1.9.4':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4
'@biomejs/cli-darwin-x64': 1.9.4
'@biomejs/cli-linux-arm64': 1.9.4
'@biomejs/cli-linux-arm64-musl': 1.9.4
'@biomejs/cli-linux-x64': 1.9.4
'@biomejs/cli-linux-x64-musl': 1.9.4
'@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 1.9.4
'@biomejs/cli-darwin-arm64@1.9.4':
optional: true
'@biomejs/cli-darwin-x64@1.9.4':
optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-arm64@1.9.4':
optional: true
'@biomejs/cli-linux-x64-musl@1.9.4':
optional: true
'@biomejs/cli-linux-x64@1.9.4':
optional: true
'@biomejs/cli-win32-arm64@1.9.4':
optional: true
'@biomejs/cli-win32-x64@1.9.4':
optional: true
'@date-fns/tz@1.2.0': {}
'@esbuild/aix-ppc64@0.25.2':

View File

@ -2,7 +2,7 @@ import {
UserPublic,
UserRegister,
UserUpdate,
ShopLoginAccessTokenData
ShopLoginAccessTokenData,
} from "@/client";
import { Shop } from "./mock/models";

View File

@ -3,10 +3,19 @@ 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 productsAPI = MockProductAPI;
export const usersAPI = MockUserAPI;
export const couponAPI = MockCouponAPI;
export const purchaseAPI = MockPurchaseAPI;

View File

@ -5,7 +5,6 @@ import { extractSubFromJWT, generateFakeJWT } from "@/utils/jwt";
import { mockDB } from "./db";
import { MockUser, Shop } from "./models";
const db = mockDB;
export const MockAuthAPI: AuthAPI = {
@ -23,7 +22,7 @@ export const MockAuthAPI: AuthAPI = {
...user,
first_name: user.first_name ?? null,
last_name: user.last_name ?? null,
profile_picture: user.profile_picture ?? null
profile_picture: user.profile_picture ?? null,
};
return publicUser;
},
@ -37,8 +36,8 @@ export const MockAuthAPI: AuthAPI = {
const newUser: MockUser = {
id: userId,
uuid: userUUID,
user_role: "customer",
status: "customer",
user_role: "owner",
status: "owner",
shop_id: null,
username: data.username,
email: data.email,
@ -49,7 +48,7 @@ export const MockAuthAPI: AuthAPI = {
profile_picture: undefined,
created_at: now,
updated_at: now,
last_login: now
last_login: now,
};
const newShop: Shop = {
@ -70,16 +69,23 @@ export const MockAuthAPI: AuthAPI = {
city: "",
state: null,
postal_code: "",
country: ""
}
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);
});
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) {
@ -115,7 +121,7 @@ export const MockAuthAPI: AuthAPI = {
phone_number: data.phone_number ?? "",
username: data.username ?? "",
first_name: data.first_name ?? undefined,
last_name: data.last_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

@ -1,5 +1,6 @@
import Dexie, { Table } from "dexie";
import {
Coupon,
MockUser,
MockUserPreferences,
MockUserStatistics,
@ -8,7 +9,9 @@ import {
ProductCategoryJunction,
ProductImage,
ProductVariant,
Shop
Purchase,
PurchaseEntry,
Shop,
} from "./models";
class MockDB extends Dexie {
@ -21,6 +24,9 @@ class MockDB extends Dexie {
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");
@ -33,7 +39,10 @@ class MockDB extends Dexie {
product_variants: "++id,product_id",
product_images: "++id,product_id",
product_categories: "++id,parent_category_id",
product_category_junctions: "[product_id+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",
});
}
}

View File

@ -81,10 +81,23 @@ export interface ProductImage {
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; // used for ordering
index: number;
name: string;
price: number;
comment?: string;
@ -92,16 +105,36 @@ export interface ProductVariant {
updated_at: string;
}
export interface ProductCreate {
export interface Coupon {
id: number;
name: string;
description: string;
price: number;
stock_quantity: number;
image_data?: string | undefined;
text: string;
valid_due: string;
discount_amount: number;
}
export interface ProductWithDetails extends Product {
images: ProductImage[];
variants: ProductVariant[];
categories: ProductCategory[];
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

@ -28,20 +28,20 @@ export const MockProductAPI = {
.toArray();
const rawCategories = await Promise.all(
categoryLinks.map((link) =>
mockDB.product_categories.get(link.category_id)
)
mockDB.product_categories.get(link.category_id),
),
);
const categories = rawCategories.filter(
(c): c is ProductCategory => c !== undefined
(c): c is ProductCategory => c !== undefined,
);
return {
...product,
images,
variants,
categories
categories,
};
})
}),
);
return detailedProducts;
@ -66,18 +66,18 @@ export const MockProductAPI = {
const rawCategories = await Promise.all(
categoryLinks.map((link) =>
mockDB.product_categories.get(link.category_id)
)
mockDB.product_categories.get(link.category_id),
),
);
const categories = rawCategories.filter(
(c): c is ProductCategory => c !== undefined
(c): c is ProductCategory => c !== undefined,
);
return {
...product,
images,
variants,
categories
categories,
};
},
@ -93,7 +93,7 @@ export const MockProductAPI = {
...productData,
shop_id: user.id,
created_at: now,
updated_at: now
updated_at: now,
});
if ("image_data" in productData && productData.image_data) {
@ -103,7 +103,7 @@ export const MockProductAPI = {
product_id: productId,
image_id: image_id,
image_url: productData.image_data,
alt_text: `${productData.name} image`
alt_text: `${productData.name} image`,
});
}
@ -117,7 +117,7 @@ export const MockProductAPI = {
const updatedProduct = {
...product,
...data,
updated_at: new Date().toISOString()
updated_at: new Date().toISOString(),
};
await mockDB.products.put(updatedProduct);
@ -130,7 +130,7 @@ export const MockProductAPI = {
await mockDB.product_images.put({
...existingImage,
image_url: data.image_data,
alt_text: `${data.name ?? product.name} image`
alt_text: `${data.name ?? product.name} image`,
});
} else {
const image_id = Date.now();
@ -139,7 +139,7 @@ export const MockProductAPI = {
product_id: productId,
image_id: image_id,
image_url: data.image_data,
alt_text: `${data.name ?? product.name} image`
alt_text: `${data.name ?? product.name} image`,
});
}
}
@ -159,5 +159,5 @@ export const MockProductAPI = {
.delete();
await mockDB.products.delete(productId);
return true;
}
},
};

View File

@ -0,0 +1,85 @@
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);
await mockDB.purchase_entries.bulkAdd(
entries.map((entry) => ({
...entry,
purchase_id: id,
id: Date.now() + Math.random(),
})),
);
return newPurchase;
},
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

@ -35,7 +35,7 @@ export const MockShopAPI: ShopAPI = {
console.log("Saving data ->", data);
await db.shops.update(user.id, {
...data,
updated_at: new Date().toISOString()
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

@ -10,4 +10,4 @@ export async function getCurrentUserDirect() {
const user = await mockDB.users.where("uuid").equals(uuid).first();
return user ?? null;
}
}

View File

@ -10,10 +10,10 @@ export const RealAuthAPI: AuthAPI = {
},
async loginUser(data) {
return LoginService.dashboardLoginAccessToken({
formData: data.formData
formData: data.formData,
});
},
async updateUser(data) {
await UserService.userUpdateUser({ requestBody: data });
}
},
};

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,14 +8,18 @@ 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;
this.body = response.body;
this.request = request;
}
}
}

View File

@ -6,16 +6,16 @@ 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;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly url: string;
};
};

View File

@ -4,4 +4,4 @@ export type ApiResult<TData = any> = {
readonly status: number;
readonly statusText: string;
readonly url: 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,15 +112,15 @@ 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 {
return this._isCancelled;
}
}
}

View File

@ -1,32 +1,32 @@
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>;
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export class Interceptors<T> {
_fns: Middleware<T>[];
_fns: Middleware<T>[];
constructor() {
this._fns = [];
}
constructor() {
this._fns = [];
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn];
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn];
}
}
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,17 +41,17 @@ 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(),
response: 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]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
.filter(([, value]) => value !== undefined && value !== null)
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} 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 = {
@ -344,4 +384,4 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>,
reject(error);
}
});
};
};

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,438 +1,485 @@
// 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 {
/**
* Get information about currently logged in user
* @returns UserPublic Successful Response
* @throws ApiError
*/
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/user'
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/user',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/user',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Delete user
* @returns boolean Successful Response
* @throws ApiError
*/
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/user'
});
}
/**
* Login Access Token
* OAuth2 compatible token login for the dashboard.
*
* This endpoint generates an access token required for authenticating future
* requests to the dashboard section of the application. The token is valid for
* a predefined expiration period.
*
* - **username**: User's email
* - **password**: User's password
*
* **Note:** This login is restricted to dashboard access only and cannot be
* used for tenant accounts access to shops
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/login/access-token',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: 'Validation Error'
}
});
}
/**
* Register New Shop
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static dashboardRegisterNewShop(data: DashboardRegisterNewShopData): CancelablePromise<DashboardRegisterNewShopResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/shop',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Get information about currently logged in user
* @returns UserPublic Successful Response
* @throws ApiError
*/
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/user",
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userUpdateUser(
data: UserUpdateUserData,
): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, {
method: "PUT",
url: "/user",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userRegister(
data: UserRegisterData,
): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/user",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Delete user
* @returns boolean Successful Response
* @throws ApiError
*/
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/user",
});
}
/**
* Login Access Token
* OAuth2 compatible token login for the dashboard.
*
* This endpoint generates an access token required for authenticating future
* requests to the dashboard section of the application. The token is valid for
* a predefined expiration period.
*
* - **username**: User's email
* - **password**: User's password
*
* **Note:** This login is restricted to dashboard access only and cannot be
* used for tenant accounts access to shops
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static dashboardLoginAccessToken(
data: DashboardLoginAccessTokenData,
): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/login/access-token",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
});
}
/**
* Register New Shop
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static dashboardRegisterNewShop(
data: DashboardRegisterNewShopData,
): CancelablePromise<DashboardRegisterNewShopResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/shop",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
}
export class LoginService {
/**
* Login Access Token
* OAuth2 compatible token login for the dashboard.
*
* This endpoint generates an access token required for authenticating future
* requests to the dashboard section of the application. The token is valid for
* a predefined expiration period.
*
* - **username**: User's email
* - **password**: User's password
*
* **Note:** This login is restricted to dashboard access only and cannot be
* used for tenant accounts access to shops
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static dashboardLoginAccessToken(data: DashboardLoginAccessTokenData): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/login/access-token',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: 'Validation Error'
}
});
}
/**
* Login Access Token
* OAuth2 compatible token login, get an access token for future requests
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/shop/{shop_uuid}/login/access-token',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: 'Validation Error'
}
});
}
/**
* Login Access Token
* OAuth2 compatible token login for the dashboard.
*
* This endpoint generates an access token required for authenticating future
* requests to the dashboard section of the application. The token is valid for
* a predefined expiration period.
*
* - **username**: User's email
* - **password**: User's password
*
* **Note:** This login is restricted to dashboard access only and cannot be
* used for tenant accounts access to shops
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static dashboardLoginAccessToken(
data: DashboardLoginAccessTokenData,
): CancelablePromise<DashboardLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/login/access-token",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
});
}
/**
* Login Access Token
* OAuth2 compatible token login, get an access token for future requests
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static shopLoginAccessToken(
data: ShopLoginAccessTokenData,
): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/shop/{shop_uuid}/login/access-token",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
});
}
}
export class ShopService {
/**
* Login Access Token
* OAuth2 compatible token login, get an access token for future requests
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static shopLoginAccessToken(data: ShopLoginAccessTokenData): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/shop/{shop_uuid}/login/access-token',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: 'Validation Error'
}
});
}
/**
* Delete user
* @param data The data for the request.
* @param data.shopUuid
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/shop/{shop_uuid}/user/delete',
path: {
shop_uuid: data.shopUuid
},
errors: {
422: 'Validation Error'
}
});
}
/**
* User logout
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/shop/{shop_uuid}/user/logout'
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.shopUuid
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/shop/{shop_uuid}/user/register',
path: {
shop_uuid: data.shopUuid
},
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/shop/{shop_uuid}/user/update',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Login Access Token
* OAuth2 compatible token login, get an access token for future requests
* @param data The data for the request.
* @param data.formData
* @returns Token Successful Response
* @throws ApiError
*/
public static shopLoginAccessToken(
data: ShopLoginAccessTokenData,
): CancelablePromise<ShopLoginAccessTokenResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/shop/{shop_uuid}/login/access-token",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
});
}
/**
* Delete user
* @param data The data for the request.
* @param data.shopUuid
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopDeleteUser(
data: ShopDeleteUserData,
): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/shop/{shop_uuid}/user/delete",
path: {
shop_uuid: data.shopUuid,
},
errors: {
422: "Validation Error",
},
});
}
/**
* User logout
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/shop/{shop_uuid}/user/logout",
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.shopUuid
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopRegister(
data: ShopRegisterData,
): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/shop/{shop_uuid}/user/register",
path: {
shop_uuid: data.shopUuid,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopUpdateUser(
data: ShopUpdateUserData,
): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, {
method: "PUT",
url: "/shop/{shop_uuid}/user/update",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
}
export class UserService {
/**
* Get information about currently logged in user
* @returns UserPublic Successful Response
* @throws ApiError
*/
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/user'
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userUpdateUser(data: UserUpdateUserData): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/user',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userRegister(data: UserRegisterData): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/user',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Delete user
* @returns boolean Successful Response
* @throws ApiError
*/
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/user'
});
}
/**
* Delete user
* @param data The data for the request.
* @param data.shopUuid
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopDeleteUser(data: ShopDeleteUserData): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/shop/{shop_uuid}/user/delete',
path: {
shop_uuid: data.shopUuid
},
errors: {
422: 'Validation Error'
}
});
}
/**
* User logout
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/shop/{shop_uuid}/user/logout'
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.shopUuid
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopRegister(data: ShopRegisterData): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/shop/{shop_uuid}/user/register',
path: {
shop_uuid: data.shopUuid
},
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopUpdateUser(data: ShopUpdateUserData): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/shop/{shop_uuid}/user/update',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Get information about currently logged in user
* @returns UserPublic Successful Response
* @throws ApiError
*/
public static userGetUser(): CancelablePromise<UserGetUserResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/user",
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userUpdateUser(
data: UserUpdateUserData,
): CancelablePromise<UserUpdateUserResponse> {
return __request(OpenAPI, {
method: "PUT",
url: "/user",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.requestBody
* @returns boolean Successful Response
* @throws ApiError
*/
public static userRegister(
data: UserRegisterData,
): CancelablePromise<UserRegisterResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/user",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Delete user
* @returns boolean Successful Response
* @throws ApiError
*/
public static userDeleteUser(): CancelablePromise<UserDeleteUserResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/user",
});
}
/**
* Delete user
* @param data The data for the request.
* @param data.shopUuid
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopDeleteUser(
data: ShopDeleteUserData,
): CancelablePromise<ShopDeleteUserResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/shop/{shop_uuid}/user/delete",
path: {
shop_uuid: data.shopUuid,
},
errors: {
422: "Validation Error",
},
});
}
/**
* User logout
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopLogout(): CancelablePromise<ShopLogoutResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/shop/{shop_uuid}/user/logout",
});
}
/**
* Register new user
* @param data The data for the request.
* @param data.shopUuid
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopRegister(
data: ShopRegisterData,
): CancelablePromise<ShopRegisterResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/shop/{shop_uuid}/user/register",
path: {
shop_uuid: data.shopUuid,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Update user details
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static shopUpdateUser(
data: ShopUpdateUserData,
): CancelablePromise<ShopUpdateUserResponse> {
return __request(OpenAPI, {
method: "PUT",
url: "/shop/{shop_uuid}/user/update",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
}
export class UtilsService {
/**
* Health Check
* Ping the API whether it's alive or not
* @returns boolean Successful Response
* @throws ApiError
*/
public static utilsHealthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/utils/health-check/'
});
}
/**
* Test Db
* Ping database using select 1 to see if connection works
* @returns boolean Successful Response
* @throws ApiError
*/
public static utilsTestDb(): CancelablePromise<UtilsTestDbResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/utils/test-db/'
});
}
}
/**
* Health Check
* Ping the API whether it's alive or not
* @returns boolean Successful Response
* @throws ApiError
*/
public static utilsHealthCheck(): CancelablePromise<UtilsHealthCheckResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/utils/health-check/",
});
}
/**
* Test Db
* Ping database using select 1 to see if connection works
* @returns boolean Successful Response
* @throws ApiError
*/
public static utilsTestDb(): CancelablePromise<UtilsTestDbResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/utils/test-db/",
});
}
}

View File

@ -1,142 +1,142 @@
// This file is auto-generated by @hey-api/openapi-ts
export type Body_Dashboard_login_access_token = {
grant_type?: (string | null);
username: string;
password: string;
scope?: string;
client_id?: (string | null);
client_secret?: (string | null);
grant_type?: string | null;
username: string;
password: string;
scope?: string;
client_id?: string | null;
client_secret?: string | null;
};
export type Body_Shop_login_access_token = {
grant_type?: (string | null);
username: string;
password: string;
scope?: string;
client_id?: (string | null);
client_secret?: (string | null);
grant_type?: string | null;
username: string;
password: string;
scope?: string;
client_id?: string | null;
client_secret?: string | null;
};
export type HTTPValidationError = {
detail?: Array<ValidationError>;
detail?: Array<ValidationError>;
};
export type ShopAddress = {
street: string;
city: string;
state: string;
postal_code: string;
country: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
};
export type ShopCreate = {
name: string;
description: string;
currency: string;
contact_email: string;
contact_phone_number: string;
address: ShopAddress;
name: string;
description: string;
currency: string;
contact_email: string;
contact_phone_number: string;
address: ShopAddress;
};
export type Token = {
access_token: string;
token_type?: string;
access_token: string;
token_type?: string;
};
export type UserPublic = {
uuid: string;
username: string;
email: string;
first_name: (string | null);
last_name: (string | null);
phone_number: string;
uuid: string;
username: string;
email: string;
first_name: string | null;
last_name: string | null;
phone_number: string;
};
export type UserRegister = {
username: string;
email: string;
phone_number: string;
/**
* Password must conform to this regex:
* ```
* ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$
* ```
*/
password: string;
username: string;
email: string;
phone_number: string;
/**
* Password must conform to this regex:
* ```
* ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$
* ```
*/
password: string;
};
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)>;
msg: string;
type: string;
loc: Array<string | number>;
msg: string;
type: string;
};
export type UserGetUserResponse = (UserPublic);
export type UserGetUserResponse = UserPublic;
export type UserUpdateUserData = {
requestBody: UserUpdate;
requestBody: UserUpdate;
};
export type UserUpdateUserResponse = (boolean);
export type UserUpdateUserResponse = boolean;
export type UserRegisterData = {
requestBody: UserRegister;
requestBody: UserRegister;
};
export type UserRegisterResponse = (boolean);
export type UserRegisterResponse = boolean;
export type UserDeleteUserResponse = (boolean);
export type UserDeleteUserResponse = boolean;
export type DashboardLoginAccessTokenData = {
formData: Body_Dashboard_login_access_token;
formData: Body_Dashboard_login_access_token;
};
export type DashboardLoginAccessTokenResponse = (Token);
export type DashboardLoginAccessTokenResponse = Token;
export type DashboardRegisterNewShopData = {
requestBody: ShopCreate;
requestBody: ShopCreate;
};
export type DashboardRegisterNewShopResponse = (boolean);
export type DashboardRegisterNewShopResponse = boolean;
export type ShopLoginAccessTokenData = {
formData: Body_Shop_login_access_token;
formData: Body_Shop_login_access_token;
};
export type ShopLoginAccessTokenResponse = (Token);
export type ShopLoginAccessTokenResponse = Token;
export type ShopDeleteUserData = {
shopUuid: unknown;
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;
requestBody: UserRegister;
shopUuid: unknown;
};
export type ShopRegisterResponse = (unknown);
export type ShopRegisterResponse = unknown;
export type ShopUpdateUserData = {
requestBody: {
[key: string]: unknown;
};
requestBody: {
[key: string]: unknown;
};
};
export type ShopUpdateUserResponse = (unknown);
export type ShopUpdateUserResponse = unknown;
export type UtilsHealthCheckResponse = (boolean);
export type UtilsHealthCheckResponse = boolean;
export type UtilsTestDbResponse = (boolean);
export type UtilsTestDbResponse = boolean;

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

@ -153,5 +153,5 @@ const currencies = [
"YER",
"ZAR",
"ZMW",
"ZWG"
"ZWG",
];

View File

@ -2,11 +2,12 @@ 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";
import { IconCoin } from "@tabler/icons-react";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
@ -14,10 +15,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<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"
"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
{...props}
>
<IconCoin />
</h1>
</SidebarHeader>
<SidebarContent>
@ -25,9 +27,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavGroup key={props.title} {...props} />
))}
</SidebarContent>
{/* <SidebarFooter>
<NavUser user={sidebarData.user} />
</SidebarFooter> */}
<SidebarRail />
</Sidebar>
);

View File

@ -1,6 +1,7 @@
import {
IconBrowserCheck,
IconBuildingStore,
IconClipboardCheckFilled,
IconCoin,
IconForklift,
IconHelp,
@ -8,14 +9,12 @@ import {
IconNotification,
IconPackage,
IconPalette,
IconPercentage,
IconSettings,
IconTag,
IconTool,
IconUserCog,
IconUsers
IconUsers,
} from "@tabler/icons-react";
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
import { type SidebarData } from "../types";
export const sidebarData: SidebarData = {
@ -26,45 +25,45 @@ 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
icon: IconPackage,
},
{
title: "Inventory",
url: "/dashboard/tasks",
icon: IconForklift
icon: IconForklift,
},
{
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 +75,36 @@ export const sidebarData: SidebarData = {
{
title: "Profile",
url: "/dashboard/settings",
icon: IconUserCog
icon: IconUserCog,
},
{
title: "Account",
url: "/dashboard/settings/account",
icon: IconTool
icon: IconTool,
},
{
title: "Appearance",
url: "/dashboard/settings/appearance",
icon: IconPalette
icon: IconPalette,
},
{
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: IconNotification
icon: IconNotification,
},
{
title: "Display",
url: "/dashboard/settings/display",
icon: IconBrowserCheck
}
]
icon: IconBrowserCheck,
},
],
},
{
title: "Help Center",
url: "/help-center",
icon: IconHelp
}
]
}
]
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

@ -5,7 +5,7 @@ import {
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles
Sparkles,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
@ -15,17 +15,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;
@ -42,7 +42,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">SN</AvatarFallback>
@ -58,7 +59,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

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

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

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,56 +1,56 @@
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>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
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
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
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,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<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

@ -25,7 +25,7 @@ function Calendar({
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"
"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",
@ -37,7 +37,7 @@ function Calendar({
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"
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
),
day_range_end: "day-range-end",
day_selected:
@ -49,7 +49,7 @@ function Calendar({
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
@ -57,7 +57,7 @@ function Calendar({
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
)
),
}}
{...props}
/>

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

@ -22,14 +22,14 @@ const useAuth = () => {
const { data: user } = useQuery<UserPublic | null, Error>({
queryKey: ["currentUser"],
queryFn: authAPI.getCurrentUser,
enabled: loggedIn
enabled: loggedIn,
});
const signUpMutation = useMutation({
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) => {
@ -42,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 = () => {
@ -58,7 +58,7 @@ const useAuth = () => {
toast({ title: "Account updated successfully" });
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
},
onError: (err: ApiError) => handleServerError(err)
onError: (err: ApiError) => handleServerError(err),
});
useEffect(() => {
@ -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

@ -8,22 +8,23 @@ export function useProduct(productId?: number) {
const product = useQuery<ProductWithDetails | null>({
queryKey: ["product", productId],
queryFn: () => productsAPI.getProductById(productId!),
enabled: !!productId
enabled: !!productId,
});
const createProduct = useMutation({
mutationFn: (data: ProductCreate) => productsAPI.createProduct(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] })
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] }),
});
const updateProduct = useMutation({
mutationFn: (data: Partial<ProductCreate>) => productsAPI.updateProduct(productId!, data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] })
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"] })
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] }),
});
return { product, createProduct, updateProduct, deleteProduct };

View File

@ -1,4 +1,3 @@
// src/hooks/useProducts.ts
import { useQuery } from "@tanstack/react-query";
import { productsAPI } from "@/api/api";
import { ProductWithDetails } from "@/api/mock/models";
@ -6,7 +5,7 @@ import { ProductWithDetails } from "@/api/mock/models";
export function useProducts() {
const query = useQuery<ProductWithDetails[]>({
queryKey: ["products"],
queryFn: productsAPI.getProductsForShop
queryFn: productsAPI.getProductsForShop,
});
return query;

View File

@ -0,0 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { PurchaseWithDetails } from "@/api/mock/models";
import { purchaseAPI } from "@/api/api";
export function usePurchase(purchaseId?: number) {
return useQuery<PurchaseWithDetails | null>({
queryKey: ["purchase", purchaseId],
queryFn: () => {
if (purchaseId === undefined) return Promise.resolve(null);
return purchaseAPI.getPurchaseWithDetails(purchaseId);
},
enabled: purchaseId !== undefined,
});
}

View File

@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { Purchase } from "@/api/mock/models";
import { purchaseAPI } from "@/api/api";
export function usePurchases() {
return useQuery<Purchase[]>({
queryKey: ["purchases"],
queryFn: purchaseAPI.getAllPurchases,
});
}

View File

@ -8,15 +8,15 @@ export function useShop() {
const {
data: shop,
isLoading,
isError
isError,
} = useQuery<Shop | null>({
queryKey: ["shop"],
queryFn: shopAPI.getShop
queryFn: shopAPI.getShop,
});
const updateShopMutation = useMutation({
mutationFn: shopAPI.updateShop,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["shop"] })
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["shop"] }),
});
return {
@ -24,6 +24,6 @@ export function useShop() {
isLoading,
isError,
updateShop: updateShopMutation.mutate,
updateStatus: updateShopMutation.status
updateStatus: updateShopMutation.status,
};
}

View File

@ -17,7 +17,7 @@ const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST"
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
@ -62,7 +62,7 @@ const addToRemoveQueue = (toastId: string) => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
@ -74,15 +74,15 @@ const reducer = (state: State, action: Action): State => {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
)
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
@ -104,22 +104,22 @@ const reducer = (state: State, action: Action): State => {
t.id === toastId || toastId === undefined
? {
...t,
open: false
open: false,
}
: t
)
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: []
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId)
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
@ -143,7 +143,7 @@ function toast({ ...props }: Toast) {
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id }
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
@ -155,14 +155,14 @@ function toast({ ...props }: Toast) {
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
}
}
},
},
});
return {
id: id,
dismiss,
update
update,
};
}
@ -182,7 +182,7 @@ function useToast() {
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId })
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}

View File

@ -0,0 +1,38 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { MockUser } from "@/api/mock/models";
import { usersAPI } from "@/api/api";
export function useUser(userId?: number) {
const queryClient = useQueryClient();
const user = useQuery<MockUser | null>({
queryKey: ["user", userId],
queryFn: () => usersAPI.getUserById(userId!),
enabled: !!userId,
});
const createUser = useMutation({
mutationFn: (data: Omit<MockUser, "id" | "created_at" | "updated_at">) =>
usersAPI.createUser(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
});
const updateUser = useMutation({
mutationFn: (data: Partial<Omit<MockUser, "id" | "created_at">>) =>
usersAPI.updateUser(userId!, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["user", userId] });
},
});
const deleteUser = useMutation({
mutationFn: () => usersAPI.deleteUser(userId!),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.removeQueries({ queryKey: ["user", userId] });
},
});
return { user, createUser, updateUser, deleteUser };
}

View File

@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { usersAPI } from "@/api/api";
import { MockUser } from "@/api/mock/models";
export function useUsers() {
return useQuery<MockUser[]>({
queryKey: ["users"],
queryFn: usersAPI.getAllUsers,
});
}

View File

@ -3,152 +3,150 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/* styles.css */
.CollapsibleContent {
overflow: hidden;
}
.CollapsibleContent[data-state='open'] {
animation: slideDown 300ms ease-out;
}
.CollapsibleContent[data-state='closed'] {
animation: slideUp 300ms ease-out;
}
/* styles.css */
.CollapsibleContent {
overflow: hidden;
}
.CollapsibleContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.CollapsibleContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes slideUp {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
}
}
@keyframes slideUp {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
}
}
/* Prevent focus zoom on mobile devices */
@media screen and (max-width: 767px) {
input,
select,
textarea {
font-size: 16px !important;
}
}
/* Prevent focus zoom on mobile devices */
@media screen and (max-width: 767px) {
input,
select,
textarea {
font-size: 16px !important;
}
}
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.faded-bottom {
@apply after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_hsl(var(--background))_70%)] after:md:block;
}
.faded-bottom {
@apply after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_hsl(var(--background))_70%)] after:md:block;
}
}
@layer base {
* {
@apply border-border;
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
html {
@apply overflow-x-hidden;
}
body {
@apply min-h-svh w-full bg-background text-foreground;
}
* {
@apply border-border;
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
html {
@apply overflow-x-hidden;
}
body {
@apply min-h-svh w-full bg-background text-foreground;
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -12,13 +12,13 @@ export const registerUser = async (userData: {
`${API_BASE_URL}/user/register`,
{
...userData,
shop_id: 0
shop_id: 0,
},
{
headers: {
"Content-Type": "application/json"
}
}
"Content-Type": "application/json",
},
},
);
return response.data;
};

View File

@ -4,7 +4,7 @@ import { AxiosError } from "axios";
import {
QueryCache,
QueryClient,
QueryClientProvider
QueryClientProvider,
} from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { useAuthStore } from "@/stores/authStore";
@ -38,7 +38,7 @@ const queryClient = new QueryClient({
);
},
refetchOnWindowFocus: import.meta.env.PROD,
staleTime: 10 * 1000 // 10s
staleTime: 10 * 1000, // 10s
},
mutations: {
onError: (error) => {
@ -48,12 +48,12 @@ const queryClient = new QueryClient({
if (error.response?.status === 304) {
toast({
variant: "destructive",
title: "Content not modified!"
title: "Content not modified!",
});
}
}
}
}
},
},
},
queryCache: new QueryCache({
onError: (error) => {
@ -61,7 +61,7 @@ const queryClient = new QueryClient({
if (error.response?.status === 401) {
toast({
variant: "destructive",
title: "Session expired!"
title: "Session expired!",
});
useAuthStore.getState().auth.reset();
const redirect = `${router.history.location.href}`;
@ -70,7 +70,7 @@ const queryClient = new QueryClient({
if (error.response?.status === 500) {
toast({
variant: "destructive",
title: "Internal Server Error!"
title: "Internal Server Error!",
});
router.navigate({ to: "/500" });
}
@ -78,8 +78,8 @@ const queryClient = new QueryClient({
// router.navigate("/forbidden", { replace: true });
}
}
}
})
},
}),
});
// Create a new router instance
@ -87,7 +87,7 @@ const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: "intent",
defaultPreloadStaleTime: 0
defaultPreloadStaleTime: 0,
});
// Register the router instance for type safety
@ -110,6 +110,6 @@ if (!rootElement.innerHTML) {
</FontProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
</StrictMode>,
);
}

View File

@ -1,4 +1,4 @@
import {AdminAppName} from "@/config/manifest"
import { AdminAppName } from "@/config/manifest";
interface Props {
children: React.ReactNode;
@ -17,7 +17,8 @@ export default function AuthLayout({ children }: Props) {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-6 w-6">
className="mr-2 h-6 w-6"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
<h1 className="text-xl font-medium">{AdminAppName}</h1>

View File

@ -10,7 +10,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@ -20,7 +20,7 @@ const formSchema = z.object({
email: z
.string()
.min(1, { message: "Please enter your email" })
.email({ message: "Invalid email address" })
.email({ message: "Invalid email address" }),
});
export function ForgotForm({ className, ...props }: ForgotFormProps) {
@ -28,7 +28,7 @@ export function ForgotForm({ className, ...props }: ForgotFormProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "" }
defaultValues: { email: "" },
});
function onSubmit(data: z.infer<typeof formSchema>) {

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