From a40707ac2e6a44ebdcae9dbbc9ddf9677450918d Mon Sep 17 00:00:00 2001 From: Wajahat Islam Gul Date: Sat, 27 Sep 2025 07:50:11 +0500 Subject: [PATCH 1/3] feat: creater seller dashboard, fix products page error, add infinite loading to chat messages --- apps/seller-ui/public/placeholder-image.jpg | Bin 0 -> 1628 bytes .../(routes)/dashboard/all-products/page.tsx | 6 +- .../dashboard/orders/[orderId]/page.tsx | 6 +- .../src/app/(routes)/dashboard/page.tsx | 94 +++++++++++++++-- apps/seller-ui/src/hooks/chat.ts | 24 ++++- .../molecules/home/ProductSummaryList.tsx | 3 +- .../src/shared/organisms/chat/ChatWindow.tsx | 93 +++++++++++++---- apps/user-ui/public/placeholder-image.jpg | Bin 0 -> 1628 bytes apps/user-ui/src/app/(routes)/cart/page.tsx | 2 +- .../src/app/(routes)/product/[slug]/page.tsx | 4 +- .../src/app/(routes)/wishlist/page.tsx | 2 +- apps/user-ui/src/app/layout.tsx | 2 - .../src/components/ui/DarkModeToggle.tsx | 6 +- apps/user-ui/src/hooks/chat/useMessages.ts | 17 +++- .../molecules/product-details-card.tsx | 4 +- .../components/organisms/chat/ChatWindow.tsx | 96 ++++++++++++++---- .../components/organisms/product-card.tsx | 2 +- .../components/organisms/product-details.tsx | 4 +- .../shared/widgets/header/action-items.tsx | 2 + 19 files changed, 303 insertions(+), 64 deletions(-) create mode 100644 apps/seller-ui/public/placeholder-image.jpg create mode 100644 apps/user-ui/public/placeholder-image.jpg diff --git a/apps/seller-ui/public/placeholder-image.jpg b/apps/seller-ui/public/placeholder-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..589e800e382adecbca601e1fa84fd25c7076245a GIT binary patch literal 1628 zcmYk7eKga19LImZ&1S7GF}H-$EQQ1jLzKqFW|XEf(!;G8#ylL#H9a6TG^WH#Ax{UD zdANwRs3a+mNghj-Nm?rMoZX*v&%NLC{(L|0&*yy3>-)#|obQ)LvA5rc1OPWX886 znNgGtS}%n0$k=x_AK%Hos{;^kBM|`rZRS8asHYF=gF89EU=t$(X$!<>{4SB+_qJ;q_CY8|vET9y&)q_n{L@kEy6ZUU~9mdcKlJ3cf62*jMzWCD;1PdF@npubk9Zg#2;OqEJ5edAxEwLb7EbBxyIGd*{9 z)_L<<;|y;sKWp`*%B6~{*_htm zF!M=+8`vyv{h;OD^;n8pN9t)Xp_{9iw};&xv~(~&ZT#e(pfM@<-ltK(y6>Em6sbm& zOm_IvO5b*xRnFQ8uycV8p>i5@&skc>-&z`~z62_Z4 zuWwkSL|i_OcdtCRyVlLTwYC5J^&%Ja*%fFNA8ki+o_Ew-^ugx~80%L$6q6?fOqSo5 z3&vYa5F=$7xrN7W+?q1P-F@17)vH^}pt`dUfmJF6{XS`=i-(K(m+i>5;T&o1E~nVG znoDjb#-{s)A0j#5j5M{)E|%N$tfo$t=Y4-&h~Q=e<8`^VVPh%J6)wI$vP~@s7lh>EIUx}ekM}{5tZyG6jko@~;HCOGFd@)>V8pH_U9?0|s z_gW-2Fp<6B0O{k%qE#%e<_XW=lFKVO=$dz$*k9elw)SDboDN~XPB>we`2o7;Ba$-#wiakF1@H$0#~*-O4n4byc%*#0b8 zqhU=vwx0Op4My3MLCuS*!cBPcw&y8sqa9UB4Xupqf8*%GF@S|`QOO__-LQ#x+~DXf zi{59%I+=Xc;;kjBj)%(w4<<>XB;N1ohFNKeXBAug(VlQb2xzI-vE9eps&kTtu z2SCUzg&K(XnGZl-{g*!sd7WP~Fvw$I|IPby<$lgXz@Li@?Ms}0GxQN8$pdmw2Tw4< kgO`>)2oZprzxqaaJa}`ND3bvI?B+62AHu-@=a=yR0KPxGWdHyG literal 0 HcmV?d00001 diff --git a/apps/seller-ui/src/app/(routes)/dashboard/all-products/page.tsx b/apps/seller-ui/src/app/(routes)/dashboard/all-products/page.tsx index 09b40d6..71377ef 100644 --- a/apps/seller-ui/src/app/(routes)/dashboard/all-products/page.tsx +++ b/apps/seller-ui/src/app/(routes)/dashboard/all-products/page.tsx @@ -24,10 +24,10 @@ const AllProducts = () => { accessorKey: 'image', header: 'Image', cell: ({ row }: any) => { - const image = row.original.images[0]; + const image = row.original.images?.[0]; return ( {row.original.title} {

Are you sure you want to delete this product?

- Product will be moved to delete state and automatically deleted after 24 hours. You can reover it, in + Product will be moved to delete state and automatically deleted after 24 hours. You can recover it, in this time.

diff --git a/apps/seller-ui/src/app/(routes)/dashboard/orders/[orderId]/page.tsx b/apps/seller-ui/src/app/(routes)/dashboard/orders/[orderId]/page.tsx index 86a5dad..14f2e3e 100644 --- a/apps/seller-ui/src/app/(routes)/dashboard/orders/[orderId]/page.tsx +++ b/apps/seller-ui/src/app/(routes)/dashboard/orders/[orderId]/page.tsx @@ -259,7 +259,11 @@ export default function OrderDetailsPage() { className="w-20 h-20 rounded-lg object-cover" /> ) : ( - + {item.product?.title )}
diff --git a/apps/seller-ui/src/app/(routes)/dashboard/page.tsx b/apps/seller-ui/src/app/(routes)/dashboard/page.tsx index 983ebb8..e426a26 100644 --- a/apps/seller-ui/src/app/(routes)/dashboard/page.tsx +++ b/apps/seller-ui/src/app/(routes)/dashboard/page.tsx @@ -1,9 +1,91 @@ -import React from 'react' +'use client'; + +import React from 'react'; +import { + SellerHomeHeader, + SellerHomeOverview, + SellerHomeProductsSection, + SellerHomeOffersSection, + SellerHomeOrdersSection, + SellerHomeEventsSection, + SellerHomeHero, +} from '../../../shared/organisms/home'; +import { useOrders } from '../../../hooks/useOrders'; +import useProduct from '../../../hooks/useProduct'; +import { useShopInfo } from '../../../hooks/useShop'; +import { useDiscountCodes } from '../../../hooks/useDiscountCodes'; +import { useEvents } from '../../../hooks/useEvents'; +import useSeller from '../../../hooks/useSeller'; + +const DashboardPage = () => { + // Fetch data for dashboard + const { data: ordersData } = useOrders(); + const { productsQuery } = useProduct(); + const { data: shopData } = useShopInfo(); + const { data: discountCodesData } = useDiscountCodes(); + const { data: eventsData } = useEvents(1, 10); // Fetch first 10 events + const { seller } = useSeller(); + + const orders = ordersData?.data || []; + const products = productsQuery?.data || []; + const offers = discountCodesData?.discountCodes || []; + const events = eventsData?.events || []; + const shopName = shopData?.name || 'Your Shop'; + + // Hero props from shop and seller data + const heroProps = { + bannerUrl: shopData?.coverBanner || null, + avatarUrl: shopData?.avatar?.url || null, + name: shopData?.name || seller?.name || 'Your Shop', + category: shopData?.category || undefined, + address: shopData?.address || undefined, + website: shopData?.website || null, + rating: shopData?.ratings || undefined, + }; -const page = () => { return ( -
page
- ) -} +
+ {/* Hero Section */} + + + {/* Main Content */} +
+ {/* Header */} + + + {/* Overview Stats */} +
+ +
+ + {/* Content Grid */} +
+ {/* Recent Orders */} +
+ +
+ + {/* Products Section */} +
+ +
+
+ + {/* Additional Sections */} +
+ {/* Offers Section */} +
+ +
+ + {/* Events Section */} +
+ +
+
+
+
+ ); +}; -export default page \ No newline at end of file +export default DashboardPage; diff --git a/apps/seller-ui/src/hooks/chat.ts b/apps/seller-ui/src/hooks/chat.ts index a223d0d..5eb2b8b 100644 --- a/apps/seller-ui/src/hooks/chat.ts +++ b/apps/seller-ui/src/hooks/chat.ts @@ -1,4 +1,4 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; import axiosInstance from '../utils/axiosIsntance'; import { useWebSocket } from '../context/websocket-context'; @@ -106,14 +106,28 @@ export const useMessages = ({ }); }; +// Infinite query for seller chat messages +export const useInfiniteMessages = ({ conversationId, limit = 20 }: { conversationId: string; limit?: number }) => { + return useInfiniteQuery({ + queryKey: ['seller-messages', 'infinite', conversationId, limit], + queryFn: ({ pageParam = 1 }) => fetchSellerMessages({ conversationId, page: pageParam, limit }), + getNextPageParam: (lastPage) => { + return lastPage.hasMore ? lastPage.currentPage + 1 : undefined; + }, + initialPageParam: 1, + enabled: !!conversationId, + staleTime: 1000 * 10, // 10 seconds + refetchOnWindowFocus: false, + }); +}; + export const useSendMessage = () => { - const queryClient = useQueryClient(); const { ws, isConnected, error: wsError } = useWebSocket(); return useMutation< { conversationId: string }, Error, - MarkAsSeenData, + SendMessageData, { previousData?: { conversations: Conversation[] }; previousUnreadCount?: number; @@ -136,6 +150,8 @@ export const useSendMessage = () => { senderType: data.senderType, }), ); + + return { conversationId: data.conversationId }; }, }); }; @@ -179,7 +195,7 @@ export const useMarkAsSeen = () => { const hadUnreadEntry = Object.prototype.hasOwnProperty.call(unreadCounts, conversationId); const previousUnreadCount = unreadCounts[conversationId]; - queryClient.setQueryData(['seller-conversations'], (oldData: any) => { + queryClient.setQueryData(['seller-conversations'], (oldData: { conversations: Conversation[] } | undefined) => { if (!oldData) return oldData; return { diff --git a/apps/seller-ui/src/shared/molecules/home/ProductSummaryList.tsx b/apps/seller-ui/src/shared/molecules/home/ProductSummaryList.tsx index e6a7a9e..f0907ab 100644 --- a/apps/seller-ui/src/shared/molecules/home/ProductSummaryList.tsx +++ b/apps/seller-ui/src/shared/molecules/home/ProductSummaryList.tsx @@ -33,7 +33,8 @@ const ProductSummaryList: React.FC = ({ products, empty // eslint-disable-next-line @next/next/no-img-element {p.title} ) : ( - IMG + // eslint-disable-next-line @next/next/no-img-element + {p.title} )}
diff --git a/apps/seller-ui/src/shared/organisms/chat/ChatWindow.tsx b/apps/seller-ui/src/shared/organisms/chat/ChatWindow.tsx index 30343d2..5314325 100644 --- a/apps/seller-ui/src/shared/organisms/chat/ChatWindow.tsx +++ b/apps/seller-ui/src/shared/organisms/chat/ChatWindow.tsx @@ -1,10 +1,10 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Loader2, MessageCircle } from 'lucide-react'; import useSeller from '../../../hooks/useSeller'; import { useWebSocket } from '../../../context/websocket-context'; -import { useMarkAsSeen, useMessages, useSendMessage } from '../../../hooks/chat'; +import { useMarkAsSeen, useInfiniteMessages, useSendMessage } from '../../../hooks/chat'; import { ChatInput, ChatMessage } from '../../molecules'; import { useQueryClient } from '@tanstack/react-query'; @@ -27,26 +27,64 @@ interface ChatWindowProps { const ChatWindow: React.FC = ({ conversationId, user }) => { const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); const { seller } = useSeller(); - const { isConnected, isConnecting, error: wsError, ws, setMessageHandler } = useWebSocket(); + const { isConnected, isConnecting, error: wsError, setMessageHandler } = useWebSocket(); const [messages, setMessages] = useState([]); const queryClient = useQueryClient(); - const messagesQuery = useMessages({ + const messagesQuery = useInfiniteMessages({ conversationId: conversationId || '', - page: 1, - limit: 50, + limit: 20, }); const { mutate: sendMessage, isPending: isSending } = useSendMessage(); const { mutate: markAsSeen, isPending: isMarking } = useMarkAsSeen(); - const { data, isLoading, isError, error } = messagesQuery; - const userInfo = data?.user || user; + const { data, isLoading, isError, error, fetchNextPage, hasNextPage, isFetchingNextPage } = messagesQuery; - // Scroll to bottom when new messages arrive + // Flatten all messages from all pages and sort by creation date + const allMessages = useMemo(() => { + if (!data?.pages) return []; + + const messages = data.pages.flatMap((page) => page.messages); + // Sort by createdAt to ensure proper chronological order + return messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + }, [data?.pages]); + const userInfo = data?.pages[0]?.user || user; + + // Scroll to bottom when new messages arrive (only for new messages, not when loading more) useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); + // Only auto-scroll if we're not currently loading more messages + // and if the user is near the bottom of the chat + if (!isFetchingNextPage && messagesContainerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; // 100px threshold + + if (isNearBottom) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + } + }, [messages, isFetchingNextPage]); + + // Handle scroll to load more messages + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight } = e.currentTarget; + + // Load more messages when scrolled to top + if (scrollTop === 0 && hasNextPage && !isFetchingNextPage) { + // Store current scroll height to maintain position after loading + const currentScrollHeight = scrollHeight; + + fetchNextPage().then(() => { + // After loading, adjust scroll to maintain position + if (messagesContainerRef.current) { + const newScrollHeight = messagesContainerRef.current.scrollHeight; + const heightDifference = newScrollHeight - currentScrollHeight; + messagesContainerRef.current.scrollTop = heightDifference; + } + }); + } + }; // Mark messages as seen when conversation is selected useEffect(() => { @@ -80,10 +118,10 @@ const ChatWindow: React.FC = ({ conversationId, user }) => { }; useEffect(() => { - if (data) { - setMessages(data.messages); + if (allMessages.length > 0) { + setMessages(allMessages); } - }, [data]); + }, [allMessages]); //Receive new messages from websocket useEffect(() => { @@ -129,15 +167,26 @@ const ChatWindow: React.FC = ({ conversationId, user }) => { return; } - queryClient.setQueryData(['seller-messages', conversationId], (oldData: any) => { + // Update infinite query data - add new message to the last page (most recent) + queryClient.setQueryData(['seller-messages', 'infinite', conversationId, 20], (oldData: any) => { if (!oldData) return oldData; + + const newPages = [...oldData.pages]; + if (newPages.length > 0) { + const lastPageIndex = newPages.length - 1; + newPages[lastPageIndex] = { + ...newPages[lastPageIndex], + messages: [...newPages[lastPageIndex].messages, data], + }; + } + return { ...oldData, - messages: [...oldData.messages, data], + pages: newPages, }; }); - if (data.senderId !== seller?.id && !isMarking) { + if (data.senderId !== seller?.id && !isMarking && conversationId) { markAsSeen({ conversationId }); } }; @@ -193,7 +242,7 @@ const ChatWindow: React.FC = ({ conversationId, user }) => { return (
{/* Messages Area */} -
+
{messages.length === 0 ? (
@@ -204,6 +253,14 @@ const ChatWindow: React.FC = ({ conversationId, user }) => {
) : ( <> + {/* Load more indicator */} + {isFetchingNextPage && ( +
+ + Loading more messages... +
+ )} + {messages.map((message, index) => ( -)#|obQ)LvA5rc1OPWX886 znNgGtS}%n0$k=x_AK%Hos{;^kBM|`rZRS8asHYF=gF89EU=t$(X$!<>{4SB+_qJ;q_CY8|vET9y&)q_n{L@kEy6ZUU~9mdcKlJ3cf62*jMzWCD;1PdF@npubk9Zg#2;OqEJ5edAxEwLb7EbBxyIGd*{9 z)_L<<;|y;sKWp`*%B6~{*_htm zF!M=+8`vyv{h;OD^;n8pN9t)Xp_{9iw};&xv~(~&ZT#e(pfM@<-ltK(y6>Em6sbm& zOm_IvO5b*xRnFQ8uycV8p>i5@&skc>-&z`~z62_Z4 zuWwkSL|i_OcdtCRyVlLTwYC5J^&%Ja*%fFNA8ki+o_Ew-^ugx~80%L$6q6?fOqSo5 z3&vYa5F=$7xrN7W+?q1P-F@17)vH^}pt`dUfmJF6{XS`=i-(K(m+i>5;T&o1E~nVG znoDjb#-{s)A0j#5j5M{)E|%N$tfo$t=Y4-&h~Q=e<8`^VVPh%J6)wI$vP~@s7lh>EIUx}ekM}{5tZyG6jko@~;HCOGFd@)>V8pH_U9?0|s z_gW-2Fp<6B0O{k%qE#%e<_XW=lFKVO=$dz$*k9elw)SDboDN~XPB>we`2o7;Ba$-#wiakF1@H$0#~*-O4n4byc%*#0b8 zqhU=vwx0Op4My3MLCuS*!cBPcw&y8sqa9UB4Xupqf8*%GF@S|`QOO__-LQ#x+~DXf zi{59%I+=Xc;;kjBj)%(w4<<>XB;N1ohFNKeXBAug(VlQb2xzI-vE9eps&kTtu z2SCUzg&K(XnGZl-{g*!sd7WP~Fvw$I|IPby<$lgXz@Li@?Ms}0GxQN8$pdmw2Tw4< kgO`>)2oZprzxqaaJa}`ND3bvI?B+62AHu-@=a=yR0KPxGWdHyG literal 0 HcmV?d00001 diff --git a/apps/user-ui/src/app/(routes)/cart/page.tsx b/apps/user-ui/src/app/(routes)/cart/page.tsx index a68032a..ca7e586 100644 --- a/apps/user-ui/src/app/(routes)/cart/page.tsx +++ b/apps/user-ui/src/app/(routes)/cart/page.tsx @@ -89,7 +89,7 @@ const CartScreen = () => { return (
{product.title} openGraph: { title: product.name || 'Product Details', description: product.short_description || product.description || 'Product details and information', - images: product.images?.[0]?.url ? [product.images[0].url] : [], + images: product.images?.[0]?.url ? [product.images[0].url] : ['/placeholder-image.jpg'], type: 'website', }, twitter: { card: 'summary_large_image', title: product.name || 'Product Details', description: product.short_description || product.description || 'Product details and information', - images: product.images?.[0]?.url ? [product.images[0].url] : [], + images: product.images?.[0]?.url ? [product.images[0].url] : ['/placeholder-image.jpg'], }, }; } catch (error) { diff --git a/apps/user-ui/src/app/(routes)/wishlist/page.tsx b/apps/user-ui/src/app/(routes)/wishlist/page.tsx index 3bb8169..3429ec6 100644 --- a/apps/user-ui/src/app/(routes)/wishlist/page.tsx +++ b/apps/user-ui/src/app/(routes)/wishlist/page.tsx @@ -50,7 +50,7 @@ const WishlistPage = () => { return (
{product.title} {children}