fixed some bugs

This commit is contained in:
Jelaletdin12
2025-12-18 23:19:45 +05:00
parent 6d0064b106
commit 0fb4e2765c
36 changed files with 1430 additions and 1485 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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