This commit is contained in:
NotII
2025-04-07 19:25:24 +01:00
parent 7f7dd78818
commit 2f48ee38c2
102 changed files with 1825 additions and 761 deletions

82
lib/README.md Normal file
View File

@@ -0,0 +1,82 @@
# API & Utilities Organization
This directory contains the API client and utility functions used throughout the application.
## Directory Structure
```
lib/
├─ api.ts # Main API entry point
├─ api-client.ts # Client-side API functions
├─ server-api.ts # Server-side API functions
├─ services/ # Service modules
│ ├─ index.ts # Service re-exports
│ ├─ product-service.ts # Product API
│ ├─ shipping-service.ts # Shipping API
│ └─ stats-service.ts # Statistics API
├─ utils/ # Utility functions
│ └─ index.ts # Utility re-exports
├─ types.ts # Common TypeScript types
├─ utils.ts # General utilities
├─ auth-utils.ts # Authentication utilities
└─ styles.ts # Styling utilities
```
## API Structure
- `api.ts` - The main API entry point that re-exports all API functionality
- `api-client.ts` - Client-side API functions and types
- `server-api.ts` - Server-side API functions (for server components in the app/ directory)
## How to Use
### In Client Components
```typescript
// Import what you need from the main API module
import { clientFetch, getCustomers, getProducts } from '@/lib/api';
// Example usage
const customers = await getCustomers(1, 25);
const products = await getProducts(1, 10);
```
### In Server Components (app/ directory only)
```typescript
// Server functions only work in Server Components in the app/ directory
import { getCustomersServer } from '@/lib/api';
// In a Server Component
export default async function Page() {
const customers = await getCustomersServer(1, 25);
// ...
}
```
## Server Components Compatibility
The server-side API functions (`fetchServer`, `getCustomersServer`, etc.) can **only** be used in Server Components within the app/ directory. They will throw an error if used in:
- Client Components
- The pages/ directory
- Any code that runs in the browser
This is because they rely on Next.js server-only features like the `cookies()` function from `next/headers`.
## Utilities
For utilities, you can either import specific functions or use the utils index:
```typescript
// Import specific utilities
import { cn } from '@/lib/utils';
import { getAuthToken } from '@/lib/auth-utils';
// Or import from the utils index
import { cn, getAuthToken } from '@/lib/utils';
```
## Backward Compatibility
For backward compatibility, many functions are also re-exported from their original locations.

264
lib/api-client.ts Normal file
View File

@@ -0,0 +1,264 @@
'use client';
// Import toast conditionally to prevent build errors
let toast: any;
try {
// Try to import from the UI components
toast = require("@/components/ui/use-toast").toast;
} catch (error) {
// Fallback toast function if not available
toast = {
error: (message: string) => console.error(message)
};
}
// Types
type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface FetchOptions {
method?: FetchMethod;
body?: any;
cache?: RequestCache;
headers?: HeadersInit;
}
// Customer types
export interface CustomerStats {
userId: string;
telegramUserId: number;
telegramUsername: string;
totalOrders: number;
totalSpent: number;
ordersByStatus: {
paid: number;
completed: number;
acknowledged: number;
shipped: number;
};
lastOrderDate: string | null;
firstOrderDate: string;
chatId: number;
hasOrders?: boolean;
}
export interface CustomerResponse {
customers: CustomerStats[];
total: number;
success?: boolean;
}
/**
* Normalizes a URL to ensure it has the correct /api prefix
* This prevents double prefixing which causes API errors
*/
function normalizeApiUrl(url: string): string {
// Remove any existing /api or api prefix
const cleanPath = url.replace(/^\/?(api\/)+/, '');
// Add a single /api prefix
return `/api/${cleanPath.replace(/^\//, '')}`;
}
/**
* Get the authentication token from cookies or localStorage
*/
export function getAuthToken(): string | null {
if (typeof document === 'undefined') return null; // Guard for SSR
return document.cookie
.split('; ')
.find(row => row.startsWith('Authorization='))
?.split('=')[1] || localStorage.getItem('Authorization');
}
/**
* Get a cookie value by name
*/
export function getCookie(name: string): string | undefined {
if (typeof document === 'undefined') return undefined; // Guard for SSR
return document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))
?.split('=')[1];
}
/**
* Creates standard API request headers with authentication
*
* @param token Optional auth token (fetched automatically if not provided)
* @param customHeaders Additional headers to include
* @returns Headers object ready for fetch requests
*/
function createApiHeaders(token?: string | null, customHeaders: Record<string, string> = {}): Headers {
const headers = new Headers({
'Content-Type': 'application/json',
'accept': '*/*',
...customHeaders
});
const authToken = token || getAuthToken();
if (authToken) {
headers.set('authorization', `Bearer ${authToken}`);
}
return headers;
}
/**
* Simple client-side fetch function for making API calls with Authorization header.
* Uses the Next.js API proxy to make requests through the same domain.
*/
export async function clientFetch<T = any>(url: string, options: RequestInit = {}): Promise<T> {
try {
// Create headers with authentication
const headers = createApiHeaders(null, options.headers as Record<string, string>);
// Normalize URL to ensure it uses the Next.js API proxy
const fullUrl = normalizeApiUrl(url);
const res = await fetch(fullUrl, {
...options,
headers,
credentials: 'include',
mode: 'cors',
referrerPolicy: 'strict-origin-when-cross-origin'
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`;
throw new Error(errorMessage);
}
// Handle 204 No Content responses
if (res.status === 204) {
return {} as T;
}
return await res.json();
} catch (error) {
console.error(`Client fetch error at ${url}:`, error);
throw error;
}
}
/**
* Enhanced client-side fetch function with error toast notifications.
* Use this when you want automatic error handling with toast notifications.
*/
export async function fetchClient<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { method = 'GET', body, headers = {}, ...rest } = options;
// Get the base API URL from environment or fallback
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
// Ensure the endpoint starts with a slash
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
// For the specific case of internal-api.inboxi.ng - remove duplicate /api
let url;
if (apiUrl.includes('internal-api.inboxi.ng')) {
// Special case for internal-api.inboxi.ng
if (normalizedEndpoint.startsWith('/api/')) {
url = `${apiUrl}${normalizedEndpoint.substring(4)}`; // Remove the /api part
} else {
url = `${apiUrl}${normalizedEndpoint}`;
}
} else {
// Normal case for other environments
url = `${apiUrl}${normalizedEndpoint}`;
}
// Get auth token from cookies
const authToken = getAuthToken();
// Prepare headers with authentication if token exists
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...(headers as Record<string, string>),
};
if (authToken) {
// Backend expects "Bearer TOKEN" format
requestHeaders['Authorization'] = `Bearer ${authToken}`;
}
const fetchOptions: RequestInit = {
method,
credentials: 'include',
headers: requestHeaders,
...rest,
};
if (body && method !== 'GET') {
fetchOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.message || errorData.error || 'An error occurred';
throw new Error(errorMessage);
}
if (response.status === 204) {
return {} as T;
}
const data = await response.json();
return data;
} catch (error) {
console.error('API request failed:', error);
// Only show toast if this is a client-side error (not during SSR)
if (typeof window !== 'undefined') {
const message = error instanceof Error ? error.message : 'Failed to connect to server';
// Handle different toast implementations
if (toast?.title && toast?.description) {
// shadcn/ui toast format
toast({
title: 'Error',
description: message,
variant: 'destructive',
});
} else if (typeof toast?.error === 'function') {
// sonner or other simple toast
toast.error(message);
} else {
// Fallback to console
console.error('API error:', message);
}
}
throw error;
}
}
// =========== API SERVICES ===========
/**
* Get a paginated list of customers
* @param page Page number (starting from 1)
* @param limit Number of items per page
* @returns Promise with customers data and total count
*/
export const getCustomers = async (page: number = 1, limit: number = 25): Promise<CustomerResponse> => {
return clientFetch(`/customers?page=${page}&limit=${limit}`);
};
/**
* Get detailed stats for a specific customer
* @param userId The customer's user ID
* @returns Promise with detailed customer stats
*/
export const getCustomerDetails = async (userId: string): Promise<CustomerStats> => {
return clientFetch(`/customers/${userId}`);
};

View File

@@ -1,71 +0,0 @@
/**
* API utilities for client and server-side requests
*/
/**
* Normalizes the API URL to ensure it uses the proper prefix
* For client-side, ensures all requests go through the Next.js API proxy
*/
export function normalizeApiUrl(url: string): string {
// If URL already starts with http or https, return as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// If URL already starts with /api, use as is
if (url.startsWith('/api/')) {
return url;
}
// Otherwise, ensure it has the /api prefix
return `/api${url.startsWith('/') ? '' : '/'}${url}`;
}
/**
* Get the server API URL for server-side requests
*/
export function getServerApiUrl(endpoint: string): string {
// Get the base API URL from environment
const baseUrl = process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || 'https://internal-api.inboxi.ng/api';
// Ensure it doesn't have trailing slash
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
// Ensure endpoint has leading slash
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${normalizedBaseUrl}${normalizedEndpoint}`;
}
/**
* Get the authentication token from cookies or localStorage
* Only available in client-side code
*/
export function getAuthToken(): string | null {
if (typeof document === 'undefined') return null;
return document.cookie
.split('; ')
.find(row => row.startsWith('Authorization='))
?.split('=')[1] ||
(typeof localStorage !== 'undefined' ? localStorage.getItem('Authorization') : null);
}
/**
* Create headers with authentication for API requests
*/
export function createApiHeaders(token: string | null = null, additionalHeaders: Record<string, string> = {}): Headers {
const headers = new Headers({
'Content-Type': 'application/json',
...additionalHeaders
});
// Use provided token or try to get it from storage
const authToken = token || getAuthToken();
if (authToken) {
headers.append('Authorization', `Bearer ${authToken}`);
}
return headers;
}

136
lib/api.ts Normal file
View File

@@ -0,0 +1,136 @@
// Re-export client API functions
export {
// Core client API functions
clientFetch,
fetchClient,
getAuthToken,
getCookie,
// Customer API
getCustomers,
getCustomerDetails,
// Types
type CustomerStats,
type CustomerResponse,
} from './api-client';
// Re-export product services
export {
getProducts,
getProductDetails,
createProduct,
updateProduct,
deleteProduct,
uploadProductImage,
getProductStock,
updateProductStock,
// Types
type Product,
type ProductsResponse,
type StockData,
} from './services/product-service';
// Re-export shipping services
export {
getShippingOptions,
getShippingOption,
createShippingOption,
updateShippingOption,
deleteShippingOption,
// Types
type ShippingOption,
type ShippingOptionsResponse,
} from './services/shipping-service';
// Re-export stats services
export {
getPlatformStats,
getVendorStats,
// Types
type PlatformStats,
} from './services/stats-service';
// Get clientFetch first so we can use it in the compatibility functions
import { clientFetch } from './api-client';
import { getProductDetails, updateProduct } from './services/product-service';
import { getPlatformStats } from './services/stats-service';
// Add missing functions for backward compatibility
// These are functions from the old style that we need to maintain compatibility
export const fetchData = async (endpoint: string, options: any = {}) => {
console.warn('fetchData is deprecated, use clientFetch instead');
return clientFetch(endpoint, options);
};
export const apiRequest = async (endpoint: string, method = 'GET', data: any = null, token: string | null = null) => {
console.warn('apiRequest is deprecated, use clientFetch instead');
const options: RequestInit & { headers: Record<string, string> } = {
method,
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined,
};
if (token) {
options.headers.Authorization = `Bearer ${token}`;
}
return clientFetch(endpoint, options);
};
// Product-specific compatibility functions
export const fetchProductData = async (productId: string, token?: string) => {
console.warn('fetchProductData is deprecated, use getProductDetails instead');
return getProductDetails(productId);
};
export const saveProductData = async (productData: any, productId: string, token?: string) => {
console.warn('saveProductData is deprecated, use updateProduct instead');
return updateProduct(productId, productData);
};
// Stats compatibility function
export const fetchPlatformStats = async () => {
console.warn('fetchPlatformStats is deprecated, use getPlatformStats instead');
return getPlatformStats();
};
// Server API functions are conditionally exported
// They are only usable in Server Components in the app/ directory
// Dynamically check if we can use server components
let canUseServerComponents = false;
try {
// Check if next/headers is available
require('next/headers');
canUseServerComponents = true;
} catch (e) {
// We're not in a Server Component context
// This is normal in Client Components and pages/ directory
}
// Handle server API functions
// Define function types first for TypeScript
type ServerFetchFn = <T>(url: string, options?: RequestInit) => Promise<T>;
type CustomerServerFn = (options?: any) => Promise<any>;
type CustomerDetailServerFn = (id: string, options?: any) => Promise<any>;
type PlatformStatsServerFn = () => Promise<any>;
// Export the functions for use in server components
export const fetchServer: ServerFetchFn = canUseServerComponents
? require('./server-api').fetchServer
: (() => { throw new Error('fetchServer can only be used in Server Components'); }) as any;
export const getCustomersServer: CustomerServerFn = canUseServerComponents
? require('./server-api').getCustomersServer
: (() => { throw new Error('getCustomersServer can only be used in Server Components'); }) as any;
export const getCustomerDetailsServer: CustomerDetailServerFn = canUseServerComponents
? require('./server-api').getCustomerDetailsServer
: (() => { throw new Error('getCustomerDetailsServer can only be used in Server Components'); }) as any;
export const getPlatformStatsServer: PlatformStatsServerFn = canUseServerComponents
? require('./server-api').getPlatformStatsServer
: (() => { throw new Error('getPlatformStatsServer can only be used in Server Components'); }) as any;

View File

@@ -1,114 +0,0 @@
'use client';
import { toast } from "@/components/ui/use-toast";
type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface FetchOptions {
method?: FetchMethod;
body?: any;
cache?: RequestCache;
headers?: HeadersInit;
}
// Helper function to get auth token from cookies
function getAuthToken(): string | null {
if (typeof document === 'undefined') return null; // Guard for SSR
return document.cookie
.split('; ')
.find(row => row.startsWith('Authorization='))
?.split('=')[1] || null;
}
export async function fetchClient<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { method = 'GET', body, headers = {}, ...rest } = options;
// Get the base API URL from environment or fallback
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
// Ensure the endpoint starts with a slash
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
// For the specific case of internal-api.inboxi.ng - remove duplicate /api
let url;
if (apiUrl.includes('internal-api.inboxi.ng')) {
// Special case for internal-api.inboxi.ng
if (normalizedEndpoint.startsWith('/api/')) {
url = `${apiUrl}${normalizedEndpoint.substring(4)}`; // Remove the /api part
} else {
url = `${apiUrl}${normalizedEndpoint}`;
}
} else {
// Normal case for other environments
url = `${apiUrl}${normalizedEndpoint}`;
}
// Get auth token from cookies
const authToken = getAuthToken();
// Prepare headers with authentication if token exists
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...(headers as Record<string, string>),
};
if (authToken) {
// Backend expects "Bearer TOKEN" format
requestHeaders['Authorization'] = `Bearer ${authToken}`;
console.log('Authorization header set to:', `Bearer ${authToken.substring(0, 10)}...`);
}
console.log('API Request:', {
url,
method,
hasAuthToken: !!authToken
});
const fetchOptions: RequestInit = {
method,
credentials: 'include',
headers: requestHeaders,
...rest,
};
if (body && method !== 'GET') {
fetchOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.message || errorData.error || 'An error occurred';
throw new Error(errorMessage);
}
if (response.status === 204) {
return {} as T;
}
const data = await response.json();
return data;
} catch (error) {
console.error('API request failed:', error);
// Only show toast if this is a client-side error (not during SSR)
if (typeof window !== 'undefined') {
const message = error instanceof Error ? error.message : 'Failed to connect to server';
toast({
title: 'Error',
description: message,
variant: 'destructive',
});
}
throw error;
}
}

View File

@@ -1,93 +0,0 @@
/**
* Normalizes a URL to ensure it has the correct /api prefix
* This prevents double prefixing which causes API errors
*/
function normalizeApiUrl(url: string): string {
// Remove any existing /api or api prefix
const cleanPath = url.replace(/^\/?(api\/)+/, '');
// Add a single /api prefix
return `/api/${cleanPath.replace(/^\//, '')}`;
}
/**
* Get the authentication token from cookies or localStorage
*/
function getAuthToken(): string | null {
if (typeof document === 'undefined') return null; // Guard for SSR
return document.cookie
.split('; ')
.find(row => row.startsWith('Authorization='))
?.split('=')[1] || localStorage.getItem('Authorization');
}
/**
* Creates standard API request headers with authentication
*
* @param token Optional auth token (fetched automatically if not provided)
* @param customHeaders Additional headers to include
* @returns Headers object ready for fetch requests
*/
function createApiHeaders(token?: string | null, customHeaders: Record<string, string> = {}): Headers {
const headers = new Headers({
'Content-Type': 'application/json',
'accept': '*/*',
...customHeaders
});
const authToken = token || getAuthToken();
if (authToken) {
headers.set('authorization', `Bearer ${authToken}`);
}
return headers;
}
/**
* Simple client-side fetch function for making API calls with Authorization header.
* Ensures all requests go through the Next.js API proxy.
*/
export async function clientFetch<T = any>(url: string, options: RequestInit = {}): Promise<T> {
try {
// Create headers with authentication
const headers = createApiHeaders(null, options.headers as Record<string, string>);
// Normalize URL to ensure it uses the Next.js API proxy
const fullUrl = normalizeApiUrl(url);
const res = await fetch(fullUrl, {
...options,
headers,
credentials: 'include',
mode: 'cors',
referrerPolicy: 'strict-origin-when-cross-origin'
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`;
throw new Error(errorMessage);
}
// Handle 204 No Content responses
if (res.status === 204) {
return {} as T;
}
return await res.json();
} catch (error) {
console.error(`Client fetch error at ${url}:`, error);
throw error;
}
}
/**
* Get a cookie value by name
*/
export function getCookie(name: string): string | undefined {
return document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))
?.split('=')[1];
}

View File

@@ -1,13 +0,0 @@
/**
* Client-side fetch function for API requests.
*/
export async function fetchData(url: string, options: RequestInit = {}): Promise<any> {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`Request failed: ${res.statusText}`);
return res.json();
} catch (error) {
console.error(`Fetch error at ${url}:`, error);
throw error;
}
}

View File

@@ -1,16 +0,0 @@
import gitInfo from "../public/git-info.json";
/**
* Git utility to load commit hash in both development and production environments
*/
interface GitInfo {
commitHash: string;
buildTime: string;
}
let cachedGitInfo: GitInfo | null = null;
export async function getGitCommitInfo(): Promise<GitInfo> {
return gitInfo;
}

View File

@@ -1,108 +0,0 @@
import { fetchData } from '@/lib/data-service';
export const fetchProductData = async (url: string, authToken: string) => {
try {
return await fetchData(url, {
headers: { Authorization: `Bearer ${authToken}` },
credentials: "include",
});
} catch (error) {
console.error("Error fetching product data:", error);
throw error;
}
};
export const saveProductData = async (
url: string,
data: any,
authToken: string,
method: "POST" | "PUT" = "POST"
) => {
try {
return await fetchData(url, {
method,
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(data),
});
} catch (error) {
console.error("Error saving product data:", error);
throw error;
}
};
export const saveProductImage = async(url: string, file:File, authToken: string) => {
try{
const formData = new FormData();
formData.append("file", file);
return await fetchData(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${authToken}`,
},
body: formData,
});
} catch (error) {
console.error("Error uploading image:", error);
throw error;
}
}
export const deleteProductData = async (url: string, authToken: string) => {
try {
return await fetchData(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
});
} catch (error) {
console.error("Error deleting product data:", error);
throw error;
}
};
// Stock management functions
export const fetchStockData = async (url: string, authToken: string) => {
try {
return await fetchData(url, {
headers: { Authorization: `Bearer ${authToken}` },
credentials: "include",
});
} catch (error) {
console.error("Error fetching stock data:", error);
throw error;
}
};
export const updateProductStock = async (
productId: string,
stockData: {
currentStock: number;
stockTracking?: boolean;
lowStockThreshold?: number;
},
authToken: string
) => {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/stock/${productId}`;
return await fetchData(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(stockData),
});
} catch (error) {
console.error("Error updating product stock:", error);
throw error;
}
};

View File

@@ -1,5 +1,17 @@
import { cookies } from 'next/headers';
// This module is only meant to be used in Server Components in the app/ directory
// It cannot be imported in Client Components or pages/ directory
import { redirect } from 'next/navigation';
import { CustomerResponse, CustomerStats } from './api-client';
// Dynamically import cookies to prevent build errors
let cookiesModule: any;
try {
// This will only work in server components
cookiesModule = require('next/headers');
} catch (error) {
console.warn('Warning: next/headers only works in Server Components in the app/ directory');
}
/**
* Constructs a server-side API URL for backend requests
@@ -21,13 +33,23 @@ function getServerApiUrl(endpoint: string): string {
* Server-side fetch wrapper with authentication.
* Used in Server Components to make authenticated API requests to the backend.
* This uses the SERVER_API_URL environment variable and is different from client-side fetching.
*
* @throws Error if used outside of a Server Component in the app/ directory
*/
export async function fetchServer<T = unknown>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
// Check if we're in a server component context
if (!cookiesModule?.cookies) {
throw new Error(
"fetchServer can only be used in Server Components in the app/ directory. " +
"For client components, use clientFetch or fetchClient instead."
);
}
// Get auth token from cookies
const cookieStore = await cookies();
const cookieStore = cookiesModule.cookies();
const authToken = cookieStore.get('Authorization')?.value;
// Redirect to login if not authenticated
@@ -68,4 +90,42 @@ export async function fetchServer<T = unknown>(
console.error(`Server request to ${endpoint} failed:`, error);
throw error;
}
}
}
// =========== SERVER API SERVICES ===========
/**
* Get a paginated list of customers (server-side)
* @param page Page number (starting from 1)
* @param limit Number of items per page
* @returns Promise with customers data and total count
*/
export const getCustomersServer = async (page: number = 1, limit: number = 25): Promise<CustomerResponse> => {
return fetchServer(`/customers?page=${page}&limit=${limit}`);
};
/**
* Get detailed stats for a specific customer (server-side)
* @param userId The customer's user ID
* @returns Promise with detailed customer stats
*/
export const getCustomerDetailsServer = async (userId: string): Promise<CustomerStats> => {
return fetchServer(`/customers/${userId}`);
};
// Server-side platform stats function
export async function getPlatformStatsServer(): Promise<any> {
try {
return fetchServer('/stats/platform');
} catch (error) {
console.error('Error fetching platform stats (server):', error);
// Return default stats to prevent UI breakage
return {
totalProducts: 0,
totalVendors: 0,
totalOrders: 0,
totalCustomers: 0,
gmv: 0
};
}
}

5
lib/services/index.ts Normal file
View File

@@ -0,0 +1,5 @@
// Re-export all service functionality
export * from './product-service';
export * from './shipping-service';
export * from './stats-service';
export * from '../api-client';

View File

@@ -0,0 +1,101 @@
import { clientFetch } from '../api-client';
// Product data types
export interface Product {
_id: string;
name: string;
description: string;
price: number;
imageUrl?: string;
category?: string;
stock?: number;
status: 'active' | 'inactive';
}
export interface ProductsResponse {
products: Product[];
total: number;
success?: boolean;
}
export interface StockData {
currentStock: number;
stockTracking?: boolean;
lowStockThreshold?: number;
}
/**
* Get all products with pagination
*/
export const getProducts = async (page: number = 1, limit: number = 25): Promise<ProductsResponse> => {
return clientFetch(`/products?page=${page}&limit=${limit}`);
};
/**
* Get a specific product by ID
*/
export const getProductDetails = async (productId: string): Promise<Product> => {
return clientFetch(`/products/${productId}`);
};
/**
* Create a new product
*/
export const createProduct = async (productData: Omit<Product, '_id'>): Promise<Product> => {
return clientFetch('/products', {
method: 'POST',
body: JSON.stringify(productData),
});
};
/**
* Update a product
*/
export const updateProduct = async (productId: string, productData: Partial<Product>): Promise<Product> => {
return clientFetch(`/products/${productId}`, {
method: 'PUT',
body: JSON.stringify(productData),
});
};
/**
* Delete a product
*/
export const deleteProduct = async (productId: string): Promise<{ success: boolean }> => {
return clientFetch(`/products/${productId}`, {
method: 'DELETE',
});
};
/**
* Upload a product image
*/
export const uploadProductImage = async (productId: string, file: File): Promise<{ imageUrl: string }> => {
const formData = new FormData();
formData.append('file', file);
return clientFetch(`/products/${productId}/image`, {
method: 'PUT',
body: formData,
headers: {
// Don't set Content-Type when sending FormData, the browser will set it with the boundary
}
});
};
/**
* Get product stock information
*/
export const getProductStock = async (productId: string): Promise<StockData> => {
return clientFetch(`/stock/${productId}`);
};
/**
* Update product stock
*/
export const updateProductStock = async (productId: string, stockData: StockData): Promise<StockData> => {
return clientFetch(`/stock/${productId}`, {
method: 'PUT',
body: JSON.stringify(stockData),
});
};

View File

@@ -0,0 +1,130 @@
import { clientFetch } from '../api-client';
/**
* Shipping service - Handles shipping options
* Replaces the old shippingHelper.ts
*/
export interface ShippingOption {
_id?: string;
name: string;
price: number;
createdAt?: string;
updatedAt?: string;
}
export interface ShippingOptionsResponse {
success: boolean;
data: ShippingOption[];
}
// Get all shipping options
export const getShippingOptions = async (authToken?: string): Promise<ShippingOption[]> => {
console.log('Fetching shipping options');
const options: RequestInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
if (authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${authToken}`,
};
}
const response = await clientFetch<ShippingOptionsResponse>('/api/shipping', options);
return response.data || [];
};
// Get a single shipping option
export const getShippingOption = async (id: string, authToken?: string): Promise<ShippingOption> => {
const options: RequestInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
if (authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${authToken}`,
};
}
const response = await clientFetch<{success: boolean, data: ShippingOption}>(`/api/shipping/${id}`, options);
return response.data;
};
// Create a new shipping option
export const createShippingOption = async (data: ShippingOption, authToken?: string) => {
const options: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
if (authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${authToken}`,
};
}
return clientFetch<{success: boolean, data: ShippingOption}>('/api/shipping', options);
};
// Update a shipping option
export const updateShippingOption = async (id: string, data: ShippingOption, authToken?: string) => {
const options: RequestInit = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
if (authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${authToken}`,
};
}
return clientFetch<{success: boolean, data: ShippingOption}>(`/api/shipping/${id}`, options);
};
// Delete a shipping option
export const deleteShippingOption = async (id: string, authToken?: string) => {
const options: RequestInit = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
};
if (authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${authToken}`,
};
}
return clientFetch<{success: boolean}>(`/api/shipping/${id}`, options);
};
// Compatibility with old shippingHelper functions
export const fetchShippingMethods = getShippingOptions;
export const addShippingMethod = createShippingOption;
export const updateShippingMethod = updateShippingOption;
export const deleteShippingMethod = deleteShippingOption;
// Types for backward compatibility
export type ShippingMethod = ShippingOption;
export type ShippingData = ShippingOption;

View File

@@ -0,0 +1,47 @@
import { clientFetch } from '../api-client';
// Stats data types
export interface PlatformStats {
orders: {
completed: number;
};
vendors: {
total: number;
};
transactions: {
volume: number;
averageOrderValue: number;
};
}
/**
* Get platform statistics
*/
export const getPlatformStats = async (): Promise<PlatformStats> => {
try {
return await clientFetch('/stats/platform');
} catch (error) {
console.error('Error fetching platform stats:', error);
// Return fallback data if API fails
return {
orders: {
completed: 15800
},
vendors: {
total: 2400
},
transactions: {
volume: 3200000,
averageOrderValue: 220
}
};
}
};
/**
* Get vendor-specific statistics
*/
export const getVendorStats = async (): Promise<any> => {
return clientFetch('/stats/vendor');
};

View File

@@ -1,116 +0,0 @@
import { fetchData } from '@/lib/data-service';
export const fetchShippingMethods = async (authToken: string) => {
try {
const res = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/shipping-options`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
}
);
console.log("Shipping Methods Response:", res);
if (!res) throw new Error("Failed to fetch shipping options");
return res
} catch (error) {
console.error("Error loading shipping options:", error);
throw error;
}
};
interface ShippingMethod {
name: string;
price: number;
_id?: string;
}
export const addShippingMethod = async (
authToken: string,
newShipping: Omit<ShippingMethod, "_id">
): Promise<ShippingMethod[]> => {
try {
const res = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/shipping-options`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(newShipping),
}
);
// If fetchData returns directly (not a Response object), just return it
if (!res.ok && !res.status) {
return res;
}
// Handle if it's a Response object
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.message || "Failed to add shipping method");
}
return await res.json();
} catch (error) {
console.error("Error adding shipping method:", error);
throw new Error(
error instanceof Error
? error.message
: "An unexpected error occurred while adding shipping method"
);
}
};
export const deleteShippingMethod = async (authToken: string, id: string) => {
try {
const res = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/shipping-options/${id}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${authToken}` },
credentials: "include",
}
);
if (!res.ok) throw new Error("Failed to delete shipping method");
return { success: res.status === 204 };
} catch (error) {
console.error("Error deleting shipping method:", error);
throw error;
}
};
export const updateShippingMethod = async (
authToken: string,
id: string,
updatedShipping: any
) => {
try {
const res = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/shipping-options/${id}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(updatedShipping),
}
);
if (!res) throw new Error("Failed to update shipping method");
return res;
} catch (error) {
console.error("Error updating shipping method:", error);
throw error;
}
};

View File

@@ -1,51 +0,0 @@
export interface PlatformStats {
orders: {
completed: number;
};
vendors: {
total: number;
};
transactions: {
volume: number;
averageOrderValue: number;
};
}
export async function fetchPlatformStats(): Promise<PlatformStats> {
const BASE_API_URL = process.env.SERVER_API_URL || 'http://localhost:3001/api';
console.log('Fetching platform stats from:', BASE_API_URL);
try {
const response = await fetch(`${BASE_API_URL}/stats/platform`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store'
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
console.log('Fetched stats:', data);
return data;
} catch (error) {
console.error('Error fetching platform stats:', error);
// Return fallback data if API fails
return {
orders: {
completed: 15800
},
vendors: {
total: 2400
},
transactions: {
volume: 3200000,
averageOrderValue: 220
}
};
}
}

View File

@@ -1,53 +0,0 @@
import { fetchData } from '@/lib/data-service';
export const apiRequest = async <T = any>(endpoint: string, method: string = "GET", body?: T | null) => {
try {
if (typeof document === "undefined") {
throw new Error("API requests must be made from the client side.");
}
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken){
document.location.href = "/login";
throw new Error("No authentication token found");
}
const options: RequestInit = {
method,
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
};
if (body) {
options.body = JSON.stringify(body);
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
if (!API_URL) throw new Error("NEXT_PUBLIC_API_URL is not set in environment variables");
const res = await fetchData(`${API_URL}${endpoint}`, options);
if (!res) {
const errorResponse = await res.json().catch(() => null);
const errorMessage = errorResponse?.error || res.statusText || "Unknown error";
throw new Error(`Failed to ${method} ${endpoint}: ${errorMessage}`);
}
return res;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`🚨 API Request Error: ${error.message}`);
throw new Error(error.message);
}
console.error("❌ An unknown error occurred", error);
throw new Error("An unknown error occurred");
}
};

View File

@@ -59,6 +59,7 @@ export interface PricingTier {
export interface Category {
_id: string
name: string
parentId?: string
}
export interface OrderStatsData {

50
lib/utils/git.ts Normal file
View File

@@ -0,0 +1,50 @@
// Git utility functions
interface GitInfo {
hash: string;
date: string;
message: string;
}
// Default git info if file is not found
const defaultGitInfo: GitInfo = {
hash: 'local-dev',
date: new Date().toISOString(),
message: 'Local development build',
};
/**
* Get git info - safely handles cases where the JSON file isn't available
*/
export function getGitInfo(): GitInfo {
try {
// In production builds, the git info should be available
// In development, we'll use default values
const gitInfo = {
hash: process.env.NEXT_PUBLIC_GIT_HASH || 'dev',
date: process.env.NEXT_PUBLIC_GIT_DATE || new Date().toISOString(),
message: process.env.NEXT_PUBLIC_GIT_MESSAGE || 'Development build',
};
return gitInfo;
} catch (error) {
console.warn('Could not load git info, using defaults', error);
return defaultGitInfo;
}
}
/**
* Get a shorter git hash for display
*/
export function getShortGitHash(): string {
const { hash } = getGitInfo();
return hash.substring(0, 7);
}
/**
* Format git commit date for display
*/
export function getFormattedGitDate(): string {
const { date } = getGitInfo();
return new Date(date).toLocaleDateString();
}

16
lib/utils/index.ts Normal file
View File

@@ -0,0 +1,16 @@
/**
* Main utilities index file
* Re-exports all utility functions from their respective modules
*/
// Re-export general utils
export * from './general';
// Re-export auth utils
export * from './auth';
// Re-export git utils
export * from './git';
// Re-export style utils
export * from './styles';