weewoo
This commit is contained in:
82
lib/README.md
Normal file
82
lib/README.md
Normal 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
264
lib/api-client.ts
Normal 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}`);
|
||||
};
|
||||
@@ -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
136
lib/api.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
5
lib/services/index.ts
Normal 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';
|
||||
101
lib/services/product-service.ts
Normal file
101
lib/services/product-service.ts
Normal 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),
|
||||
});
|
||||
};
|
||||
130
lib/services/shipping-service.ts
Normal file
130
lib/services/shipping-service.ts
Normal 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;
|
||||
47
lib/services/stats-service.ts
Normal file
47
lib/services/stats-service.ts
Normal 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');
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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
50
lib/utils/git.ts
Normal 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
16
lib/utils/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user