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/components/dashboard/BuyerOrderInfo.tsx b/components/dashboard/BuyerOrderInfo.tsx index aa95cb4..af1c231 100644 --- a/components/dashboard/BuyerOrderInfo.tsx +++ b/components/dashboard/BuyerOrderInfo.tsx @@ -10,23 +10,22 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { clientFetch } from "@/lib/client-utils"; +import { getCookie } from "@/lib/client-utils"; +import axios from "axios"; import { useRouter } from "next/navigation"; -interface OrderProduct { - productId: string; - quantity: number; - pricePerUnit: number; - totalItemPrice: number; -} - interface Order { _id: string; orderId: number; status: string; totalPrice: number; orderDate: string; - products: OrderProduct[]; + products: Array<{ + productId: string; + quantity: number; + pricePerUnit: number; + totalItemPrice: number; + }>; } interface BuyerOrderInfoProps { @@ -34,26 +33,17 @@ interface BuyerOrderInfoProps { chatId: string; } -/** - * Component that displays order information for a buyer in a chat - * Shows a tooltip with recent orders and allows navigation to order details - */ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) { const router = useRouter(); - - // State const [loading, setLoading] = useState(false); const [orders, setOrders] = useState([]); const [hasOrders, setHasOrders] = useState(null); const [isTooltipOpen, setIsTooltipOpen] = useState(false); - - // Refs to prevent unnecessary re-renders and API calls const lastFetchedRef = useRef(0); const isFetchingRef = useRef(false); + const tooltipDelayRef = useRef(null); - /** - * Fetch buyer orders from the API - */ + // Fetch data without unnecessary dependencies to reduce render cycles const fetchBuyerOrders = useCallback(async (force = false) => { // Prevent multiple simultaneous fetches if (isFetchingRef.current) return; @@ -72,12 +62,27 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) setLoading(true); try { - // Use clientFetch to handle auth and API routing automatically - const response = await clientFetch(`/chats/${chatId}/orders?limit=10`); + const authToken = getCookie("Authorization"); - if (response && response.orders) { - setOrders(response.orders); - setHasOrders(response.orders.length > 0); + if (!authToken) { + isFetchingRef.current = false; + setLoading(false); + return; + } + + const authAxios = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + // Use the new endpoint that works with sub-users + const response = await authAxios.get(`/chats/${chatId}/orders?limit=10`); // Limit to fewer orders for faster response + + if (response.data && response.data.orders) { + setOrders(response.data.orders); + setHasOrders(response.data.orders.length > 0); } else { setHasOrders(false); } @@ -90,44 +95,48 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) setLoading(false); isFetchingRef.current = false; } - }, [chatId, orders.length, hasOrders]); + }, [chatId]); // Minimize dependencies even further - // Fetch orders when component mounts + // Start fetching immediately when component mounts useEffect(() => { if (chatId) { + // Immediately attempt to fetch in the background fetchBuyerOrders(); } + + return () => { + // Clean up any pending timeouts + if (tooltipDelayRef.current) { + clearTimeout(tooltipDelayRef.current); + } + }; }, [chatId, fetchBuyerOrders]); - /** - * Navigate to order details page - */ const handleViewOrder = (orderId: string) => { router.push(`/dashboard/orders/${orderId}`); }; - /** - * Handle mouse enter on the button to start loading data - */ + // Handle hover with immediate tooltip opening const handleButtonMouseEnter = () => { + // Start fetching data, but don't wait for it to complete if (!isFetchingRef.current) { - queueMicrotask(() => fetchBuyerOrders()); + queueMicrotask(() => { + fetchBuyerOrders(); + }); } }; - /** - * Handle tooltip state change, load data if opening - */ + // Handle tooltip state change const handleTooltipOpenChange = (open: boolean) => { setIsTooltipOpen(open); if (open && !isFetchingRef.current) { - queueMicrotask(() => fetchBuyerOrders()); + queueMicrotask(() => { + fetchBuyerOrders(); + }); } }; - /** - * Format price as currency - */ + // Format the price as currency const formatPrice = (price: number) => { return `£${price.toFixed(2)}`; }; @@ -137,26 +146,13 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) return null; } - // Calculate total products across all orders + // Precompute product count for button display (only if we have orders) const productCount = orders.length > 0 ? orders.reduce((total, order) => { return total + order.products.reduce((sum, product) => sum + product.quantity, 0); }, 0) : 0; - /** - * Get badge variant based on order status - */ - const getStatusBadgeVariant = (status: string) => { - switch (status) { - case "paid": return "paid"; - case "unpaid": return "unpaid"; - case "shipped": return "shipped"; - case "completed": return "completed"; - default: return "secondary"; - } - }; - return ( @@ -215,10 +211,13 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) Order #{order.orderId} - + {order.status.toUpperCase()} diff --git a/components/dashboard/NewChatForm.tsx b/components/dashboard/NewChatForm.tsx index f2f4fbf..d8fdb83 100644 --- a/components/dashboard/NewChatForm.tsx +++ b/components/dashboard/NewChatForm.tsx @@ -8,8 +8,9 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ArrowLeft, Send, RefreshCw, Search, User } from "lucide-react"; +import axios from "axios"; import { toast } from "sonner"; -import { clientFetch } from "@/lib/client-utils"; +import { getCookie } from "@/lib/client-utils"; import debounce from "lodash/debounce"; interface User { @@ -17,63 +18,60 @@ interface User { telegramUsername: string | null; } -interface Store { - _id: string; - name: string; -} - export default function NewChatForm() { const router = useRouter(); const searchParams = useSearchParams(); - - // State management const [buyerId, setBuyerId] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [searching, setSearching] = useState(false); + const [open, setOpen] = useState(false); const [initialMessage, setInitialMessage] = useState(""); - const [vendorStores, setVendorStores] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingUser, setLoadingUser] = useState(false); + const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]); const [selectedStore, setSelectedStore] = useState(""); const [selectedUser, setSelectedUser] = useState(null); - // Loading states - const [searching, setSearching] = useState(false); - const [loading, setLoading] = useState(false); - const [loadingUser, setLoadingUser] = useState(false); - const [loadingStores, setLoadingStores] = useState(false); - - // UI state - const [open, setOpen] = useState(false); + // Create an axios instance with auth + const getAuthAxios = () => { + const authToken = getCookie("Authorization"); + if (!authToken) return null; + + return axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { Authorization: `Bearer ${authToken}` } + }); + }; // Parse URL parameters for buyerId and fetch user details if present useEffect(() => { const buyerIdParam = searchParams.get('buyerId'); if (buyerIdParam) { setBuyerId(buyerIdParam); + // We'll fetch user details after stores are loaded } }, [searchParams]); - // Fetch vendor stores on component mount - useEffect(() => { - fetchVendorStores(); - }, []); - - // Fetch user information if buyer ID changes - useEffect(() => { - if (buyerId && vendorStores.length > 0) { - fetchUserById(buyerId); - } - }, [buyerId, vendorStores]); - // Fetch user information by ID const fetchUserById = async (userId: string) => { if (!userId || !vendorStores[0]?._id) return; + const authAxios = getAuthAxios(); + if (!authAxios) { + toast.error("You need to be logged in"); + router.push("/auth/login"); + return; + } + setLoadingUser(true); try { - const userData = await clientFetch(`/chats/user/${userId}`); - if (userData) { - setSelectedUser(userData); - setSearchQuery(userData.telegramUsername || `User ${userId}`); + const response = await authAxios.get(`/chats/user/${userId}`); + if (response.data) { + setSelectedUser(response.data); + setSearchQuery(response.data.telegramUsername || `User ${userId}`); + } else { + // Just leave the buyerId as is without username display } } catch (error) { console.error("Error fetching user:", error); @@ -83,47 +81,17 @@ export default function NewChatForm() { } }; - // Fetch vendor stores - const fetchVendorStores = async () => { - setLoadingStores(true); - try { - // Get stores - const stores = await clientFetch('/storefront'); - - // Handle both array and single object responses - if (Array.isArray(stores)) { - setVendorStores(stores); - if (stores.length > 0) { - setSelectedStore(stores[0]._id); - } - } else if (stores && typeof stores === 'object' && stores._id) { - const singleStore = [stores]; - setVendorStores(singleStore); - setSelectedStore(stores._id); - } - } catch (error) { - console.error("Error fetching stores:", error); - toast.error("Failed to load your stores"); - - // Redirect if there's a login issue - if (error instanceof Error && error.message.includes('logged in')) { - router.push("/auth/login"); - } - } finally { - setLoadingStores(false); - } - }; - // Debounced search function const searchUsers = debounce(async (query: string) => { - if (!query.trim() || !selectedStore) return; + if (!query.trim() || !vendorStores[0]?._id) return; + + const authAxios = getAuthAxios(); + if (!authAxios) return; - setSearching(true); try { - const results = await clientFetch( - `/chats/search/users?query=${encodeURIComponent(query)}&storeId=${selectedStore}` - ); - setSearchResults(results); + setSearching(true); + const response = await authAxios.get(`/chats/search/users?query=${encodeURIComponent(query)}&storeId=${vendorStores[0]._id}`); + setSearchResults(response.data); } catch (error) { console.error("Error searching users:", error); toast.error("Failed to search users"); @@ -147,40 +115,113 @@ export default function NewChatForm() { setOpen(false); }; - // Navigation handlers + // Fetch vendor stores + useEffect(() => { + const fetchVendorStores = async () => { + const authAxios = getAuthAxios(); + if (!authAxios) { + toast.error("You must be logged in to start a chat"); + router.push("/auth/login"); + return; + } + + try { + // Get vendor profile first + const vendorResponse = await authAxios.get('/auth/me'); + + // Extract vendor ID properly + const vendorId = vendorResponse.data.vendor?._id; + + if (!vendorId) { + console.error("Vendor ID not found in profile response:", vendorResponse.data); + toast.error("Could not retrieve vendor information"); + return; + } + + // Fetch store + const storeResponse = await authAxios.get(`/storefront`); + + // Handle both array and single object responses + if (Array.isArray(storeResponse.data)) { + setVendorStores(storeResponse.data); + if (storeResponse.data.length > 0) { + setSelectedStore(storeResponse.data[0]._id); + } + } else if (storeResponse.data && typeof storeResponse.data === 'object' && storeResponse.data._id) { + const singleStore = [storeResponse.data]; + setVendorStores(singleStore); + setSelectedStore(storeResponse.data._id); + } else { + console.error("Expected store data but received:", storeResponse.data); + setVendorStores([]); + toast.error("Failed to load store data in expected format"); + } + + // Now that we have the store, fetch user details if buyerId was set + const buyerIdParam = searchParams.get('buyerId'); + if (buyerIdParam) { + fetchUserById(buyerIdParam); + } + } catch (error) { + console.error("Error fetching store:", error); + toast.error("Failed to load store"); + setVendorStores([]); + } + }; + + fetchVendorStores(); + }, []); + const handleBackClick = () => { router.push("/dashboard/chats"); }; - // Start new chat const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!buyerId || !initialMessage.trim() || !selectedStore) { - toast.error("Please fill in all required fields"); + if (!buyerId) { + toast.error("Please select a customer"); return; } + if (vendorStores.length === 0) { + toast.error("No store available. Please create a store first."); + return; + } + + const storeId = vendorStores[0]._id; + setLoading(true); try { - const response = await clientFetch('/chats', { - method: 'POST', - body: JSON.stringify({ - buyerId, - storeId: selectedStore, - initialMessage: initialMessage.trim() - }), - headers: { - 'Content-Type': 'application/json' - } + const authAxios = getAuthAxios(); + if (!authAxios) { + toast.error("You need to be logged in"); + router.push("/auth/login"); + return; + } + + const response = await authAxios.post("/chats/create", { + buyerId, + storeId: storeId, + initialMessage: initialMessage.trim() || undefined }); - // Navigate to the new chat - toast.success("Chat created successfully"); - router.push(`/dashboard/chats/${response._id}`); - } catch (error) { + if (response.data.chatId) { + toast.success("Chat created successfully!"); + router.push(`/dashboard/chats/${response.data.chatId}`); + } else if (response.data.error === "Chat already exists") { + toast.info("Chat already exists, redirecting..."); + router.push(`/dashboard/chats/${response.data.chatId}`); + } + } catch (error: any) { console.error("Error creating chat:", error); - toast.error("Failed to create chat. Please try again."); + + if (error.response?.status === 409) { + toast.info("Chat already exists, redirecting..."); + router.push(`/dashboard/chats/${error.response.data.chatId}`); + } else { + toast.error("Failed to create chat"); + } } finally { setLoading(false); } 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 index 04c2411..653bff6 100644 --- a/config/quotes.ts +++ b/config/quotes.ts @@ -1,87 +1,22 @@ -/** - * Business motivation quotes for the dashboard - * Collection of quotes from successful entrepreneurs and business leaders - */ +// Random quotes for loading/dashboard screens -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" }, +const quotes = [ + "Checking inventory...", + "Analyzing market trends...", + "Connecting to secure channels...", + "Loading vendor dashboard...", + "Processing request...", + "Preparing your dashboard...", + "Calculating revenue metrics...", + "Gathering order data...", + "Initializing secure connection...", + "Syncing with database...", + "Loading vendor interface..." ]; -/** - * Returns a random business quote from the collection - */ -export function getRandomQuote(): Quote { - const randomIndex = Math.floor(Math.random() * businessQuotes.length); - return businessQuotes[randomIndex]; +export function getRandomQuote(): string { + const randomIndex = Math.floor(Math.random() * quotes.length); + return quotes[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 +export default quotes; \ No newline at end of file diff --git a/lib/api-utils.ts b/lib/api-utils.ts deleted file mode 100644 index 4ee689d..0000000 --- a/lib/api-utils.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * API utilities for consistent request handling - */ - -/** - * Normalizes a URL to ensure it passes through the Next.js API proxy - * This ensures all client-side requests go through the Next.js rewrites. - * - * @param url The endpoint URL to normalize - * @returns A normalized URL with proper /api prefix - */ -export function normalizeApiUrl(url: string): string { - if (url.startsWith('/api/')) { - return url; // Already has /api/ prefix - } else { - // Add /api prefix, handling any leading slashes - const cleanUrl = url.startsWith('/') ? url : `/${url}`; - return `/api${cleanUrl}`; - } -} - -/** - * 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 - */ -export 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}`; -} - -/** - * 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'); -} - -/** - * Check if the user is logged in - */ -export function isAuthenticated(): boolean { - return !!getAuthToken(); -} - -/** - * 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 - */ -export 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; -} \ 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 59a0676..964811e 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. @@ -12,6 +55,8 @@ export async function clientFetch(url: string, options: RequestInit = { // Normalize URL to ensure it uses the Next.js API proxy const fullUrl = normalizeApiUrl(url); + console.log(`Fetching URL: ${fullUrl}`); + const res = await fetch(fullUrl, { ...options, headers, 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..e1b626f 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. 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/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",