first commit
This commit is contained in:
170
lib/api.ts
Normal file
170
lib/api.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// lib/api.ts
|
||||
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios";
|
||||
import TokenStorage from "./tokenStorage";
|
||||
|
||||
const localeToApiLang = (locale: string): string => {
|
||||
const mapping: Record<string, string> = { tm: "tk", ru: "ru" };
|
||||
return mapping[locale] || locale;
|
||||
};
|
||||
|
||||
class APIClient {
|
||||
private client: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
private isRefreshing = false;
|
||||
private failedQueue: Array<{
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}> = [];
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "https://api.example.com";
|
||||
console.log("API URL:", this.baseUrl);
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: `${this.baseUrl}/api/v1`,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Token": process.env.NEXT_PUBLIC_API_TOKEN || "123",
|
||||
},
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
// Request interceptor
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = TokenStorage.getActiveToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Add language parameter
|
||||
let lang = "tm";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
if ((window as any).i18n?.language) {
|
||||
lang = localeToApiLang((window as any).i18n.language);
|
||||
} else {
|
||||
const pathLocale = window.location.pathname.split("/")[1];
|
||||
if (pathLocale === "tm" || pathLocale === "ru") {
|
||||
lang = localeToApiLang(pathLocale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = config.url || "";
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
config.url = `${url}${separator}lang=${lang}`;
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (this.isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then(() => this.client(originalRequest))
|
||||
.catch((err) => Promise.reject(err));
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const guestTokenResponse = await axios.post(
|
||||
`${this.baseUrl}/api/v1/auth/guest-token`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Token": process.env.NEXT_PUBLIC_API_TOKEN || "123",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const newToken = guestTokenResponse.data?.token || guestTokenResponse.data?.data;
|
||||
|
||||
if (newToken) {
|
||||
TokenStorage.setGuestToken(newToken);
|
||||
this.processQueue(null);
|
||||
return this.client(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
this.processQueue(refreshError);
|
||||
TokenStorage.clearTokens();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
error.response?.data &&
|
||||
typeof error.response.data === "string" &&
|
||||
error.response.data.includes("<!DOCTYPE html>")
|
||||
) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
response: {
|
||||
...error.response,
|
||||
data: { message: "Server returned HTML instead of JSON" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private processQueue(error: any): void {
|
||||
this.failedQueue.forEach((promise) => {
|
||||
if (error) {
|
||||
promise.reject(error);
|
||||
} else {
|
||||
promise.resolve();
|
||||
}
|
||||
});
|
||||
this.failedQueue = [];
|
||||
}
|
||||
|
||||
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.get<T>(url, config);
|
||||
}
|
||||
|
||||
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.post<T>(url, data, config);
|
||||
}
|
||||
|
||||
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.put<T>(url, data, config);
|
||||
}
|
||||
|
||||
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.patch<T>(url, data, config);
|
||||
}
|
||||
|
||||
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.delete<T>(url, config);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new APIClient();
|
||||
25
lib/hooks/index.ts
Normal file
25
lib/hooks/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export * from "../../features/products/hooks/useProducts";
|
||||
export * from "../../features/category/hooks/useCategories";
|
||||
export * from "../../features/cart/hooks/useCart";
|
||||
export * from "../../features/favorites/hooks/useFavorites";
|
||||
export * from "../../features/orders/hooks/useOrders";
|
||||
export * from "../../features/search/hooks/useSearch";
|
||||
export * from "../../features/profile/hooks/useUserProfile";
|
||||
export * from "../../features/openStore/hooks/useOpenStore";
|
||||
|
||||
export * from "../../features/cart/hooks/useAddresses";
|
||||
export * from "../../features/cart/hooks/usePaymentTypes";
|
||||
|
||||
export * from "../../features/home/hooks/useMedia";
|
||||
export * from "../../features/home/hooks/useCollections";
|
||||
|
||||
// Export types
|
||||
export type {
|
||||
Product,
|
||||
Category,
|
||||
Cart,
|
||||
CartItem,
|
||||
Order,
|
||||
Favorite,
|
||||
Banner,
|
||||
} from "@/lib/types/api";
|
||||
237
lib/hooks/useAuth.ts
Normal file
237
lib/hooks/useAuth.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
// lib/hooks/useAuth.ts
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
import { apiClient } from "@/lib/api";
|
||||
import TokenStorage from "@/lib/tokenStorage";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// ==================== TYPES ====================
|
||||
interface LoginCredentials {
|
||||
phone_number: number;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface RegisterData {
|
||||
phone_number: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface VerifyTokenData {
|
||||
phone_number: number;
|
||||
code: number;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token?: string;
|
||||
data?: string;
|
||||
user?: {
|
||||
id: string;
|
||||
phone_number: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AuthError {
|
||||
message: string;
|
||||
code?: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
// ==================== UTILITIES ====================
|
||||
function extractToken(data: AuthResponse): string {
|
||||
// Enforce consistent token extraction
|
||||
const token = data.token || data.data;
|
||||
if (!token) {
|
||||
throw new Error("No token received from server");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function handleAuthError(error: unknown): AuthError {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return {
|
||||
message: "Request timeout - server not responding",
|
||||
code: "TIMEOUT",
|
||||
statusCode: 408
|
||||
};
|
||||
}
|
||||
if (error.response) {
|
||||
return {
|
||||
message: error.response.data?.message || "Authentication failed",
|
||||
code: error.response.data?.code || "AUTH_ERROR",
|
||||
statusCode: error.response.status
|
||||
};
|
||||
}
|
||||
if (error.request) {
|
||||
return {
|
||||
message: "Network error - cannot reach server",
|
||||
code: "NETWORK_ERROR",
|
||||
statusCode: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== AUTH STATUS ====================
|
||||
export function useAuthStatus() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAuthenticated(TokenStorage.hasAuthToken());
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return { isAuthenticated, isLoading };
|
||||
}
|
||||
|
||||
// ==================== GUEST TOKEN ====================
|
||||
export function useGetGuestToken() {
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<string> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/guest-token",
|
||||
{},
|
||||
{
|
||||
signal: controller.signal,
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
clearTimeout(timeoutId);
|
||||
return extractToken(response.data);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw handleAuthError(error);
|
||||
}
|
||||
},
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setGuestToken(token);
|
||||
},
|
||||
onError: (error: AuthError) => {
|
||||
console.error("[Guest Token] Failed:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
statusCode: error.statusCode
|
||||
});
|
||||
},
|
||||
retry: (failureCount, error) => {
|
||||
const authError = error as AuthError;
|
||||
// Retry on network errors, not on auth errors
|
||||
if (authError.code === "NETWORK_ERROR" || authError.code === "TIMEOUT") {
|
||||
return failureCount < 2;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 5000),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== LOGIN ====================
|
||||
export function useLogin() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (credentials: LoginCredentials): Promise<string> => {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/login",
|
||||
credentials,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
return extractToken(response.data);
|
||||
},
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Login] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== REGISTER ====================
|
||||
export function useRegister() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (userData: RegisterData): Promise<string> => {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/register",
|
||||
userData,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
return extractToken(response.data);
|
||||
},
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Register] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== VERIFY TOKEN ====================
|
||||
export function useVerifyToken() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (verifyData: VerifyTokenData): Promise<string> => {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/verify",
|
||||
verifyData,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
return extractToken(response.data);
|
||||
},
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Verify] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== LOGOUT ====================
|
||||
export function useLogout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<void> => {
|
||||
try {
|
||||
await apiClient.post("/auth/logout", {}, { timeout: 5000 });
|
||||
} catch (error) {
|
||||
console.warn("[Logout] Server call failed, clearing local state anyway");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
TokenStorage.clearTokens();
|
||||
queryClient.clear();
|
||||
},
|
||||
onError: () => {
|
||||
TokenStorage.clearTokens();
|
||||
queryClient.clear();
|
||||
},
|
||||
});
|
||||
}
|
||||
29
lib/i18n-utils.ts
Normal file
29
lib/i18n-utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { useLocale } from "next-intl"
|
||||
|
||||
export function useLocaleInfo() {
|
||||
const locale = useLocale()
|
||||
|
||||
return {
|
||||
locale,
|
||||
isRussian: locale === "ru",
|
||||
isTurkmen: locale === "tm",
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocaleFlag(locale: string) {
|
||||
const flags: Record<string, string> = {
|
||||
ru: "🇷🇺",
|
||||
tm: "🇹🇲",
|
||||
}
|
||||
return flags[locale] || "🌐"
|
||||
}
|
||||
|
||||
export function getLocaleName(locale: string) {
|
||||
const names: Record<string, string> = {
|
||||
ru: "Русский",
|
||||
tm: "Türkmençe",
|
||||
}
|
||||
return names[locale] || locale.toUpperCase()
|
||||
}
|
||||
19
lib/queryClient.ts
Normal file
19
lib/queryClient.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { QueryClient } from "@tanstack/react-query"
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
|
||||
retry: 1,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: true,
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
},
|
||||
},
|
||||
})
|
||||
56
lib/tokenStorage.ts
Normal file
56
lib/tokenStorage.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// lib/services/tokenStorage.ts
|
||||
|
||||
/**
|
||||
* Centralized token storage using localStorage only
|
||||
* Single source of truth for all token operations
|
||||
*/
|
||||
|
||||
const AUTH_TOKEN_KEY = "authToken";
|
||||
const GUEST_TOKEN_KEY = "guestToken";
|
||||
|
||||
class TokenStorage {
|
||||
private static isClient = typeof window !== "undefined";
|
||||
|
||||
static getAuthToken(): string | null {
|
||||
if (!this.isClient) return null;
|
||||
return localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
static getGuestToken(): string | null {
|
||||
if (!this.isClient) return null;
|
||||
return localStorage.getItem(GUEST_TOKEN_KEY);
|
||||
}
|
||||
|
||||
static getActiveToken(): string | null {
|
||||
return this.getAuthToken() || this.getGuestToken();
|
||||
}
|
||||
|
||||
static setAuthToken(token: string): void {
|
||||
if (!this.isClient) return;
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||
localStorage.removeItem(GUEST_TOKEN_KEY);
|
||||
}
|
||||
|
||||
static setGuestToken(token: string): void {
|
||||
if (!this.isClient) return;
|
||||
if (!this.getAuthToken()) {
|
||||
localStorage.setItem(GUEST_TOKEN_KEY, token);
|
||||
}
|
||||
}
|
||||
|
||||
static clearTokens(): void {
|
||||
if (!this.isClient) return;
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
localStorage.removeItem(GUEST_TOKEN_KEY);
|
||||
}
|
||||
|
||||
static hasAuthToken(): boolean {
|
||||
return !!this.getAuthToken();
|
||||
}
|
||||
|
||||
static hasAnyToken(): boolean {
|
||||
return !!this.getActiveToken();
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenStorage;
|
||||
524
lib/types/api.ts
Normal file
524
lib/types/api.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
/**
|
||||
* API Response and Entity Type Definitions
|
||||
*/
|
||||
|
||||
// Product Types
|
||||
export interface ProductMedia {
|
||||
thumbnail: string;
|
||||
images_400x400: string;
|
||||
images_720x720: string;
|
||||
images_800x800: string;
|
||||
images_1200x1200: string;
|
||||
|
||||
}
|
||||
|
||||
export interface Carousel {
|
||||
title: string
|
||||
image: string
|
||||
url?: string | null
|
||||
thumbnail: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: number;
|
||||
rating: number;
|
||||
title: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";
|
||||
|
||||
export interface PaymentType {
|
||||
id: number;
|
||||
name: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface ProductProperty {
|
||||
attribute_id: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProductReviews {
|
||||
count: number;
|
||||
rating: string;
|
||||
}
|
||||
|
||||
export interface ProductBrand {
|
||||
id: number | null;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface ProductChannel {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProductCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
parent_id: number | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
sku: string | null;
|
||||
barcode: string;
|
||||
stock: number;
|
||||
price_amount: string;
|
||||
old_price_amount: string | null;
|
||||
backorder: string;
|
||||
weight_value: number | null;
|
||||
weight_unit: string | null;
|
||||
height_value: number | null;
|
||||
height_unit: string | null;
|
||||
media: ProductMedia[];
|
||||
created_at: string;
|
||||
seo_title: string | null;
|
||||
seo_description: string | null;
|
||||
is_visible: boolean;
|
||||
colour: string | null;
|
||||
size: string | null;
|
||||
available_colors?: string[];
|
||||
available_sizes?: string[];
|
||||
brand: ProductBrand;
|
||||
channel?: ProductChannel[];
|
||||
properties?: ProductProperty[];
|
||||
variations?: any[];
|
||||
reviews: ProductReviews;
|
||||
reviews_resources?: any[];
|
||||
categories?: ProductCategory[];
|
||||
}
|
||||
|
||||
// Category Types
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
image: string;
|
||||
parent_id?: number | null;
|
||||
children?: Category[];
|
||||
media:ProductMedia[];
|
||||
|
||||
}
|
||||
|
||||
// Collection Types
|
||||
export interface Collection {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
created_at?: string;
|
||||
media?: ProductMedia[];
|
||||
}
|
||||
|
||||
// Cart Types
|
||||
export interface CartProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
price_amount: string;
|
||||
old_price_amount: string | null;
|
||||
media?: ProductMedia[];
|
||||
channel?: ProductChannel[];
|
||||
stock: number;
|
||||
image?: string;
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
id: number;
|
||||
product_id: number;
|
||||
product: CartProduct;
|
||||
product_quantity: number;
|
||||
seller?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
quantity: number;
|
||||
price: number;
|
||||
total: number;
|
||||
price_formatted: string;
|
||||
sub_total_formatted: string;
|
||||
total_formatted: string;
|
||||
discount_formatted: string;
|
||||
}
|
||||
|
||||
export interface CartResponse {
|
||||
message?: string;
|
||||
data: CartItem[];
|
||||
count?: number;
|
||||
total?: number;
|
||||
total_formatted?: string;
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
id: string;
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
total_formatted?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
// Favorites Types
|
||||
export interface Favorite {
|
||||
id?: number;
|
||||
product_id: number;
|
||||
product: Product;
|
||||
added_at?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
// Order Types
|
||||
export interface OrderProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
thumbnail: string;
|
||||
images_400x400: string;
|
||||
images_800x800: string;
|
||||
images_1200x1200: string;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
product: OrderProduct;
|
||||
order: {
|
||||
id: number;
|
||||
};
|
||||
quantity: number;
|
||||
unit_price_amount: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
status: string;
|
||||
shipping_method: string;
|
||||
notes: string | null;
|
||||
customer_name: string;
|
||||
customer_phone: string;
|
||||
customer_address: string;
|
||||
delivery_time: string;
|
||||
delivery_at: string;
|
||||
region: string;
|
||||
user_id: number;
|
||||
province_id: number | null;
|
||||
payment_type: string;
|
||||
orderItems: OrderItem[];
|
||||
}
|
||||
|
||||
export interface OrdersResponse {
|
||||
message: string;
|
||||
data: Order[];
|
||||
pagination: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
count: number;
|
||||
first_page_url: string;
|
||||
next_page_url: string | null;
|
||||
prev_page_url: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateOrderRequest {
|
||||
customer_name: string;
|
||||
customer_phone: string;
|
||||
customer_address: string;
|
||||
shipping_method: string;
|
||||
payment_type_id: number;
|
||||
delivery_time?: string;
|
||||
delivery_at?: string;
|
||||
region: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderPayload {
|
||||
customer_name?: string;
|
||||
customer_phone?: string;
|
||||
customer_address: string;
|
||||
shipping_method: string;
|
||||
payment_type_id: number;
|
||||
delivery_time?: string;
|
||||
delivery_at?: string;
|
||||
region: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// Pagination Types
|
||||
export interface Pagination {
|
||||
page: number;
|
||||
perPage: number;
|
||||
count: number;
|
||||
first_page_url?: string;
|
||||
next_page_url?: string | null;
|
||||
prev_page_url?: string | null;
|
||||
current_page?: number;
|
||||
last_page?: number;
|
||||
per_page?: number;
|
||||
total?: number;
|
||||
hasMorePages?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
message?: string;
|
||||
data: T[];
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
// Search Types
|
||||
export interface SearchFilters {
|
||||
q?: string;
|
||||
category_id?: number;
|
||||
brand_id?: number;
|
||||
price_from?: number;
|
||||
price_to?: number;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
products: Product[];
|
||||
total: number;
|
||||
filters?: {
|
||||
brands: Array<{ id: number; name: string }>;
|
||||
categories: Array<{ id: number; name: string }>;
|
||||
price_range: { min: number; max: number };
|
||||
};
|
||||
}
|
||||
|
||||
// User Profile Types
|
||||
export interface UserProfile {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface ProfileResponse {
|
||||
message: string;
|
||||
data: UserProfile;
|
||||
}
|
||||
|
||||
export interface UpdateProfileRequest {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
phone_number?: string;
|
||||
address?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileResponse {
|
||||
message: string;
|
||||
data: UserProfile;
|
||||
}
|
||||
|
||||
// Auth Types
|
||||
export interface AuthResponse {
|
||||
token: string;
|
||||
user: UserProfile;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
phone_number: string;
|
||||
}
|
||||
|
||||
export interface VerifyTokenRequest {
|
||||
phone_number: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface VerifyTokenResponse {
|
||||
message: string;
|
||||
token: string;
|
||||
user: UserProfile;
|
||||
}
|
||||
|
||||
// Banner Types
|
||||
export interface Banner {
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
thumbnail?: string;
|
||||
link?: string;
|
||||
url?: string;
|
||||
type?: string;
|
||||
place?: string;
|
||||
}
|
||||
|
||||
// Region and Province Types
|
||||
export interface Region {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export interface Province {
|
||||
id: number;
|
||||
name: string;
|
||||
region: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
// Address Types
|
||||
export interface Address {
|
||||
id: number;
|
||||
title: string;
|
||||
region_id: number;
|
||||
address: string;
|
||||
phone?: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
// Payment Type Options
|
||||
export interface PaymentTypeOption {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// Shipping Method Types
|
||||
export interface ShippingMethod {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// Generic API Error Response
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
errors?: Record<string, string[]>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// API Response Wrapper
|
||||
export interface ApiResponse<T = any> {
|
||||
message?: string;
|
||||
data?: T;
|
||||
error?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
// Add to Cart Request
|
||||
export interface AddToCartRequest {
|
||||
productId: number;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
// Update Cart Item Quantity Request
|
||||
export interface UpdateCartItemQuantityRequest {
|
||||
productId: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
// Remove from Cart Request
|
||||
export interface RemoveFromCartRequest {
|
||||
productId: number;
|
||||
}
|
||||
|
||||
// Add to Favorites Request
|
||||
export interface AddToFavoritesRequest {
|
||||
productId: number;
|
||||
}
|
||||
|
||||
// Remove from Favorites Request
|
||||
export interface RemoveFromFavoritesRequest {
|
||||
productId: number;
|
||||
}
|
||||
|
||||
// Cancel Order Request
|
||||
export interface CancelOrderRequest {
|
||||
orderId: number;
|
||||
}
|
||||
|
||||
// Order Summary for Cart Page
|
||||
export interface OrderBillingItem {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface OrderBilling {
|
||||
body: OrderBillingItem[];
|
||||
footer: {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrderSummary {
|
||||
id: number;
|
||||
seller: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
items: CartItem[];
|
||||
billing: OrderBilling;
|
||||
}
|
||||
|
||||
// Category Products Response
|
||||
export interface CategoryProductsResponse {
|
||||
message?: string;
|
||||
data: Product[];
|
||||
pagination?: Pagination;
|
||||
}
|
||||
|
||||
// Query Options for Hooks
|
||||
export interface QueryOptions {
|
||||
enabled?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
refetchOnWindowFocus?: boolean;
|
||||
refetchOnMount?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
// User Store Data
|
||||
export interface UserOrderData {
|
||||
customer_name: string;
|
||||
customer_phone: string;
|
||||
customer_address?: string;
|
||||
}
|
||||
|
||||
// lib/types/api.ts içine eklenecek tipler
|
||||
|
||||
export interface FilterBrand {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FilterCategory {
|
||||
id: number;
|
||||
parent_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FiltersResponse {
|
||||
message: string;
|
||||
data: {
|
||||
categories: FilterCategory[];
|
||||
brands: FilterBrand[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface ProductFilters {
|
||||
brands?: number[];
|
||||
categories?: number[];
|
||||
min_price?: number;
|
||||
max_price?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
collection_id?: number;
|
||||
}
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user