fixed some bugs
This commit is contained in:
172
lib/api.ts
172
lib/api.ts
@@ -1,61 +1,24 @@
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios"
|
||||
// lib/api.ts
|
||||
|
||||
/**
|
||||
* Token management utilities
|
||||
*/
|
||||
const getTokenFromCookie = (name: string): string | null => {
|
||||
if (typeof document === "undefined") return null
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) return parts.pop()?.split(";").shift() || null
|
||||
return null
|
||||
}
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios";
|
||||
import TokenStorage from "./tokenStorage";
|
||||
|
||||
const setTokenInCookie = (name: string, token: string): void => {
|
||||
if (typeof document === "undefined") return
|
||||
document.cookie = `${name}=${token}; path=/; secure; SameSite=Strict; max-age=2592000`
|
||||
}
|
||||
|
||||
const removeTokenFromCookie = (name: string): void => {
|
||||
if (typeof document === "undefined") return
|
||||
document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;`
|
||||
}
|
||||
|
||||
const getToken = (): string | null => {
|
||||
const authToken = getTokenFromCookie("authToken")
|
||||
if (authToken) return authToken
|
||||
|
||||
const guestToken = getTokenFromCookie("guestToken")
|
||||
if (guestToken) return guestToken
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Map internal locale codes to API language codes
|
||||
*/
|
||||
const localeToApiLang = (locale: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
tm: "tk",
|
||||
ru: "ru",
|
||||
}
|
||||
return mapping[locale] || locale
|
||||
}
|
||||
const mapping: Record<string, string> = { tm: "tk", ru: "ru" };
|
||||
return mapping[locale] || locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* Centralized API client with interceptors
|
||||
*/
|
||||
class APIClient {
|
||||
private client: AxiosInstance
|
||||
private baseUrl: string
|
||||
private isRefreshing = false
|
||||
private client: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
private isRefreshing = false;
|
||||
private failedQueue: Array<{
|
||||
resolve: (value?: unknown) => void
|
||||
reject: (reason?: unknown) => void
|
||||
}> = []
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}> = [];
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "https://api.example.com"
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "https://api.example.com";
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: `${this.baseUrl}/api/v1`,
|
||||
@@ -64,64 +27,60 @@ class APIClient {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Token": process.env.NEXT_PUBLIC_API_TOKEN || "123",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
this.setupInterceptors()
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
// Request interceptor
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = getToken()
|
||||
const token = TokenStorage.getActiveToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Add language parameter
|
||||
let lang = "tk" // default fallback
|
||||
let lang = "tk";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// Try to get from i18n
|
||||
if ((window as any).i18n?.language) {
|
||||
lang = localeToApiLang((window as any).i18n.language)
|
||||
}
|
||||
// Try to get from pathname as fallback
|
||||
else {
|
||||
const pathLocale = window.location.pathname.split("/")[1]
|
||||
lang = localeToApiLang((window as any).i18n.language);
|
||||
} else {
|
||||
const pathLocale = window.location.pathname.split("/")[1];
|
||||
if (pathLocale === "tm" || pathLocale === "ru") {
|
||||
lang = localeToApiLang(pathLocale)
|
||||
lang = localeToApiLang(pathLocale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = config.url || ""
|
||||
const separator = url.includes("?") ? "&" : "?"
|
||||
config.url = `${url}${separator}lang=${lang}`
|
||||
const url = config.url || "";
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
config.url = `${url}${separator}lang=${lang}`;
|
||||
|
||||
return config
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Handle 401 errors
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (this.isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.failedQueue.push({ resolve, reject })
|
||||
this.failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then(() => this.client(originalRequest))
|
||||
.catch((err) => Promise.reject(err))
|
||||
.catch((err) => Promise.reject(err));
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
this.isRefreshing = true
|
||||
originalRequest._retry = true;
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const guestTokenResponse = await axios.post(
|
||||
@@ -133,30 +92,29 @@ class APIClient {
|
||||
"Api-Token": process.env.NEXT_PUBLIC_API_TOKEN || "123",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const newToken = guestTokenResponse.data?.token || guestTokenResponse.data?.data
|
||||
const newToken = guestTokenResponse.data?.token || guestTokenResponse.data?.data;
|
||||
|
||||
if (newToken) {
|
||||
setTokenInCookie("guestToken", newToken)
|
||||
this.processQueue(null)
|
||||
return this.client(originalRequest)
|
||||
TokenStorage.setGuestToken(newToken);
|
||||
this.processQueue(null);
|
||||
return this.client(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
this.processQueue(refreshError)
|
||||
this.clearAuthToken()
|
||||
this.processQueue(refreshError);
|
||||
TokenStorage.clearTokens();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/login"
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
return Promise.reject(refreshError)
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
this.isRefreshing = false
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HTML error responses
|
||||
if (
|
||||
error.response?.data &&
|
||||
typeof error.response.data === "string" &&
|
||||
@@ -168,64 +126,44 @@ class APIClient {
|
||||
...error.response,
|
||||
data: { message: "Server returned HTML instead of JSON" },
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private processQueue(error: any): void {
|
||||
this.failedQueue.forEach((promise) => {
|
||||
if (error) {
|
||||
promise.reject(error)
|
||||
promise.reject(error);
|
||||
} else {
|
||||
promise.resolve()
|
||||
promise.resolve();
|
||||
}
|
||||
})
|
||||
this.failedQueue = []
|
||||
});
|
||||
this.failedQueue = [];
|
||||
}
|
||||
|
||||
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.get<T>(url, config)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
setAuthToken(token: string): void {
|
||||
removeTokenFromCookie("guestToken")
|
||||
setTokenInCookie("authToken", token)
|
||||
this.client.defaults.headers.common["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
setGuestToken(token: string): void {
|
||||
setTokenInCookie("guestToken", token)
|
||||
this.client.defaults.headers.common["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
clearAuthToken(): void {
|
||||
removeTokenFromCookie("authToken")
|
||||
removeTokenFromCookie("guestToken")
|
||||
delete this.client.defaults.headers.common["Authorization"]
|
||||
return this.client.delete<T>(url, config);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new APIClient()
|
||||
export const setAuthToken = (token: string) => apiClient.setAuthToken(token)
|
||||
export const setGuestToken = (token: string) => apiClient.setGuestToken(token)
|
||||
export const clearAuthToken = () => apiClient.clearAuthToken()
|
||||
export const apiClient = new APIClient();
|
||||
@@ -1,23 +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/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/cart/hooks/useAddresses";
|
||||
export * from "../../features/cart/hooks/usePaymentTypes";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export * from "../../features/home/hooks/useMedia"
|
||||
export * from "../../features/home/hooks/useCollections"
|
||||
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"
|
||||
export type {
|
||||
Product,
|
||||
Category,
|
||||
Cart,
|
||||
CartItem,
|
||||
Order,
|
||||
Favorite,
|
||||
Banner,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// lib/hooks/useAuth.ts
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
import { apiClient, setAuthToken, clearAuthToken, setGuestToken } from "@/lib/api";
|
||||
import { apiClient } from "@/lib/api";
|
||||
import TokenStorage from "@/lib/tokenStorage";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// ==================== TYPES ====================
|
||||
interface LoginCredentials {
|
||||
@@ -30,59 +34,131 @@ interface AuthResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== AUTH STATUS ====================
|
||||
const getTokenFromCookie = (name: string): string | null => {
|
||||
if (typeof document === "undefined") return null;
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(";").shift() || null;
|
||||
return null;
|
||||
};
|
||||
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(() => {
|
||||
const authToken = getTokenFromCookie("authToken");
|
||||
setIsAuthenticated(!!authToken);
|
||||
setIsAuthenticated(TokenStorage.hasAuthToken());
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
};
|
||||
return { isAuthenticated, isLoading };
|
||||
}
|
||||
|
||||
// ==================== GUEST TOKEN ====================
|
||||
export function useGetGuestToken() {
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>("/auth/guest-token", {});
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const token = data?.token || data?.data;
|
||||
if (token) {
|
||||
setGuestToken(token);
|
||||
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);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Guest token hatası:", 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<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>("/auth/login", credentials);
|
||||
return response.data;
|
||||
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) => {
|
||||
console.error("Login hatası:", error);
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Login] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -92,19 +168,22 @@ export function useRegister() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (userData: RegisterData): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>("/auth/register", userData);
|
||||
return response.data;
|
||||
mutationFn: async (userData: RegisterData): Promise<string> => {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/register",
|
||||
userData,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
return extractToken(response.data);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const token = data?.token || data?.data;
|
||||
if (token) {
|
||||
setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
}
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Register hatası:", error);
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Register] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -114,19 +193,22 @@ export function useVerifyToken() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (verifyData: VerifyTokenData): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>("/auth/verify", verifyData);
|
||||
return response.data;
|
||||
mutationFn: async (verifyData: VerifyTokenData): Promise<string> => {
|
||||
const response = await apiClient.post<AuthResponse>(
|
||||
"/auth/verify",
|
||||
verifyData,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
return extractToken(response.data);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const token = data?.data || data?.token;
|
||||
if (token) {
|
||||
setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
}
|
||||
onSuccess: (token) => {
|
||||
TokenStorage.setAuthToken(token);
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-status"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Verify hatası:", error);
|
||||
const authError = handleAuthError(error);
|
||||
console.error("[Verify] Failed:", authError);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -138,23 +220,28 @@ export function useLogout() {
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<void> => {
|
||||
try {
|
||||
await apiClient.post("/auth/logout");
|
||||
await apiClient.post("/auth/logout", {}, { timeout: 5000 });
|
||||
} catch (error) {
|
||||
console.warn("Logout endpoint çalışmadı:", error);
|
||||
// Logout should succeed even if server call fails
|
||||
console.warn("[Logout] Server call failed, clearing local state anyway");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
clearAuthToken();
|
||||
TokenStorage.clearTokens();
|
||||
queryClient.clear();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/login";
|
||||
window.location.href = "/";
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Logout hatası:", error);
|
||||
clearAuthToken();
|
||||
onError: () => {
|
||||
// Always clear local state on logout
|
||||
TokenStorage.clearTokens();
|
||||
queryClient.clear();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Debounce function for handling rapid state changes
|
||||
* @param func - Function to debounce
|
||||
* @param delay - Delay in milliseconds
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(func: T, delay: number): (...args: Parameters<T>) => void {
|
||||
let timeoutId: ReturnType<typeof setTimeout>
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => func(...args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function for rate-limiting function calls
|
||||
* @param func - Function to throttle
|
||||
* @param limit - Minimum time between calls
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(func: T, limit: number): (...args: Parameters<T>) => void {
|
||||
let lastRun = 0
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
const now = Date.now()
|
||||
if (now - lastRun >= limit) {
|
||||
func(...args)
|
||||
lastRun = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility for simulating delays
|
||||
* @param ms - Milliseconds to sleep
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate loading state
|
||||
* @param duration - Duration of loading state
|
||||
*/
|
||||
export async function simulateLoading(duration = 500): Promise<void> {
|
||||
return sleep(duration)
|
||||
}
|
||||
57
lib/tokenStorage.ts
Normal file
57
lib/tokenStorage.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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); // Auth token replaces guest token
|
||||
}
|
||||
|
||||
static setGuestToken(token: string): void {
|
||||
if (!this.isClient) return;
|
||||
// Only set guest token if no auth token exists
|
||||
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;
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Centralized error handling utility
|
||||
* Converts API errors to user-friendly messages
|
||||
*/
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
message?: string
|
||||
errors?: Record<string, string[]>
|
||||
status?: number
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: any): string {
|
||||
if (!error) return "An unexpected error occurred"
|
||||
|
||||
// Axios error
|
||||
if (error.response?.data?.message) {
|
||||
return error.response.data.message
|
||||
}
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
return "Please log in to continue"
|
||||
}
|
||||
|
||||
if (error.response?.status === 403) {
|
||||
return "You don't have permission to perform this action"
|
||||
}
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
return "The requested resource was not found"
|
||||
}
|
||||
|
||||
if (error.response?.status === 500) {
|
||||
return "Server error occurred. Please try again later"
|
||||
}
|
||||
|
||||
if (error.message === "Network Error") {
|
||||
return "Network connection error. Please check your internet connection"
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error
|
||||
}
|
||||
|
||||
return "An error occurred. Please try again"
|
||||
}
|
||||
|
||||
export function getValidationErrors(error: any): Record<string, string> {
|
||||
if (error.response?.data?.errors && typeof error.response.data.errors === "object") {
|
||||
const errors: Record<string, string> = {}
|
||||
for (const [key, messages] of Object.entries(error.response.data.errors)) {
|
||||
errors[key] = Array.isArray(messages) ? messages[0] : String(messages)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function isNetworkError(error: any): boolean {
|
||||
return error?.message === "Network Error" || !error?.response
|
||||
}
|
||||
|
||||
export function isUnauthorized(error: any): boolean {
|
||||
return error?.response?.status === 401
|
||||
}
|
||||
|
||||
export function isForbidden(error: any): boolean {
|
||||
return error?.response?.status === 403
|
||||
}
|
||||
|
||||
export function isNotFound(error: any): boolean {
|
||||
return error?.response?.status === 404
|
||||
}
|
||||
|
||||
export function isServerError(error: any): boolean {
|
||||
return error?.response?.status >= 500
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Loading state utilities for better UX
|
||||
*/
|
||||
|
||||
export const loadingMessages = {
|
||||
fetching: "Loading...",
|
||||
submitting: "Processing...",
|
||||
deleting: "Deleting...",
|
||||
updating: "Updating...",
|
||||
saving: "Saving...",
|
||||
cart: "Adding to cart...",
|
||||
checkout: "Processing order...",
|
||||
} as const
|
||||
|
||||
export const skeletonCounts = {
|
||||
products: 10,
|
||||
categories: 6,
|
||||
cartItems: 3,
|
||||
orders: 6,
|
||||
reviews: 4,
|
||||
} as const
|
||||
Reference in New Issue
Block a user