diff --git a/.env.production b/.env.production index 7d66291..c41ca8d 100644 --- a/.env.production +++ b/.env.production @@ -11,5 +11,4 @@ NEXT_LOG_LEVEL=error GENERATE_SOURCEMAP=false # API configuration -NEXT_PUBLIC_API_URL=/api SERVER_API_URL=https://internal-api.inboxi.ng/api diff --git a/Dockerfile b/Dockerfile index d455564..acb41cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,13 @@ # Use official Node.js image as base FROM node:18-alpine AS builder WORKDIR /app - -# Install dependencies COPY package.json package-lock.json ./ + RUN npm install --force -# Copy source code COPY . . -# Set environment variables for build -ENV NODE_ENV=production ENV NEXT_PUBLIC_API_URL=/api -ENV SERVER_API_URL=https://internal-api.inboxi.ng/api # Build the Next.js application RUN npm run build @@ -23,22 +18,19 @@ FROM node:18-alpine AS runner # Set working directory inside the container WORKDIR /app -# Create necessary directories RUN mkdir -p /app/public # Copy only necessary files from builder COPY --from=builder /app/package.json /app/package-lock.json ./ COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/public ./public -# Expose the app port EXPOSE 3000 -# Set runtime environment variables ENV NODE_ENV=production ENV NEXT_PUBLIC_API_URL=/api ENV SERVER_API_URL=https://internal-api.inboxi.ng/api + # Start Next.js server CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 0000000..bfb3cb7 --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,99 @@ +"use server"; + +/** + * NOTE: This file contains workarounds for missing backend endpoints. + * In the future, consider implementing proper API endpoints for these functions: + * - GET /chats/:chatId/members - To get chat members + * - GET /users/:userId - To get user information + */ + +/** + * Helper function to make a server-side fetch with proper error handling + */ +async function safeFetch(path: string) { + try { + // Use absolute URL with the API_URL from environment variable or default to local + const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; + + // Remove any leading /api/ or api/ to prevent double prefixing + let cleanPath = path.replace(/^\/?(api\/)+/, ''); + + // Ensure path starts with a / for consistent joining + if (!cleanPath.startsWith('/')) { + cleanPath = '/' + cleanPath; + } + + // Construct the URL with a single /api prefix + const url = `${baseUrl}/api${cleanPath}`; + + console.log(`Server action fetching URL: ${url}`); + + // For server components, use the fetch API directly + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Error fetching ${path}:`, error); + return null; + } +} + +/** + * Get chat details and extract members information + */ +export async function getChatMembers(chatId: string) { + try { + // Use the safeFetch helper with the correct path - avoid any api prefix + const response = await safeFetch(`chats/${chatId}`); + + if (response) { + // Create member objects for buyer and vendor + const members = [ + { + _id: response.buyerId, + isVendor: false, + telegramUsername: response.telegramUsername || null + }, + { + _id: response.vendorId, + isVendor: true + } + ]; + + return { members }; + } + + return { members: [] }; + } catch (error) { + console.error("Error in getChatMembers:", error); + return { members: [] }; + } +} + +/** + * Get user information by ID + * This function is imported but doesn't appear to be used in the component. + * Keeping a placeholder implementation for future use. + */ +export async function getUserInfoById(userId: string) { + try { + // This function isn't actually used currently, but we'll keep a + // placeholder implementation in case it's needed in the future + return { + user: { + _id: userId, + name: userId && userId.length > 8 + ? `Customer ${userId.slice(-4)}` // For buyers + : "Vendor", // For vendors + isVendor: userId && userId.length <= 8 + } + }; + } catch (error) { + console.error("Error in getUserInfoById:", error); + return { user: null }; + } +} \ No newline at end of file diff --git a/app/dashboard/chats/[id]/page.tsx b/app/dashboard/chats/[id]/page.tsx index 405e362..a4f82af 100644 --- a/app/dashboard/chats/[id]/page.tsx +++ b/app/dashboard/chats/[id]/page.tsx @@ -1,17 +1,17 @@ -"use client"; - import React from "react"; -import { useParams } from "next/navigation"; +import { Metadata } from "next"; import ChatDetail from "@/components/dashboard/ChatDetail"; import Dashboard from "@/components/dashboard/dashboard"; -export default function ChatDetailPage() { - const params = useParams(); - const chatId = params.id as string; - +export const metadata: Metadata = { + title: "Chat Conversation", + description: "View and respond to customer messages", +}; + +export default function ChatDetailPage({ params }: { params: { id: string } }) { return ( - + ); } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 4176beb..195c8ed 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,7 +9,7 @@ const inter = Inter({ subsets: ["latin"] }) export const metadata = { title: "Ember", - description: "E-commerce management dashboard", + description: "E-Commerce with a twist, Buy, Sell, and Chat with ease", } export default function RootLayout({ diff --git a/components/dashboard/content.tsx b/components/dashboard/content.tsx index 448a99e..4c2bac8 100644 --- a/components/dashboard/content.tsx +++ b/components/dashboard/content.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react" import OrderStats from "./order-stats" import { getGreeting } from "@/lib/utils" import { statsConfig } from "@/config/dashboard" +import { getRandomQuote } from "@/config/quotes" import type { OrderStatsData } from "@/lib/types" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ShoppingCart, RefreshCcw } from "lucide-react" @@ -26,96 +27,46 @@ interface TopProduct { revenue: number; } -// Business quotes array -const businessQuotes = [ - { text: "Your most unhappy customers are your greatest source of learning.", author: "Bill Gates" }, - { text: "The secret of getting ahead is getting started.", author: "Mark Twain" }, - { text: "Success is not final; failure is not fatal: It is the courage to continue that counts.", author: "Winston Churchill" }, - { text: "The way to get started is to quit talking and begin doing.", author: "Walt Disney" }, - { text: "Opportunities don't happen. You create them.", author: "Chris Grosser" }, - { text: "The best way to predict the future is to create it.", author: "Peter Drucker" }, - { text: "Don't watch the clock; do what it does. Keep going.", author: "Sam Levenson" }, - { text: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" }, - { text: "Entrepreneurship is living a few years of your life like most people won't so you can spend the rest of your life like most people can't.", author: "Anonymous" }, - { text: "Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work.", author: "Steve Jobs" }, - - // Additional quotes - { text: "If you are not willing to risk the usual, you will have to settle for the ordinary.", author: "Jim Rohn" }, - { text: "The only limit to our realization of tomorrow will be our doubts of today.", author: "Franklin D. Roosevelt" }, - { text: "What would you do if you weren't afraid?", author: "Sheryl Sandberg" }, - { text: "The most valuable businesses of coming decades will be built by entrepreneurs who seek to empower people rather than try to make them obsolete.", author: "Peter Thiel" }, - { text: "If you really look closely, most overnight successes took a long time.", author: "Steve Jobs" }, - { text: "Twenty years from now, you will be more disappointed by the things that you didn't do than by the ones you did do.", author: "Mark Twain" }, - { text: "The biggest risk is not taking any risk. In a world that's changing quickly, the only strategy that is guaranteed to fail is not taking risks.", author: "Mark Zuckerberg" }, - { text: "I have not failed. I've just found 10,000 ways that won't work.", author: "Thomas Edison" }, - { text: "Chase the vision, not the money; the money will end up following you.", author: "Tony Hsieh" }, - { text: "It's not about ideas. It's about making ideas happen.", author: "Scott Belsky" }, - { text: "If you can't fly, then run. If you can't run, then walk. If you can't walk, then crawl. But whatever you do, you have to keep moving forward.", author: "Martin Luther King Jr." }, - { text: "The only place where success comes before work is in the dictionary.", author: "Vidal Sassoon" }, - { text: "Make every detail perfect and limit the number of details to perfect.", author: "Jack Dorsey" }, - { text: "If you're not embarrassed by the first version of your product, you've launched too late.", author: "Reid Hoffman" }, - { text: "Your reputation is more important than your paycheck, and your integrity is worth more than your career.", author: "Ryan Freitas" }, - { text: "Every time you state what you want or believe, you're the first to hear it. It's a message to both you and others about what you think is possible.", author: "Oprah Winfrey" }, - { text: "The question isn't who is going to let me; it's who is going to stop me.", author: "Ayn Rand" }, - { text: "Innovation distinguishes between a leader and a follower.", author: "Steve Jobs" }, - { text: "There's no shortage of remarkable ideas, what's missing is the will to execute them.", author: "Seth Godin" }, - { text: "You don't need to have a 100-person company to develop that idea.", author: "Larry Page" }, - { text: "When everything seems to be going against you, remember that the airplane takes off against the wind, not with it.", author: "Henry Ford" }, - { text: "Don't be afraid to give up the good to go for the great.", author: "John D. Rockefeller" }, - { text: "Always deliver more than expected.", author: "Larry Page" }, - { text: "Risk more than others think is safe. Dream more than others think is practical.", author: "Howard Schultz" }, - { text: "The battles that count aren't the ones for gold medals. The struggles within yourself—the invisible, inevitable battles inside all of us—that's where it's at.", author: "Jesse Owens" }, - { text: "If you do build a great experience, customers tell each other about that. Word of mouth is very powerful.", author: "Jeff Bezos" }, - { text: "No matter how brilliant your mind or strategy, if you're playing a solo game, you'll always lose out to a team.", author: "Reid Hoffman" }, - { text: "If you want to achieve greatness stop asking for permission.", author: "Anonymous" }, - { text: "Things work out best for those who make the best of how things work out.", author: "John Wooden" }, - { text: "If you are not embarrassed by the first version of your product, you've launched too late.", author: "Reid Hoffman" } -]; - -// Function to get a random quote that's not the Mark Twain one -function getRandomQuote() { - // Filter out the Mark Twain quote for now to ensure variety - const filteredQuotes = businessQuotes.filter(quote => quote.author !== "Mark Twain"); - const randomIndex = Math.floor(Math.random() * filteredQuotes.length); - return filteredQuotes[randomIndex]; -} - export default function Content({ username, orderStats }: ContentProps) { - const [greeting, setGreeting] = useState("") - const [topProducts, setTopProducts] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const { toast } = useToast() + const [greeting, setGreeting] = useState(""); + const [topProducts, setTopProducts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); - // Initialize with a random quote that's not Mark Twain - const [randomQuote, setRandomQuote] = useState(getRandomQuote()) + // Initialize with a random quote from the quotes config + const [randomQuote, setRandomQuote] = useState(getRandomQuote()); + // Fetch top-selling products data const fetchTopProducts = async () => { try { - setIsLoading(true) + setIsLoading(true); - // Use clientFetch to handle URL routing and authentication properly const data = await clientFetch('/orders/top-products'); setTopProducts(data); } catch (err) { console.error("Error fetching top products:", err); - setError(err instanceof Error ? err.message : "Failed to fetch top products") + setError(err instanceof Error ? err.message : "Failed to fetch top products"); toast({ title: "Error loading top products", description: "Please try refreshing the page", variant: "destructive" - }) + }); } finally { - setIsLoading(false) + setIsLoading(false); } }; + // Initialize greeting and fetch data on component mount useEffect(() => { - setGreeting(getGreeting()) - - // Fetch top products + setGreeting(getGreeting()); fetchTopProducts(); - }, []) + }, []); + + // Retry fetching top products data + const handleRetry = () => { + fetchTopProducts(); + }; return (
@@ -128,6 +79,7 @@ export default function Content({ username, orderStats }: ContentProps) {

+ {/* Order Statistics */}
{statsConfig.map((stat) => ( - Retry + Retry )} + {isLoading ? ( + // Loading skeleton
{[...Array(5)].map((_, i) => (
-
-
-
-
+ +
+ + +
+
+ +
-
))}
) : error ? ( -
-

Error loading products

-

{error}

+ // Error state +
+
Failed to load products
- ) : topProducts.length > 0 ? ( + ) : topProducts.length === 0 ? ( + // Empty state +
+ +

No products sold yet

+

+ Your best-selling products will appear here after you make some sales. +

+
+ ) : ( + // Data view
- {topProducts.map(product => ( -
-
-
- {product.image ? ( - {product.name} { - const target = e.currentTarget; - target.src = ""; - if (target.parentElement) { - target.parentElement.classList.add("bg-muted"); - const iconSpan = document.createElement("span"); - iconSpan.className = "h-5 w-5 text-muted-foreground"; - target.parentElement.appendChild(iconSpan); - } - }} - /> - ) : ( - - )} -
-
-

{product.name}

-
+ {topProducts.map((product) => ( +
+
+ {!product.image && ( + + )} +
+
+

{product.name}

-

{product.count} sold

+
{product.count} sold
))}
- ) : ( -
- -

No sales data available yet

-

Your best-selling products will appear here once you have orders

-
)}
- ) + ); } diff --git a/components/modals/broadcast-dialog.tsx b/components/modals/broadcast-dialog.tsx index 3cc668c..3a7435d 100644 --- a/components/modals/broadcast-dialog.tsx +++ b/components/modals/broadcast-dialog.tsx @@ -90,6 +90,9 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) try { setIsSending(true); + const API_URL = process.env.NEXT_PUBLIC_API_URL; + if (!API_URL) throw new Error("API URL not configured"); + // Get auth token from cookie const authToken = document.cookie .split("; ") diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index 2039c18..b506965 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -51,7 +51,7 @@ export const ProductModal: React.FC = ({ // If productData.image is a *URL* (string), show it as a default preview useEffect(() => { if (productData.image && typeof productData.image === "string" && productData._id) { - setImagePreview(`/api/products/${productData._id}/image`); + setImagePreview(`${process.env.NEXT_PUBLIC_API_URL}/products/${productData._id}/image`); } else if (productData.image && typeof productData.image === "string") { // Image exists but no ID, this is probably a new product setImagePreview(null); diff --git a/config/quotes.ts b/config/quotes.ts new file mode 100644 index 0000000..9268b9f --- /dev/null +++ b/config/quotes.ts @@ -0,0 +1,91 @@ +/** + * Business motivation quotes for the dashboard + * Collection of quotes from successful entrepreneurs and business leaders + */ + +export interface Quote { + text: string; + author: string; +} + +export const businessQuotes: Quote[] = [ + // Steve Jobs quotes + { text: "Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work.", author: "Steve Jobs" }, + { text: "Innovation distinguishes between a leader and a follower.", author: "Steve Jobs" }, + { text: "If you really look closely, most overnight successes took a long time.", author: "Steve Jobs" }, + + // Entrepreneurs and CEOs + { text: "Your most unhappy customers are your greatest source of learning.", author: "Bill Gates" }, + { text: "The way to get started is to quit talking and begin doing.", author: "Walt Disney" }, + { text: "Opportunities don't happen. You create them.", author: "Chris Grosser" }, + { text: "The best way to predict the future is to create it.", author: "Peter Drucker" }, + { text: "If you are not willing to risk the usual, you will have to settle for the ordinary.", author: "Jim Rohn" }, + { text: "Chase the vision, not the money; the money will end up following you.", author: "Tony Hsieh" }, + { text: "It's not about ideas. It's about making ideas happen.", author: "Scott Belsky" }, + { text: "If you do build a great experience, customers tell each other about that. Word of mouth is very powerful.", author: "Jeff Bezos" }, + + // Persistence and growth + { text: "The secret of getting ahead is getting started.", author: "Mark Twain" }, + { text: "Success is not final; failure is not fatal: It is the courage to continue that counts.", author: "Winston Churchill" }, + { text: "Don't watch the clock; do what it does. Keep going.", author: "Sam Levenson" }, + { text: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" }, + { text: "If you can't fly, then run. If you can't run, then walk. If you can't walk, then crawl. But whatever you do, you have to keep moving forward.", author: "Martin Luther King Jr." }, + + // Risk and innovation + { text: "The biggest risk is not taking any risk. In a world that's changing quickly, the only strategy that is guaranteed to fail is not taking risks.", author: "Mark Zuckerberg" }, + { text: "I have not failed. I've just found 10,000 ways that won't work.", author: "Thomas Edison" }, + { text: "What would you do if you weren't afraid?", author: "Sheryl Sandberg" }, + { text: "When everything seems to be going against you, remember that the airplane takes off against the wind, not with it.", author: "Henry Ford" }, + { text: "If you're not embarrassed by the first version of your product, you've launched too late.", author: "Reid Hoffman" }, + + // Quality and execution + { text: "The only place where success comes before work is in the dictionary.", author: "Vidal Sassoon" }, + { text: "Make every detail perfect and limit the number of details to perfect.", author: "Jack Dorsey" }, + { text: "There's no shortage of remarkable ideas, what's missing is the will to execute them.", author: "Seth Godin" }, + { text: "Always deliver more than expected.", author: "Larry Page" }, + { text: "Your reputation is more important than your paycheck, and your integrity is worth more than your career.", author: "Ryan Freitas" }, + + // Teamwork and determination + { text: "No matter how brilliant your mind or strategy, if you're playing a solo game, you'll always lose out to a team.", author: "Reid Hoffman" }, + { text: "If you want to achieve greatness stop asking for permission.", author: "Anonymous" }, + { text: "Things work out best for those who make the best of how things work out.", author: "John Wooden" }, + { text: "The most valuable businesses of coming decades will be built by entrepreneurs who seek to empower people rather than try to make them obsolete.", author: "Peter Thiel" }, +]; + +// For backward compatibility with existing code +export const quotes = businessQuotes; +export default businessQuotes; + +/** + * Returns a random business quote from the collection + */ +export function getRandomQuote(): Quote { + const randomIndex = Math.floor(Math.random() * businessQuotes.length); + return businessQuotes[randomIndex]; +} + +/** + * Returns a random quote by a specific author if available, + * otherwise returns a random quote from any author + */ +export function getRandomQuoteByAuthor(author: string): Quote { + const authorQuotes = businessQuotes.filter(quote => + quote.author.toLowerCase() === author.toLowerCase() + ); + + if (authorQuotes.length === 0) { + return getRandomQuote(); + } + + const randomIndex = Math.floor(Math.random() * authorQuotes.length); + return authorQuotes[randomIndex]; +} + +/** + * Returns quotes filtered by a theme or keyword in the text + */ +export function getQuotesByTheme(keyword: string): Quote[] { + return businessQuotes.filter(quote => + quote.text.toLowerCase().includes(keyword.toLowerCase()) + ); +} \ No newline at end of file diff --git a/lib/api-utils.ts b/lib/api-utils.ts deleted file mode 100644 index 6801bcf..0000000 --- a/lib/api-utils.ts +++ /dev/null @@ -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 = {}): 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; -} \ No newline at end of file diff --git a/lib/client-service.ts b/lib/client-service.ts index 98e198f..b6a2ebe 100644 --- a/lib/client-service.ts +++ b/lib/client-service.ts @@ -1,7 +1,6 @@ 'use client'; import { toast } from "@/components/ui/use-toast"; -import { normalizeApiUrl, getAuthToken, createApiHeaders } from "./api-utils"; type FetchMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; @@ -12,26 +11,61 @@ interface FetchOptions { headers?: HeadersInit; } -/** - * Client-side fetch utility that ensures all requests go through the Next.js API proxy - */ +// 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( endpoint: string, options: FetchOptions = {} ): Promise { const { method = 'GET', body, headers = {}, ...rest } = options; - // Normalize the endpoint to ensure it starts with /api/ - const url = normalizeApiUrl(endpoint); + // Get the base API URL from environment or fallback + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; - // Get auth token and prepare headers - const requestHeaders = createApiHeaders(null, headers as Record); + // 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 = { + 'Content-Type': 'application/json', + ...(headers as Record), + }; + + if (authToken) { + // Backend expects "Bearer TOKEN" format + requestHeaders['Authorization'] = `Bearer ${authToken}`; + console.log('Authorization header set to:', `Bearer ${authToken.substring(0, 10)}...`); + } - // Log request details (useful for debugging) console.log('API Request:', { url, method, - hasAuthToken: requestHeaders.has('Authorization') + hasAuthToken: !!authToken }); const fetchOptions: RequestInit = { @@ -50,22 +84,24 @@ export async function fetchClient( if (!response.ok) { const errorData = await response.json().catch(() => ({})); - const errorMessage = errorData.message || errorData.error || `Request failed with status ${response.status}`; + const errorMessage = errorData.message || errorData.error || 'An error occurred'; + throw new Error(errorMessage); } - // Handle 204 No Content responses if (response.status === 204) { return {} as T; } - return await response.json(); + const data = await response.json(); + return data; } catch (error) { console.error('API request failed:', error); - // Only show toast in browser environment + // 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, diff --git a/lib/client-utils.ts b/lib/client-utils.ts index fccbe20..1f91d24 100644 --- a/lib/client-utils.ts +++ b/lib/client-utils.ts @@ -1,4 +1,47 @@ -import { normalizeApiUrl, getAuthToken, createApiHeaders } from './api-utils'; +/** + * 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 = {}): Headers { + const headers = new Headers({ + 'Content-Type': 'application/json', + ...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. diff --git a/lib/data-service.ts b/lib/data-service.ts index c16ca2c..04aa20b 100644 --- a/lib/data-service.ts +++ b/lib/data-service.ts @@ -1,27 +1,13 @@ /** * Client-side fetch function for API requests. - * A simple wrapper over fetch with improved error handling. */ -export async function fetchData(url: string, options: RequestInit = {}): Promise { +export async function fetchData(url: string, options: RequestInit = {}): Promise { try { - const res = await fetch(url, options); - - // Check for no content response - if (res.status === 204) { - return {} as T; - } - - // Handle errors - 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); - } - - // Parse normal response - return await res.json(); + 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; + console.error(`Fetch error at ${url}:`, error); + throw error; } } \ No newline at end of file diff --git a/lib/productData.ts b/lib/productData.ts index 1b249d6..1576440 100644 --- a/lib/productData.ts +++ b/lib/productData.ts @@ -1,12 +1,8 @@ import { fetchData } from '@/lib/data-service'; -import { normalizeApiUrl } from './api-utils'; -/** - * Fetches product data from the API - */ export const fetchProductData = async (url: string, authToken: string) => { try { - return await fetchData(normalizeApiUrl(url), { + return await fetchData(url, { headers: { Authorization: `Bearer ${authToken}` }, credentials: "include", }); @@ -16,9 +12,6 @@ export const fetchProductData = async (url: string, authToken: string) => { } }; -/** - * Saves product data to the API - */ export const saveProductData = async ( url: string, data: any, @@ -26,7 +19,7 @@ export const saveProductData = async ( method: "POST" | "PUT" = "POST" ) => { try { - return await fetchData(normalizeApiUrl(url), { + return await fetchData(url, { method, headers: { Authorization: `Bearer ${authToken}`, @@ -41,15 +34,12 @@ export const saveProductData = async ( } }; -/** - * Uploads a product image - */ -export const saveProductImage = async(url: string, file: File, authToken: string) => { +export const saveProductImage = async(url: string, file:File, authToken: string) => { try{ const formData = new FormData(); formData.append("file", file); - return await fetchData(normalizeApiUrl(url), { + return await fetchData(url, { method: "PUT", headers: { Authorization: `Bearer ${authToken}`, @@ -62,12 +52,9 @@ export const saveProductImage = async(url: string, file: File, authToken: string } } -/** - * Deletes a product - */ export const deleteProductData = async (url: string, authToken: string) => { try { - return await fetchData(normalizeApiUrl(url), { + return await fetchData(url, { method: "DELETE", headers: { Authorization: `Bearer ${authToken}`, @@ -81,12 +68,10 @@ export const deleteProductData = async (url: string, authToken: string) => { } }; -/** - * Fetches product stock information - */ +// Stock management functions export const fetchStockData = async (url: string, authToken: string) => { try { - return await fetchData(normalizeApiUrl(url), { + return await fetchData(url, { headers: { Authorization: `Bearer ${authToken}` }, credentials: "include", }); @@ -96,9 +81,6 @@ export const fetchStockData = async (url: string, authToken: string) => { } }; -/** - * Updates product stock information - */ export const updateProductStock = async ( productId: string, stockData: { @@ -109,7 +91,7 @@ export const updateProductStock = async ( authToken: string ) => { try { - const url = `/api/stock/${productId}`; + const url = `${process.env.NEXT_PUBLIC_API_URL}/stock/${productId}`; return await fetchData(url, { method: "PUT", headers: { diff --git a/lib/server-service.ts b/lib/server-service.ts index fc9fe5b..3fdfe2f 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -1,6 +1,21 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; -import { getServerApiUrl } from './api-utils'; + +/** + * Constructs a server-side API URL for backend requests + * Used in Server Components and API routes to directly access the backend API + * + * @param endpoint The API endpoint path + * @returns A complete URL to the backend API + */ +function getServerApiUrl(endpoint: string): string { + const apiUrl = process.env.SERVER_API_URL || 'https://internal-api.inboxi.ng/api'; + const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + return apiUrl.endsWith('/') + ? `${apiUrl}${cleanEndpoint}` + : `${apiUrl}/${cleanEndpoint}`; +} /** * Server-side fetch wrapper with authentication. @@ -22,8 +37,6 @@ export async function fetchServer( // Get the complete backend URL using the utility function const url = getServerApiUrl(endpoint); - console.log(`Making server request to: ${url}`); - // Make the request with proper auth headers const res = await fetch(url, { ...options, diff --git a/lib/shippingHelper.ts b/lib/shippingHelper.ts index e432fce..9c9463b 100644 --- a/lib/shippingHelper.ts +++ b/lib/shippingHelper.ts @@ -1,22 +1,9 @@ import { fetchData } from '@/lib/data-service'; -import { normalizeApiUrl } from './api-utils'; -/** - * Interface for shipping method data - */ -interface ShippingMethod { - name: string; - price: number; - _id?: string; -} - -/** - * Fetches all shipping methods for the current store - */ export const fetchShippingMethods = async (authToken: string) => { try { const res = await fetchData( - `/api/shipping-options`, + `${process.env.NEXT_PUBLIC_API_URL}/shipping-options`, { headers: { Authorization: `Bearer ${authToken}`, @@ -36,16 +23,19 @@ export const fetchShippingMethods = async (authToken: string) => { } }; -/** - * Adds a new shipping method - */ +interface ShippingMethod { + name: string; + price: number; + _id?: string; +} + export const addShippingMethod = async ( authToken: string, newShipping: Omit ): Promise => { try { const res = await fetchData( - `/api/shipping-options`, + `${process.env.NEXT_PUBLIC_API_URL}/shipping-options`, { method: "POST", headers: { @@ -79,13 +69,10 @@ export const addShippingMethod = async ( } }; -/** - * Deletes a shipping method by ID - */ export const deleteShippingMethod = async (authToken: string, id: string) => { try { const res = await fetchData( - `/api/shipping-options/${id}`, + `${process.env.NEXT_PUBLIC_API_URL}/shipping-options/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${authToken}` }, @@ -101,17 +88,14 @@ export const deleteShippingMethod = async (authToken: string, id: string) => { } }; -/** - * Updates an existing shipping method - */ export const updateShippingMethod = async ( authToken: string, id: string, - updatedShipping: Partial + updatedShipping: any ) => { try { const res = await fetchData( - `/api/shipping-options/${id}`, + `${process.env.NEXT_PUBLIC_API_URL}/shipping-options/${id}`, { method: "PUT", headers: { diff --git a/lib/storeHelper.ts b/lib/storeHelper.ts index d5e1867..ddb4167 100644 --- a/lib/storeHelper.ts +++ b/lib/storeHelper.ts @@ -1,17 +1,11 @@ import { fetchData } from '@/lib/data-service'; -import { normalizeApiUrl } from './api-utils'; -/** - * Sends authenticated API requests, ensuring they go through the Next.js API proxy - */ export const apiRequest = async (endpoint: string, method: string = "GET", body?: T | null) => { try { - // Enforce client-side execution if (typeof document === "undefined") { throw new Error("API requests must be made from the client side."); } - // Get authentication token const authToken = document.cookie .split("; ") .find((row) => row.startsWith("Authorization=")) @@ -22,7 +16,6 @@ export const apiRequest = async (endpoint: string, method: string = "GE throw new Error("No authentication token found"); } - // Prepare request options const options: RequestInit = { method, headers: { @@ -36,10 +29,10 @@ export const apiRequest = async (endpoint: string, method: string = "GE options.body = JSON.stringify(body); } - // Normalize URL to ensure it uses the Next.js API proxy - const url = normalizeApiUrl(endpoint); + 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(url, options); + const res = await fetchData(`${API_URL}${endpoint}`, options); if (!res) { const errorResponse = await res.json().catch(() => null); @@ -50,11 +43,11 @@ export const apiRequest = async (endpoint: string, method: string = "GE return res; } catch (error: unknown) { if (error instanceof Error) { - console.error(`API Request Error: ${error.message}`); + console.error(`🚨 API Request Error: ${error.message}`); throw new Error(error.message); } - console.error("An unknown error occurred", error); + console.error("❌ An unknown error occurred", error); throw new Error("An unknown error occurred"); } }; \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 17658c4..a15c1f9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,14 +23,7 @@ const nextConfig = { destination: 'https://internal-api.inboxi.ng/api/:path*', }, ]; - } , - // Build optimization settings for slower CPUs - experimental: { - swcMinify: true, - turbotrace: { - logLevel: 'error' - } - }, + }, // Reduce memory usage during builds onDemandEntries: { // Period (in ms) where the server will keep pages in the buffer diff --git a/package-lock.json b/package-lock.json index 9d97b1d..6f5021b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "lucide-react": "^0.454.0", "next": "^15.2.3", "next-themes": "latest", + "pusher-js": "^8.4.0", "react": "^19.0.0", "react-day-picker": "8.10.1", "react-dom": "^19.0.0", @@ -5763,6 +5764,15 @@ "node": ">=6" } }, + "node_modules/pusher-js": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz", + "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6623,6 +6633,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 7a24c4b..736fc65 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "lucide-react": "^0.454.0", "next": "^15.2.3", "next-themes": "latest", + "pusher-js": "^8.4.0", "react": "^19.0.0", "react-day-picker": "8.10.1", "react-dom": "^19.0.0",