first commit

This commit is contained in:
Jelaletdin12
2026-02-01 20:55:57 +05:00
commit b8c871750a
128 changed files with 23114 additions and 0 deletions

170
lib/api.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}