From 1aaf349d3de568d8ef5fc46b4cb3ac8524dbb430 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:52:02 +0000 Subject: [PATCH 1/8] fix --- lib/server-service.ts | 2 +- tsconfig.json | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/server-service.ts b/lib/server-service.ts index 0560b86..fc9fe5b 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -12,7 +12,7 @@ export async function fetchServer( options: RequestInit = {} ): Promise { // Get auth token from cookies - const cookieStore = cookies(); + const cookieStore = await cookies(); const authToken = cookieStore.get('Authorization')?.value; // Redirect to login if not authenticated diff --git a/tsconfig.json b/tsconfig.json index e7ff90f..d81d4ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,9 +22,19 @@ } ], "paths": { - "@/*": ["./*"] - } + "@/*": [ + "./*" + ] + }, + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From 0778c279d0089292d37152554146ef70bc988cff Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:59:13 +0000 Subject: [PATCH 2/8] Update layout.tsx --- app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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({ From 92a27192ee51013f7fa5367d50adcf279153ba00 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:08:18 +0000 Subject: [PATCH 3/8] weewoo --- components/dashboard/NewChatForm.tsx | 219 +++++++++++---------------- components/dashboard/content.tsx | 182 ++++++++-------------- config/quotes.ts | 87 +++++++++++ 3 files changed, 242 insertions(+), 246 deletions(-) create mode 100644 config/quotes.ts diff --git a/components/dashboard/NewChatForm.tsx b/components/dashboard/NewChatForm.tsx index d8fdb83..f2f4fbf 100644 --- a/components/dashboard/NewChatForm.tsx +++ b/components/dashboard/NewChatForm.tsx @@ -8,9 +8,8 @@ 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 { getCookie } from "@/lib/client-utils"; +import { clientFetch } from "@/lib/client-utils"; import debounce from "lodash/debounce"; interface User { @@ -18,60 +17,63 @@ 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 [loading, setLoading] = useState(false); - const [loadingUser, setLoadingUser] = useState(false); - const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]); + const [vendorStores, setVendorStores] = useState([]); const [selectedStore, setSelectedStore] = useState(""); const [selectedUser, setSelectedUser] = useState(null); - // 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}` } - }); - }; + // 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); // 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 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 + const userData = await clientFetch(`/chats/user/${userId}`); + if (userData) { + setSelectedUser(userData); + setSearchQuery(userData.telegramUsername || `User ${userId}`); } } catch (error) { console.error("Error fetching user:", error); @@ -81,17 +83,47 @@ 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() || !vendorStores[0]?._id) return; - - const authAxios = getAuthAxios(); - if (!authAxios) return; + if (!query.trim() || !selectedStore) return; + setSearching(true); try { - setSearching(true); - const response = await authAxios.get(`/chats/search/users?query=${encodeURIComponent(query)}&storeId=${vendorStores[0]._id}`); - setSearchResults(response.data); + const results = await clientFetch( + `/chats/search/users?query=${encodeURIComponent(query)}&storeId=${selectedStore}` + ); + setSearchResults(results); } catch (error) { console.error("Error searching users:", error); toast.error("Failed to search users"); @@ -115,113 +147,40 @@ export default function NewChatForm() { setOpen(false); }; - // 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(); - }, []); - + // Navigation handlers const handleBackClick = () => { router.push("/dashboard/chats"); }; + // Start new chat const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!buyerId) { - toast.error("Please select a customer"); + if (!buyerId || !initialMessage.trim() || !selectedStore) { + toast.error("Please fill in all required fields"); 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 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 + const response = await clientFetch('/chats', { + method: 'POST', + body: JSON.stringify({ + buyerId, + storeId: selectedStore, + initialMessage: initialMessage.trim() + }), + headers: { + 'Content-Type': 'application/json' + } }); - 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) { + // Navigate to the new chat + toast.success("Chat created successfully"); + router.push(`/dashboard/chats/${response._id}`); + } catch (error) { console.error("Error creating chat:", error); - - 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"); - } + toast.error("Failed to create chat. Please try again."); } finally { setLoading(false); } 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/config/quotes.ts b/config/quotes.ts new file mode 100644 index 0000000..04c2411 --- /dev/null +++ b/config/quotes.ts @@ -0,0 +1,87 @@ +/** + * 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" }, +]; + +/** + * 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 From 1e395b8684f62f95694c5dedee27c69435179dff Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:09:49 +0000 Subject: [PATCH 4/8] Update BuyerOrderInfo.tsx --- components/dashboard/BuyerOrderInfo.tsx | 117 ++++++++++++------------ 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/components/dashboard/BuyerOrderInfo.tsx b/components/dashboard/BuyerOrderInfo.tsx index af1c231..aa95cb4 100644 --- a/components/dashboard/BuyerOrderInfo.tsx +++ b/components/dashboard/BuyerOrderInfo.tsx @@ -10,22 +10,23 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { getCookie } from "@/lib/client-utils"; -import axios from "axios"; +import { clientFetch } from "@/lib/client-utils"; 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: Array<{ - productId: string; - quantity: number; - pricePerUnit: number; - totalItemPrice: number; - }>; + products: OrderProduct[]; } interface BuyerOrderInfoProps { @@ -33,17 +34,26 @@ 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 data without unnecessary dependencies to reduce render cycles + /** + * Fetch buyer orders from the API + */ const fetchBuyerOrders = useCallback(async (force = false) => { // Prevent multiple simultaneous fetches if (isFetchingRef.current) return; @@ -62,27 +72,12 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) setLoading(true); try { - const authToken = getCookie("Authorization"); + // Use clientFetch to handle auth and API routing automatically + const response = await clientFetch(`/chats/${chatId}/orders?limit=10`); - 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); + if (response && response.orders) { + setOrders(response.orders); + setHasOrders(response.orders.length > 0); } else { setHasOrders(false); } @@ -95,48 +90,44 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) setLoading(false); isFetchingRef.current = false; } - }, [chatId]); // Minimize dependencies even further + }, [chatId, orders.length, hasOrders]); - // Start fetching immediately when component mounts + // Fetch orders 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 hover with immediate tooltip opening + /** + * Handle mouse enter on the button to start loading data + */ const handleButtonMouseEnter = () => { - // Start fetching data, but don't wait for it to complete if (!isFetchingRef.current) { - queueMicrotask(() => { - fetchBuyerOrders(); - }); + queueMicrotask(() => fetchBuyerOrders()); } }; - // Handle tooltip state change + /** + * Handle tooltip state change, load data if opening + */ const handleTooltipOpenChange = (open: boolean) => { setIsTooltipOpen(open); if (open && !isFetchingRef.current) { - queueMicrotask(() => { - fetchBuyerOrders(); - }); + queueMicrotask(() => fetchBuyerOrders()); } }; - // Format the price as currency + /** + * Format price as currency + */ const formatPrice = (price: number) => { return `£${price.toFixed(2)}`; }; @@ -146,13 +137,26 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) return null; } - // Precompute product count for button display (only if we have orders) + // Calculate total products across all 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 ( @@ -211,13 +215,10 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) Order #{order.orderId}
- + {order.status.toUpperCase()}
From 39c349509cbca226cdf1012d79536d73c660c3aa Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:46:11 +0000 Subject: [PATCH 5/8] ugh --- .env.production | 1 - Dockerfile | 12 +- app/actions.ts | 99 +++++++++++ app/dashboard/chats/[id]/page.tsx | 16 +- components/dashboard/BuyerOrderInfo.tsx | 117 +++++++------ components/dashboard/NewChatForm.tsx | 219 ++++++++++++++---------- components/modals/broadcast-dialog.tsx | 3 + components/modals/product-modal.tsx | 2 +- config/quotes.ts | 99 ++--------- lib/api-utils.ts | 76 -------- lib/client-service.ts | 64 +++++-- lib/client-utils.ts | 47 ++++- lib/data-service.ts | 26 +-- lib/productData.ts | 34 +--- lib/server-service.ts | 17 +- lib/shippingHelper.ts | 38 ++-- lib/storeHelper.ts | 17 +- package-lock.json | 16 ++ package.json | 1 + 19 files changed, 477 insertions(+), 427 deletions(-) create mode 100644 app/actions.ts delete mode 100644 lib/api-utils.ts 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", From 333f657b7e75d623f56b328e677f4bc41c9ceb49 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:50:19 +0000 Subject: [PATCH 6/8] Update quotes.ts --- config/quotes.ts | 107 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 21 deletions(-) diff --git a/config/quotes.ts b/config/quotes.ts index 653bff6..fa38982 100644 --- a/config/quotes.ts +++ b/config/quotes.ts @@ -1,22 +1,87 @@ -// Random quotes for loading/dashboard screens +/** + * Business motivation quotes for the dashboard + * Collection of quotes from successful entrepreneurs and business leaders + */ -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..." -]; - -export function getRandomQuote(): string { - const randomIndex = Math.floor(Math.random() * quotes.length); - return quotes[randomIndex]; -} - -export default quotes; \ No newline at end of file +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" }, + ]; + + /** + * 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 From 5a2a9d59b7f9008442aac14b8d832f8ad585644c Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:54:26 +0000 Subject: [PATCH 7/8] Update next.config.mjs --- next.config.mjs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 29ae3a9..0f4915d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -24,13 +24,8 @@ const nextConfig = { }, ]; }, - // Build optimization settings for slower CPUs - experimental: { - swcMinify: true, - turbotrace: { - logLevel: 'error' - } - }, + // Build optimization settings for Next.js 15+ + swcMinify: true, // Reduce memory usage during builds onDemandEntries: { // Period (in ms) where the server will keep pages in the buffer From dd25a45abc4cd7b7795ac9b6b975a8a4283f8a20 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Mon, 24 Mar 2025 01:56:17 +0000 Subject: [PATCH 8/8] clean up --- config/quotes.ts | 74 +++++++++++++++++++++++-------------------- lib/client-utils.ts | 2 -- lib/server-service.ts | 2 -- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/config/quotes.ts b/config/quotes.ts index fa38982..9268b9f 100644 --- a/config/quotes.ts +++ b/config/quotes.ts @@ -6,14 +6,14 @@ export interface Quote { text: string; author: string; - } - - export const businessQuotes: Quote[] = [ +} + +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" }, @@ -23,65 +23,69 @@ export interface Quote { { 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" }, - ]; - - /** - * Returns a random business quote from the collection - */ - export function getRandomQuote(): Quote { +]; + +// 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() +} + +/** + * 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(); + 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()) +} + +/** + * 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 +} \ No newline at end of file diff --git a/lib/client-utils.ts b/lib/client-utils.ts index 964811e..8053dd7 100644 --- a/lib/client-utils.ts +++ b/lib/client-utils.ts @@ -55,8 +55,6 @@ 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/server-service.ts b/lib/server-service.ts index e1b626f..3fdfe2f 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -37,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,