diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fa5bae..2ad8954 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,10 @@ "@fontsource-variable/inter": "^5.2.8", "@hookform/resolvers": "^5.4.0", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/table-core": "^8.21.3", "axios": "^1.17.0", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.17.0", @@ -19,8 +22,10 @@ "maplibre-gl": "^5.24.0", "radix-ui": "^1.5.0", "react": "^19.2.6", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.6", "react-hook-form": "^7.78.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.17.0", "shadcn": "^4.11.0", "tailwind-merge": "^3.6.0", @@ -956,6 +961,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -3284,6 +3295,39 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -3775,6 +3819,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3973,7 +4029,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/data-uri-to-buffer": { @@ -5101,6 +5156,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6881,6 +6945,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", @@ -6909,6 +6983,23 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1699437..84c716b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,10 @@ "@fontsource-variable/inter": "^5.2.8", "@hookform/resolvers": "^5.4.0", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/table-core": "^8.21.3", "axios": "^1.17.0", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.17.0", @@ -21,8 +24,10 @@ "maplibre-gl": "^5.24.0", "radix-ui": "^1.5.0", "react": "^19.2.6", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.6", "react-hook-form": "^7.78.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.17.0", "shadcn": "^4.11.0", "tailwind-merge": "^3.6.0", @@ -41,4 +46,4 @@ "globals": "^17.6.0", "vite": "^8.0.12" } -} \ No newline at end of file +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 17722f5..13d3e0e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,15 +7,17 @@ import { } from './components/RoleBasedSidebar'; import WorkspaceView from './components/WorkspaceView'; - +import { AuthProvider } from './context/AuthContext'; import { CartProvider } from './context/CartContext'; +import { GuestRoute, ProtectedRoute, SellerRoute } from './components/RouteGuards'; +import { Toaster } from 'react-hot-toast'; -// Auth Pages import Login from './pages/shared/Login'; import Signup from './pages/shared/Signup'; import SellerVerification from './pages/shared/SellerVerification'; -// Buyer Pages + + import MapDiscovery from './pages/buyer/MapDiscovery'; import Discover from './pages/buyer/Discover'; import SavedShops from './pages/buyer/SavedShops'; @@ -25,105 +27,107 @@ import Cart from './pages/buyer/Cart'; import Checkout from './pages/buyer/Checkout'; import Orders from './pages/buyer/Orders'; import BuyerProfile from './pages/buyer/BuyerProfile'; +import PaymentSuccess from './pages/buyer/PaymentSuccess'; + -// Seller Pages import Dashboard from './pages/seller/dashboard'; import Catalog from './pages/seller/catalog'; import Inventory from './pages/seller/inventory'; import SellerOrders from './pages/seller/orders'; import Earnings from './pages/seller/earnings'; -// Admin Pages -import Overview from './pages/admin/overview'; -import Verify from './pages/admin/verify'; -import Moderate from './pages/admin/moderate'; -import Users from './pages/admin/users'; -import Logs from './pages/admin/logs'; -import Disputes from './pages/admin/disputes'; -// Admin Pages import Overview from './pages/admin/overview'; import Verify from './pages/admin/verify'; import Moderate from './pages/admin/moderate'; import Users from './pages/admin/users'; import Logs from './pages/admin/logs'; -import Disputes from './pages/admin/disputes'; +import Withdrawals from './pages/admin/withdrawals'; const App = () => ( - - - } />} > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - } />}> - } /> - - - } />}> - } /> - - - - - - - } /> - } /> - } /> - - {/* Admin Routes */} - } />} - > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Seller Routes */} - } />} - > - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Buyer Routes */} - } />} - > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + + + + + + + }> + } /> + } /> + + + } /> + + {/* Admin Routes */} + }> + } />} + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + }> + } />} + > + } /> + } /> + } /> + } /> + } /> + + } /> + + + + {/* Public & Private Buyer Routes */} + } />} + > + {/* Public Buyer Routes */} + } /> + } /> + } /> + } /> + } /> + + {/* Authenticated Buyer Only Routes */} + }> + } /> + } /> + } /> + } /> + + } /> + + + } /> + + + + + ); export default App; \ No newline at end of file diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index bbf773b..8a7ca1f 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,17 +1,14 @@ import { Link } from 'react-router-dom'; import { Menu } from 'lucide-react'; -/** - * Header — visible only on mobile (md:hidden). - * On desktop the sidebar branding acts as the top-left identity. - */ + const Header = ({ toggleSidebar }) => { return ( -
- {/* Hamburger */} +
+ - -
- - 1-4 of 24 - - - - - {/* Table Content */} -
- - - - - - - - - - - - {data.map((row, i) => ( - onRowClick(row.id)} - className={`border-b border-neutral-dark/5 hover:bg-neutral-dark/5 transition-colors cursor-pointer group/row ${i === data.length - 1 ? 'border-b-0' : ''}`} - > - - - - - - - ))} - -
Dispute IDArtisan / ShopCustomerReasonAmount
- - {row.id} - - -
- {row.shopName} -
- - {row.shopName} - -
- - {row.customerName} - - - - - - {row.amount} - -
-
- - {/* Footer / Pagination */} -
-
- Showing 1-4 of 24 open disputes -
-
- -
-
- - ); -}; - -export default ActiveDisputeQueue; diff --git a/frontend/src/components/admin/AdminActionModal.jsx b/frontend/src/components/admin/AdminActionModal.jsx deleted file mode 100644 index 90f832a..0000000 --- a/frontend/src/components/admin/AdminActionModal.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useEffect } from 'react'; -import { X, Info } from 'lucide-react'; - -const AdminActionModal = ({ isOpen, onClose, title, message }) => { - useEffect(() => { - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - return () => { - document.body.style.overflow = 'unset'; - }; - }, [isOpen]); - - if (!isOpen) return null; - - return ( -
- {/* Backdrop */} -
- - {/* Modal Dialog */} -
- {/* Header */} -
-

- - {title} -

- -
- - {/* Content */} -
-

- {message || "This feature is currently under development for the Admin Portal. Check back later!"} -

-
- - {/* Footer */} -
- -
-
-
- ); -}; - -export default AdminActionModal; diff --git a/frontend/src/components/admin/AdminHeader.jsx b/frontend/src/components/admin/AdminHeader.jsx deleted file mode 100644 index 6185d77..0000000 --- a/frontend/src/components/admin/AdminHeader.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { Search, Bell, HelpCircle } from 'lucide-react'; - -const AdminHeader = ({ - searchPlaceholder = "Search for shops, orders, or reports...", - primaryActionText = "Export Report", - onPrimaryActionClick, - secondaryActionText, - secondaryActionIcon: SecondaryActionIcon, - onSecondaryActionClick -}) => { - return ( -
- {/* Search Bar */} -
- - -
- - {/* Right Controls */} -
-
- {secondaryActionText && ( - - )} - -
- - -
-
- ); -}; - -export default AdminHeader; diff --git a/frontend/src/components/admin/AdminStatCard.jsx b/frontend/src/components/admin/AdminStatCard.jsx deleted file mode 100644 index 1ff34ad..0000000 --- a/frontend/src/components/admin/AdminStatCard.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -const AdminStatCard = ({ title, value, subtext, subtextColor = 'text-neutral-dark/60', icon: Icon, iconBgClass = 'bg-[#F5EDE8]', iconColorClass = 'text-primary', accentClass = '' }) => { - return ( -
-
-

- {title} -

- {Icon && ( -
- -
- )} -
- -
-
- {value} -
-
- {subtext} -
-
-
- ); -}; -export default AdminStatCard; \ No newline at end of file diff --git a/frontend/src/components/admin/ApplicationsTable.jsx b/frontend/src/components/admin/ApplicationsTable.jsx deleted file mode 100644 index bbac8a5..0000000 --- a/frontend/src/components/admin/ApplicationsTable.jsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { Filter, Download, ChevronRight } from 'lucide-react'; -import Pagination from './Pagination'; - -const StatusBadge = ({ status }) => { - const isFlagged = status === 'FLAGGED'; - return ( - - {status} - - ); -}; - -const ScoreBar = ({ score }) => { - const isLow = score < 50; - const colorClass = isLow ? 'bg-destructive' : 'bg-[#8C5233]'; - - return ( -
-
- {score} -
-
-
-
-
- ); -}; - -const ApplicationsTable = ({ data, onFilterClick, onDownloadClick, onRowClick }) => { - return ( -
- {/* Header */} -
-

Applications Overview

-
- - -
-
- - {/* Table Content */} -
- - - - - - - - - - - - - {data.map((row, i) => ( - onRowClick(row.name)} - className={`border-b border-neutral-dark/5 hover:bg-neutral-dark/5 transition-colors group/row cursor-pointer ${i === data.length - 1 ? 'border-b-0' : ''}`} - > - - - - - - - - ))} - -
Shop NameCategoryApplied OnScoreStatus
-
- -
-
-
- {row.name} -
-
- {row.id} -
-
-
- {row.category} - - {row.appliedOn} - - - - - - -
-
- - {/* Footer / Pagination */} -
-
- Showing 4 of 42 pending shops -
-
- -
-
-
- ); -}; - -export default ApplicationsTable; diff --git a/frontend/src/components/admin/AuditSummaryCard.jsx b/frontend/src/components/admin/AuditSummaryCard.jsx deleted file mode 100644 index e320b85..0000000 --- a/frontend/src/components/admin/AuditSummaryCard.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -const AuditSummaryCard = ({ onReviewClick, onDownloadClick }) => { - return ( -
- {/* Background Graphic Suggestion */} -
- -
-

- Automated Audit Summary -

-

- The platform AI has analyzed the last 500 transactions. No systemic anomalies detected, but 2 shops are flagged for manual review based on volume spikes. -

-
- -
- - -
-
- ); -}; - -export default AuditSummaryCard; diff --git a/frontend/src/components/admin/BulkActionOverlay.jsx b/frontend/src/components/admin/BulkActionOverlay.jsx deleted file mode 100644 index 8bb2a4e..0000000 --- a/frontend/src/components/admin/BulkActionOverlay.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Check, XCircle, CornerRightUp } from 'lucide-react'; - -const BulkActionOverlay = ({ selectedCount = 4, onActionClick }) => { - if (selectedCount === 0) return null; - - return ( -
-
- - {/* Selection Count */} -
-
- {selectedCount} -
- - SELECTED - -
- - {/* Divider */} -
- - {/* Quick Actions */} -
- - - -
- - {/* Primary Action */} - -
-
- ); -}; - -export default BulkActionOverlay; diff --git a/frontend/src/components/admin/CategoryPerformanceTable.jsx b/frontend/src/components/admin/CategoryPerformanceTable.jsx deleted file mode 100644 index 1e639fe..0000000 --- a/frontend/src/components/admin/CategoryPerformanceTable.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { Filter, ChevronDown, ChevronRight } from 'lucide-react'; - -const StatusBadge = ({ status }) => { - const isTrending = status === 'TRENDING'; - return ( - - {status} - - ); -}; - -const CategoryPerformanceTable = ({ data, onViewDetailedAnalyticsClick, onAllRegionsClick }) => { - return ( -
- {/* Header */} -
-

Shop Performance by Category

-
- - -
-
- - {/* Table Content */} -
- - - - - - - - - - - - - {data.map((row, i) => ( - - - - - - - - - ))} - -
CategoryActive ShopsAvg. SalesGrowthStatus
-
- -
- - {row.category.name} - -
- {row.activeShops} - - {row.avgSales} - - - {row.growth} - - - - - -
-
- - {/* Footer Link */} -
- - View Detailed Analytics - -
-
- ); -}; - -export default CategoryPerformanceTable; diff --git a/frontend/src/components/admin/DisputeReasonsCard.jsx b/frontend/src/components/admin/DisputeReasonsCard.jsx deleted file mode 100644 index 18e5473..0000000 --- a/frontend/src/components/admin/DisputeReasonsCard.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -const ReasonBar = ({ label, percentage }) => { - return ( -
-
- - {label} - - - {percentage}% - -
-
-
-
-
- ); -}; - -const DisputeReasonsCard = ({ data, trend }) => { - return ( -
-

- Dispute Reasons -

- -
- {data.map((item, i) => ( - - ))} -
- -
-

- Trend Analysis -

-

- {trend} -

-
-
- ); -}; - -export default DisputeReasonsCard; diff --git a/frontend/src/components/admin/ModerationTable.jsx b/frontend/src/components/admin/ModerationTable.jsx deleted file mode 100644 index 06a9d54..0000000 --- a/frontend/src/components/admin/ModerationTable.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import React from 'react'; -import { ChevronDown, Download } from 'lucide-react'; -import Pagination from './Pagination'; - -const ViolationBadge = ({ type }) => { - let styleClass = "bg-neutral-dark/10 text-neutral-dark/60"; // Default - if (type === 'COPYRIGHT') { - styleClass = "bg-[#F8E2DF] text-destructive"; // Pink/Red - } else if (type === 'QUALITY') { - styleClass = "bg-[#FDF3E1] text-[#B7791F]"; // Yellow/Orange - } else if (type === 'PROHIBITED') { - styleClass = "bg-[#E2E8F0] text-[#4A5568]"; // Gray - } - - return ( - - {type} - - ); -}; - -const SeverityBar = ({ score }) => { - let colorClass = "bg-[#CBD5E1] text-[#64748B]"; // Default gray (<30) - if (score >= 80) { - colorClass = "bg-destructive text-destructive"; // High (red) - } else if (score >= 40) { - colorClass = "bg-[#ED8936] text-[#DD6B20]"; // Medium (orange) - } - - return ( -
-
- {score} -
-
-
-
-
- ); -}; - -const StatusDisplay = ({ status }) => { - if (status === 'ESCALATED') { - return ( -
-
- - ESCALATED - -
- ); - } - return ( - - {status} - - ); -}; - -const ModerationTable = ({ data, onFilterClick, onDownloadClick, onReviewClick }) => { - return ( -
- {/* Header */} -
-

Moderation Queue

-
- - -
- -
-
- - {/* Table Content */} -
{/* Extra padding bottom for the floating bar */} - - - - - - - - - - - - - {data.map((row, i) => ( - - - - - - - - - ))} - -
Product & IDShop NameViolationFlagged OnSeverityStatus
-
- {row.productName} -
-
-
- {row.productName} -
-
- {row.id} -
-
-
- - {row.shopName} - - - - -
- {row.flaggedDate} -
-
- {row.flaggedTime} -
-
- - -
- - -
-
-
- - {/* Footer / Pagination */} -
-
- Showing 1 to 10 of 124 tickets -
-
- -
-
-
- ); -}; - -export default ModerationTable; diff --git a/frontend/src/components/admin/Pagination.jsx b/frontend/src/components/admin/Pagination.jsx deleted file mode 100644 index a4ed84a..0000000 --- a/frontend/src/components/admin/Pagination.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; - -const Pagination = () => { - return ( -
- {/* Previous Button */} - - - {/* Page Numbers */} - - - - - {/* Ellipsis */} - - ... - - - - - {/* Next Button */} - -
- ); -}; - -export default Pagination; diff --git a/frontend/src/components/admin/RecentActivityCard.jsx b/frontend/src/components/admin/RecentActivityCard.jsx deleted file mode 100644 index 48fa3fa..0000000 --- a/frontend/src/components/admin/RecentActivityCard.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { CheckCircle, MessageSquare } from 'lucide-react'; - -const ActivityItem = ({ type, text, timeAgo }) => { - const Icon = type === 'resolution' ? CheckCircle : MessageSquare; - const iconColor = type === 'resolution' ? 'text-[#8C5233]' : 'text-destructive'; - const iconBg = type === 'resolution' ? 'bg-[#8C5233]/10' : 'bg-destructive/10'; - - return ( -
-
- -
-
-

-

- {timeAgo} -

-
-
- ); -}; - -const RecentActivityCard = ({ activities, onViewAllClick }) => { - return ( -
-

- Recent Activity -

- -
- {activities.map((activity, i) => ( - - ))} -
- -
- -
-
- ); -}; - -export default RecentActivityCard; diff --git a/frontend/src/components/admin/SystemAlerts.jsx b/frontend/src/components/admin/SystemAlerts.jsx deleted file mode 100644 index e08955f..0000000 --- a/frontend/src/components/admin/SystemAlerts.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { MoreVertical, ArrowRight } from 'lucide-react'; - -const SystemAlerts = ({ alerts, onViewAllClick }) => { - return ( -
-
-

- SYSTEM ALERTS -

- -
- -
- {alerts.map((alert, index) => ( -
- {/* Indicator Dot */} -
-
-
- {/* Content */} -
-

- {alert.title} -

-

- {alert.message} -

-
- {alert.timeAgo} -
-
-
- ))} -
- - {/* Footer Link */} -
- -
-
- ); -}; - -export default SystemAlerts; diff --git a/frontend/src/components/admin/TransactionLogsTable.jsx b/frontend/src/components/admin/TransactionLogsTable.jsx deleted file mode 100644 index d59395c..0000000 --- a/frontend/src/components/admin/TransactionLogsTable.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from 'react'; -import { ChevronDown, Eye } from 'lucide-react'; -import Pagination from './Pagination'; - -const TypeBadge = ({ type }) => { - return ( - - {type} - - ); -}; - -const AmountDisplay = ({ amount, type }) => { - let colorClass = "text-neutral-dark"; - if (type === 'SALE') colorClass = "text-[#16A34A]"; // Green - if (type === 'REFUND') colorClass = "text-destructive"; // Red - - return ( - - {amount} - - ); -}; - -const StatusDisplay = ({ status }) => { - let colorClass = "bg-[#16A34A] text-[#16A34A]"; // COMPLETED (Green) - if (status === 'PENDING') colorClass = "bg-[#D97706] text-[#D97706]"; - if (status === 'DISPUTED') colorClass = "bg-destructive text-destructive"; - - return ( -
-
- - {status} - -
- ); -}; - -const TransactionLogsTable = ({ data, onFilterClick, onActionClick }) => { - return ( -
- {/* Header */} -
-

Recent Financial Activities

-
- - -
- - 1-5 of 1,284 - -
-
- - {/* Table Content */} -
- - - - - - - - - - - - - - {data.map((row, i) => ( - - - - - - - - - - ))} - -
DateTransaction IDShop NameTypeAmountStatusActions
-
- {row.date.split(',')[0]}, -
-
- {row.date.split(',')[1]} -
-
- - {row.id} - - - - {row.shopName} - - - - - - - - -
- -
-
-
- - {/* Footer / Pagination */} -
-
- Showing 1 to 5 of 1,284 transactions -
-
- -
-
-
- ); -}; - -export default TransactionLogsTable; diff --git a/frontend/src/components/admin/UserDirectoryTable.jsx b/frontend/src/components/admin/UserDirectoryTable.jsx deleted file mode 100644 index 073265b..0000000 --- a/frontend/src/components/admin/UserDirectoryTable.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react'; -import { ChevronDown, Pencil, Store, Ban, CheckCircle, RefreshCw } from 'lucide-react'; -import Pagination from './Pagination'; - -const RoleBadge = ({ role }) => { - return ( - - {role} - - ); -}; - -const StatusDisplay = ({ status }) => { - let colorClass = "bg-primary text-primary"; - if (status === 'PENDING') colorClass = "bg-[#D97706] text-[#D97706]"; - if (status === 'SUSPENDED') colorClass = "bg-destructive text-destructive"; - - return ( -
-
- - {status} - -
- ); -}; - -const UserDirectoryTable = ({ data, onFilterClick, onActionClick }) => { - return ( -
- {/* Header */} -
-

Directory

-
- - -
- - 1-10 of 24,892 - -
-
- - {/* Table Content */} -
- - - - - - - - - - - - {data.map((row, i) => ( - - - - - - - - ))} - -
Name / ShopRoleStatusJoin DateActions
-
- {row.name} -
-
-
- {row.name} -
-
- {row.subtext} -
-
-
- - - - - {row.joinDate} - -
- {/* Common Edit Action */} - - - {/* Role-specific Action */} - {row.role === 'SELLER' ? ( - - ) : row.status === 'PENDING' ? ( - - ) : ( -
/* Spacer if no action */ - )} - - {/* Status-specific Action (Suspend / Restore) */} - {row.status === 'SUSPENDED' ? ( - - ) : ( - - )} -
-
-
- - {/* Footer / Pagination */} -
-
- Showing 1 to 10 of 24,892 users -
-
- -
-
-
- ); -}; - -export default UserDirectoryTable; diff --git a/frontend/src/components/admin/analytics/AuditSummaryCard.jsx b/frontend/src/components/admin/analytics/AuditSummaryCard.jsx new file mode 100644 index 0000000..4321290 --- /dev/null +++ b/frontend/src/components/admin/analytics/AuditSummaryCard.jsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const AuditSummaryCard = ({ onReviewClick }) => { + return ( +
+ {/* Subtle ambient */} +
+ +
+

+ Automated Audit Summary +

+

+ The platform AI has analyzed the last 500 transactions. No systemic anomalies detected, but 2 shops are flagged for manual review based on volume spikes. +

+
+ +
+ +
+
+ ); +}; + +export default AuditSummaryCard; diff --git a/frontend/src/components/admin/analytics/CategoryPerformanceTable.jsx b/frontend/src/components/admin/analytics/CategoryPerformanceTable.jsx new file mode 100644 index 0000000..0d426c5 --- /dev/null +++ b/frontend/src/components/admin/analytics/CategoryPerformanceTable.jsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { Store } from 'lucide-react'; +import DataTable from '../../ui/DataTable'; + +const CategoryPerformanceTable = ({ data }) => { + const columns = useMemo(() => [ + { + header: 'Category', + accessorKey: 'category', + cell: ({ row }) => ( +
+
+ +
+ + {row.original.category?.name || row.original.category || 'Uncategorized'} + +
+ ) + }, + { + header: 'Total Shops', + accessorKey: 'activeShops', + cell: ({ row }) => ( + + {row.original.activeShops || 0} + + ) + }, + { + header: 'Active Products', + accessorKey: 'activeProducts', + cell: ({ row }) => ( + + {row.original.activeProducts || '—'} + + ) + }, + { + header: 'Total Sales', + accessorKey: 'totalSales', + cell: ({ row }) => ( + + {row.original.totalSales || '—'} + + ) + }, + { + header: 'Avg Rating', + accessorKey: 'avgRating', + cell: ({ row }) => ( + + {row.original.avgRating ? `${row.original.avgRating} ★` : '—'} + + ) + }, + { + header: 'Trend', + accessorKey: 'trend', + cell: ({ row }) => ( + 0 ? 'text-green-600' : row.original.trend < 0 ? 'text-destructive' : 'text-muted-foreground'}`}> + {row.original.trend ? (row.original.trend > 0 ? `+${row.original.trend}%` : `${row.original.trend}%`) : '—'} + + ) + } + ], []); + + return ( +
+ +
+ ); +}; + +export default CategoryPerformanceTable; diff --git a/frontend/src/components/admin/PlatformGrowthChart.jsx b/frontend/src/components/admin/analytics/PlatformGrowthChart.jsx similarity index 50% rename from frontend/src/components/admin/PlatformGrowthChart.jsx rename to frontend/src/components/admin/analytics/PlatformGrowthChart.jsx index 1f5930c..ff17bba 100644 --- a/frontend/src/components/admin/PlatformGrowthChart.jsx +++ b/frontend/src/components/admin/analytics/PlatformGrowthChart.jsx @@ -1,34 +1,33 @@ import React from 'react'; +import { Button } from '../../ui/button'; const PlatformGrowthChart = ({ data }) => { - // data is expected to be an array of objects: { label: 'OCT 01', value: 40, isHighlighted: false, tooltip: null } const maxVal = Math.max(...data.map(d => d.value), 1); return ( -
+
-

- PLATFORM GROWTH +

+ Platform Growth

-

- Merchant acquisition vs Transaction volume +

+ Active users vs transaction volume

-
- - + - + +
- {/* Bar Chart Area */}
{data.map((item, i) => { @@ -36,23 +35,21 @@ const PlatformGrowthChart = ({ data }) => { return (
- {/* Tooltip for highlighted bar */} {item.isHighlighted && item.tooltip && ( -
-
- PEAK PERFORMANCE +
+
+ Peak Traffic
-
+
{item.tooltip.value}
-
+
{item.tooltip.date}
- {/* Arrow down */} -
+
)}
@@ -60,8 +57,7 @@ const PlatformGrowthChart = ({ data }) => { })}
- {/* X-Axis Labels */} -
+
{data.map((item, i) => ( {item.showLabel ? item.label : ''} diff --git a/frontend/src/components/admin/common/AdminActionModal.jsx b/frontend/src/components/admin/common/AdminActionModal.jsx new file mode 100644 index 0000000..f81a27b --- /dev/null +++ b/frontend/src/components/admin/common/AdminActionModal.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Info } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; + +const AdminActionModal = ({ isOpen, onClose, title, message }) => { + return ( + + + + + {/* Header */} + + + + {title} + + + + {/* Content */} +
+

+ {message || 'This feature is currently under development for the Admin Portal. Check back later!'} +

+
+ + {/* Footer */} + + + +
+
+ ); +}; + +export default AdminActionModal; + diff --git a/frontend/src/components/admin/common/BulkActionOverlay.jsx b/frontend/src/components/admin/common/BulkActionOverlay.jsx new file mode 100644 index 0000000..eae088c --- /dev/null +++ b/frontend/src/components/admin/common/BulkActionOverlay.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Check, XCircle, CornerRightUp } from 'lucide-react'; + +const BulkActionOverlay = ({ selectedCount = 4, onActionClick }) => { + if (selectedCount === 0) return null; + + return ( +
+
+ {/* Count Badge */} +
+
+ {selectedCount} +
+ + Selected + +
+ +
+ + {/* Quick Actions */} +
+ + + +
+ + {/* Primary Action */} + +
+
+ ); +}; + +export default BulkActionOverlay; diff --git a/frontend/src/components/admin/common/Pagination.jsx b/frontend/src/components/admin/common/Pagination.jsx new file mode 100644 index 0000000..cd8f6b9 --- /dev/null +++ b/frontend/src/components/admin/common/Pagination.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +const Pagination = ({ currentPage = 1, totalPages = 1, onPageChange }) => { + if (totalPages <= 1) return null; + + const handlePrev = () => { + if (currentPage > 1 && onPageChange) onPageChange(currentPage - 1); + }; + + const handleNext = () => { + if (currentPage < totalPages && onPageChange) onPageChange(currentPage + 1); + }; + + const getPages = () => { + const pages = []; + if (totalPages <= 5) { + for (let i = 1; i <= totalPages; i++) pages.push(i); + } else { + if (currentPage <= 3) { + pages.push(1, 2, 3, '...', totalPages); + } else if (currentPage >= totalPages - 2) { + pages.push(1, '...', totalPages - 2, totalPages - 1, totalPages); + } else { + pages.push(1, '...', currentPage, '...', totalPages); + } + } + return pages; + }; + + return ( +
+ + + {getPages().map((page, index) => { + if (page === '...') { + return ( + + ... + + ); + } + + return ( + + ); + })} + + +
+ ); +}; + +export default Pagination; diff --git a/frontend/src/components/admin/common/StatusBadge.jsx b/frontend/src/components/admin/common/StatusBadge.jsx new file mode 100644 index 0000000..b19f053 --- /dev/null +++ b/frontend/src/components/admin/common/StatusBadge.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +// ── Shared status badge utility ─────────────────────────────────────────────── +export const statusVariants = { + VERIFIED: 'bg-green-50 text-green-700 border border-green-200', + ACTIVE: 'bg-green-50 text-green-700 border border-green-200', + DELIVERED: 'bg-green-50 text-green-700 border border-green-200', + COMPLETED: 'bg-green-50 text-green-700 border border-green-200', + PENDING: 'bg-amber-50 text-amber-700 border border-amber-200', + 'UNDER REVIEW': 'bg-amber-50 text-amber-700 border border-amber-200', + CONFIRMED: 'bg-blue-50 text-blue-700 border border-blue-200', + ACCEPTED: 'bg-blue-50 text-blue-700 border border-blue-200', + APPROVED: 'bg-blue-50 text-blue-700 border border-blue-200', + SUSPENDED: 'bg-destructive/10 text-destructive border border-destructive/20', + REJECTED: 'bg-destructive/10 text-destructive border border-destructive/20', + FLAGGED: 'bg-destructive/10 text-destructive border border-destructive/20', + HIDDEN: 'bg-destructive/10 text-destructive border border-destructive/20', + CANCELLED: 'bg-destructive/10 text-destructive border border-destructive/20', +}; + +export const StatusBadge = ({ status }) => { + const cls = statusVariants[status] || 'bg-muted text-muted-foreground border border-border'; + return ( + + + {status} + + ); +}; + +export default StatusBadge; diff --git a/frontend/src/components/admin/dashboard/AdminStatCard.jsx b/frontend/src/components/admin/dashboard/AdminStatCard.jsx new file mode 100644 index 0000000..33b9212 --- /dev/null +++ b/frontend/src/components/admin/dashboard/AdminStatCard.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +const AdminStatCard = ({ title, value, subtext, icon: Icon }) => { + return ( +
+
+

+ {title} +

+ {Icon && ( +
+ +
+ )} +
+ +
+
+ {value} +
+
+ {subtext} +
+
+
+ ); +}; +export default AdminStatCard; \ No newline at end of file diff --git a/frontend/src/components/admin/dashboard/RecentActivityCard.jsx b/frontend/src/components/admin/dashboard/RecentActivityCard.jsx new file mode 100644 index 0000000..81849eb --- /dev/null +++ b/frontend/src/components/admin/dashboard/RecentActivityCard.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { CheckCircle, MessageSquare } from 'lucide-react'; + +const ActivityItem = ({ type, text, timeAgo }) => { + const Icon = type === 'resolution' ? CheckCircle : MessageSquare; + const iconColor = type === 'resolution' ? 'text-primary' : 'text-destructive'; + const iconBg = type === 'resolution' ? 'bg-primary/10' : 'bg-destructive/10'; + + return ( +
+
+ +
+
+

+

+ {timeAgo} +

+
+
+ ); +}; + +const RecentActivityCard = ({ activities, onViewAllClick }) => { + return ( +
+

+ Recent Activity +

+ +
+ {activities.map((activity, i) => ( + + ))} +
+ +
+ +
+
+ ); +}; + +export default RecentActivityCard; diff --git a/frontend/src/components/admin/dashboard/SystemAlerts.jsx b/frontend/src/components/admin/dashboard/SystemAlerts.jsx new file mode 100644 index 0000000..66f215d --- /dev/null +++ b/frontend/src/components/admin/dashboard/SystemAlerts.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const SystemAlerts = ({ alerts }) => { + return ( +
+

+ Pending Verifications +

+ +
+ {alerts.map((alert, index) => ( +
+
+
+
+
+

+ {alert.title} +

+

+ {alert.message} +

+
+ {alert.timeAgo} +
+
+
+ ))} +
+
+ ); +}; + +export default SystemAlerts; diff --git a/frontend/src/components/admin/PayoutHealthCard.jsx b/frontend/src/components/admin/finance/PayoutHealthCard.jsx similarity index 56% rename from frontend/src/components/admin/PayoutHealthCard.jsx rename to frontend/src/components/admin/finance/PayoutHealthCard.jsx index b923174..991a6ac 100644 --- a/frontend/src/components/admin/PayoutHealthCard.jsx +++ b/frontend/src/components/admin/finance/PayoutHealthCard.jsx @@ -2,21 +2,22 @@ import React from 'react'; const HealthBar = ({ label, percentage }) => { const isHealthy = percentage >= 95; - const colorClass = isHealthy ? "bg-[#16A34A] text-[#16A34A]" : "bg-destructive text-destructive"; + const barColor = isHealthy ? 'bg-green-600' : 'bg-destructive'; + const textColor = isHealthy ? 'text-green-600' : 'text-destructive'; return (
- + {label} - + {percentage}%
-
-
+
@@ -26,19 +27,19 @@ const HealthBar = ({ label, percentage }) => { const PayoutHealthCard = ({ data, lastUpdated }) => { return ( -
-

+
+

Payout Method Health

- +
{data.map((item, i) => ( ))}
-
- +
+ Last updated: {lastUpdated}
diff --git a/frontend/src/components/admin/finance/TransactionLogsTable.jsx b/frontend/src/components/admin/finance/TransactionLogsTable.jsx new file mode 100644 index 0000000..2032c6f --- /dev/null +++ b/frontend/src/components/admin/finance/TransactionLogsTable.jsx @@ -0,0 +1,86 @@ +import React, { useState, useMemo } from 'react'; +import { StatusBadge } from '../common/StatusBadge'; +import { Search, Eye } from 'lucide-react'; +import DataTable from '../../ui/DataTable'; + +const TransactionLogsTable = ({ data }) => { + const [statusFilter, setStatusFilter] = useState('All'); + + const filtered = data.filter(row => { + const matchStatus = statusFilter === 'All' || row.status === statusFilter; + return matchStatus; + }); + + const uniqueStatuses = ['All', ...new Set(data.map(r => r.status))]; + + const columns = useMemo(() => [ + { + header: 'Date', + accessorKey: 'date', + cell: ({ row }) => {row.original.date} + }, + { + header: 'Order ID', + accessorKey: 'id', + cell: ({ row }) => #{row.original.id} + }, + { + header: 'Shop', + accessorKey: 'shopName', + cell: ({ row }) => {row.original.shopName} + }, + { + header: 'Amount', + accessorKey: 'amount', + cell: ({ row }) => {row.original.amount} + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => + }, + { + header: 'Actions', + id: 'actions', + meta: { headerClassName: 'text-center', cellClassName: 'flex justify-center' }, + cell: () => ( + + ) + } + ], []); + + return ( +
+ + +
+ } + footer={ +
+ Showing {filtered.length} of{' '} + {data.length} transactions +
+ } + /> +
+ ); +}; + +export default TransactionLogsTable; diff --git a/frontend/src/components/admin/moderation/ModerationTable.jsx b/frontend/src/components/admin/moderation/ModerationTable.jsx new file mode 100644 index 0000000..32c76eb --- /dev/null +++ b/frontend/src/components/admin/moderation/ModerationTable.jsx @@ -0,0 +1,152 @@ +import React, { useMemo } from 'react'; +import { ChevronDown } from 'lucide-react'; +import Pagination from '../common/Pagination'; +import { StatusBadge } from '../common/StatusBadge'; +import DataTable from '../../../components/ui/DataTable'; + +// ── Violation Badge ─────────────────────────────────────────────────────────── +const violationVariants = { + COPYRIGHT: 'bg-destructive/10 text-destructive border border-destructive/20', + QUALITY: 'bg-amber-50 text-amber-700 border border-amber-200', + PROHIBITED: 'bg-muted text-muted-foreground border border-border', +}; + +const ViolationBadge = ({ type }) => { + const cls = violationVariants[type] || 'bg-muted text-muted-foreground border border-border'; + return ( + + {type} + + ); +}; + +// ── Severity Bar ───────────────────────────────────────────────────────────── +const SeverityBar = ({ score }) => { + let barColor = 'bg-muted-foreground/30'; + let textColor = 'text-muted-foreground'; + if (score >= 80) { barColor = 'bg-destructive'; textColor = 'text-destructive'; } + else if (score >= 40) { barColor = 'bg-amber-500'; textColor = 'text-amber-600'; } + + return ( +
+
{score}
+
+
+
+
+ ); +}; + +// ── Main Component ──────────────────────────────────────────────────────────── +const ModerationTable = ({ data, onFilterClick, onReviewClick }) => { + const columns = useMemo(() => [ + { + header: 'Product', + accessorKey: 'productName', + cell: ({ row }) => ( +
+
+ {row.original.productName} +
+
{row.original.productName}
+
+ ), + }, + { + header: 'Product ID', + accessorKey: 'id', + cell: ({ row }) => {row.original.id}, + }, + { + header: 'Shop', + accessorKey: 'shopName', + cell: ({ row }) => {row.original.shopName}, + }, + { + header: 'Violation', + accessorKey: 'violation', + cell: ({ row }) => , + }, + { + header: 'Flagged Date', + accessorKey: 'flaggedDate', + cell: ({ row }) => {row.original.flaggedDate}, + }, + { + header: 'Flagged Time', + accessorKey: 'flaggedTime', + cell: ({ row }) => {row.original.flaggedTime}, + }, + { + header: 'Severity', + accessorKey: 'severity', + meta: { + headerClassName: 'text-center', + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => ( +
+ + +
+ ), + }, + ], [onReviewClick]); + + return ( + + + + + } + footer={ + <> +
+ Showing 1 to{' '} + 10 of{' '} + 124 tickets +
+ {}} + /> + + } + /> + ); +}; + +export default ModerationTable; diff --git a/frontend/src/components/admin/shops/ApplicationsTable.jsx b/frontend/src/components/admin/shops/ApplicationsTable.jsx new file mode 100644 index 0000000..93a9bd4 --- /dev/null +++ b/frontend/src/components/admin/shops/ApplicationsTable.jsx @@ -0,0 +1,70 @@ +import React, { useMemo } from 'react'; +import { Eye } from 'lucide-react'; +import StatusBadge from '../../../components/ui/StatusBadge'; +import DataTable from '../../../components/ui/DataTable'; + +const ApplicationsTable = ({ data, onRowClick }) => { + const columns = useMemo(() => [ + { + header: 'Shop Name', + accessorKey: 'name', + cell: ({ row }) => ( + + {row.original.name} + + ) + }, + { + header: 'Owner Name', + accessorKey: 'ownerName', + cell: ({ row }) => ( + + {row.original.owner?.name || row.original.ownerName || '—'} + + ) + }, + { + header: 'Category', + accessorKey: 'category', + cell: ({ row }) => {row.original.category} + }, + { + header: 'Registered On', + accessorKey: 'appliedOn', + cell: ({ row }) => {row.original.appliedOn} + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => + }, + { + header: 'Actions', + id: 'actions', + meta: { headerClassName: 'text-center', cellClassName: 'flex justify-center' }, + cell: () => ( + + ) + } + ], []); + + return ( + + Showing {data.length} shops +
+ } + /> + ); +}; + +export default ApplicationsTable; diff --git a/frontend/src/components/admin/BatchVerifyFAB.jsx b/frontend/src/components/admin/shops/BatchVerifyFAB.jsx similarity index 56% rename from frontend/src/components/admin/BatchVerifyFAB.jsx rename to frontend/src/components/admin/shops/BatchVerifyFAB.jsx index b1776c1..d98f30a 100644 --- a/frontend/src/components/admin/BatchVerifyFAB.jsx +++ b/frontend/src/components/admin/shops/BatchVerifyFAB.jsx @@ -3,12 +3,12 @@ import { BadgeCheck } from 'lucide-react'; const BatchVerifyFAB = ({ count = 5, onClick }) => { return ( - diff --git a/frontend/src/components/admin/users/UserDirectoryTable.jsx b/frontend/src/components/admin/users/UserDirectoryTable.jsx new file mode 100644 index 0000000..3ccd183 --- /dev/null +++ b/frontend/src/components/admin/users/UserDirectoryTable.jsx @@ -0,0 +1,229 @@ +import React, { useState, useMemo } from 'react'; +import { Store, Ban, CheckCircle, RefreshCw, Eye, X, Trash2, Search } from 'lucide-react'; +import StatusBadge from '../../../components/ui/StatusBadge'; +import DataTable from '../../../components/ui/DataTable'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../components/ui/Tabs'; + +// ── Role Badge ──────────────────────────────────────────────────────────────── +const RoleBadge = ({ role }) => ( + + {role} + +); + +// ── User Detail Modal ───────────────────────────────────────────────────────── +const UserDetailModal = ({ user, shop, onClose, onActionClick, onDeleteClick }) => { + if (!user) return null; + return ( +
+
+
+ {/* Header */} +
+
+ User Details +

{user.name}

+
+ +
+ + {/* Body */} +
+
+
+

{user.name}

+

{user.subtext}

+
+ + +
+
+
+ +
+
+ Joined + {user.joinDate} +
+
+ Account Status + + {user.status} + +
+ {shop && ( + <> +
+ Store Name + {shop.name} +
+
+ Store Status + + {shop.isVerified ? 'Verified' : 'Pending'} + +
+ + )} +
+
+ + {/* Footer */} +
+ {user.role === 'SELLER' && ( + + )} + {user.status === 'SUSPENDED' ? ( + + ) : ( + + )} + {user.role !== 'ADMIN' && ( + + )} + +
+
+
+ ); +}; + +// ── Main Table ──────────────────────────────────────────────────────────────── +const UserDirectoryTable = ({ data, shops, onActionClick, onDeleteClick }) => { + const [selectedUser, setSelectedUser] = useState(null); + const [roleFilter, setRoleFilter] = useState('All'); + const [statusFilter, setStatusFilter] = useState('All'); + + const filtered = data.filter(row => { + const roleMatch = roleFilter === 'All' || row.role === roleFilter; + const statusMatch = statusFilter === 'All' || row.status === statusFilter; + return roleMatch && statusMatch; + }); + + const userShop = selectedUser + ? shops?.find(s => s.owner?._id === selectedUser.id || s.owner === selectedUser.id) + : null; + + const columns = useMemo(() => [ + { + header: 'Name', + accessorKey: 'name', + cell: ({ row }) => ( + + {row.original.name} + + ) + }, + { + header: 'Email', + accessorKey: 'email', + cell: ({ row }) => ( + + {row.original.email || row.original.subtext || '—'} + + ) + }, + { + header: 'Role', + accessorKey: 'role', + cell: ({ row }) => + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => + }, + { + header: 'Joined', + accessorKey: 'joinDate', + cell: ({ row }) => {row.original.joinDate} + }, + { + header: 'Actions', + id: 'actions', + meta: { headerClassName: 'text-center', cellClassName: 'flex items-center justify-center gap-3' }, + cell: ({ row }) => ( + + ) + } + ], [onActionClick, onDeleteClick]); + + return ( +
+ + + {['All', 'BUYER', 'SELLER', 'ADMIN'].map(role => ( + setRoleFilter(role)} + > + {role === 'All' ? 'All Users' : role} ({data.filter(r => role === 'All' || r.role === role).length}) + + ))} + + + + +
+ } + footer={ +
+ Showing {filtered.length} of{' '} + {data.length} users +
+ } + /> + + + + {selectedUser && ( + setSelectedUser(null)} + onActionClick={onActionClick} + onDeleteClick={onDeleteClick} + /> + )} +
+ ); +}; + +export default UserDirectoryTable; diff --git a/frontend/src/components/buyer/cart/CartItem.jsx b/frontend/src/components/buyer/cart/CartItem.jsx index 64386d5..c0a9647 100644 --- a/frontend/src/components/buyer/cart/CartItem.jsx +++ b/frontend/src/components/buyer/cart/CartItem.jsx @@ -1,6 +1,5 @@ import { Link } from 'react-router-dom'; import { Trash2 } from 'lucide-react'; -import { mockShops } from '../../../lib/mockData'; import { QuantityControl } from './QuantityControl'; const formatPrice = (centavos) => @@ -8,14 +7,17 @@ const formatPrice = (centavos) => export const CartItem = ({ item, onUpdateQuantity, onRemove, onUpdateOptions }) => { if (!item || !item.product) return null; - const shop = mockShops.find((s) => s._id === item.product.shop); + + const shop = typeof item.product.shop === 'object' ? item.product.shop : null; + const shopId = shop?._id || item.product.shop; return ( -
- {/* Thumbnail */} +
+ + {/* Product image */} {shop?.name || 'Artisan Shop'} {item.product.name} @@ -42,14 +44,14 @@ export const CartItem = ({ item, onUpdateQuantity, onRemove, onUpdateOptions }) {formatPrice(item.product.price)} each - {/* Variant Selectors */} + {/* Variant selectors */} {(item.product.colors?.length > 0 || item.product.sizes?.length > 0) && (
{item.product.colors?.length > 0 && ( @@ -58,17 +60,29 @@ export const CartItem = ({ item, onUpdateQuantity, onRemove, onUpdateOptions }) )}
)} + + {/* Quantity + total row (visible on mobile below details) */} +
+ onUpdateQuantity(item.product._id, item.quantity - 1)} + onIncrement={() => onUpdateQuantity(item.product._id, item.quantity + 1)} + /> + + {formatPrice(item.product.price * item.quantity)} + +
- {/* Quantity */} -
+ {/* Quantity — desktop */} +
onUpdateQuantity(item.product._id, item.quantity - 1)} @@ -76,17 +90,17 @@ export const CartItem = ({ item, onUpdateQuantity, onRemove, onUpdateOptions }) />
- {/* Line Total */} -
+ {/* Line total — desktop */} +
{formatPrice(item.product.price * item.quantity)}
{/* Remove */} -
+
diff --git a/frontend/src/components/buyer/cart/EmptyCart.jsx b/frontend/src/components/buyer/cart/EmptyCart.jsx index 2f74499..950534a 100644 --- a/frontend/src/components/buyer/cart/EmptyCart.jsx +++ b/frontend/src/components/buyer/cart/EmptyCart.jsx @@ -1,23 +1,27 @@ import { Link } from 'react-router-dom'; import { ShoppingBag } from 'lucide-react'; +import { Button } from '@/components/ui/button'; export const EmptyCart = () => ( -
-
-

Shopping Cart

-

Review and manage your selected artisan products.

-
+
+
+

Shopping Cart

+

Review and manage your selected artisan products.

-
- -

Your Cart is Empty

-

- You haven't added any items yet. Discover unique handmade pieces from local craftsmen. -

- - Explore Catalog - +
+
+ +

Your Cart is Empty

+

+ You haven't added any items yet. Discover unique handmade pieces from local craftsmen. +

+ +
); diff --git a/frontend/src/components/buyer/cart/QuantityControl.jsx b/frontend/src/components/buyer/cart/QuantityControl.jsx index beea164..508ad64 100644 --- a/frontend/src/components/buyer/cart/QuantityControl.jsx +++ b/frontend/src/components/buyer/cart/QuantityControl.jsx @@ -1,24 +1,11 @@ -import { Plus, Minus } from 'lucide-react'; +import { QuantityStepper } from '../../ui/quantity-stepper'; -export const QuantityControl = ({ quantity, onDecrement, onIncrement }) => ( -
- -
- {quantity} -
- -
+export const QuantityControl = ({ quantity, onDecrement, onIncrement, max }) => ( + ); diff --git a/frontend/src/components/buyer/discover/DiscoverFilters.jsx b/frontend/src/components/buyer/discover/DiscoverFilters.jsx index c228025..ad6843b 100644 --- a/frontend/src/components/buyer/discover/DiscoverFilters.jsx +++ b/frontend/src/components/buyer/discover/DiscoverFilters.jsx @@ -1,14 +1,13 @@ import { useState, useRef, useEffect } from 'react'; import { Search, SlidersHorizontal, Store, Package, X, ChevronDown, Sparkles, Star, Clock, TrendingUp } from 'lucide-react'; -import { Input } from '@/components/ui/input'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; const SORT_OPTIONS = [ - { value: 'featured', label: 'Featured', icon: Sparkles }, - { value: 'rating_desc',label: 'Top Rated', icon: Star }, - { value: 'newest', label: 'Newest', icon: Clock }, - { value: 'price_asc', label: 'Price: Low → High', icon: TrendingUp }, - { value: 'price_desc', label: 'Price: High → Low', icon: TrendingUp }, + { value: 'featured', label: 'Featured', icon: Sparkles }, + { value: 'rating_desc', label: 'Top Rated', icon: Star }, + { value: 'newest', label: 'Newest', icon: Clock }, + { value: 'price_asc', label: 'Price: Low → High', icon: TrendingUp }, + { value: 'price_desc', label: 'Price: High → Low', icon: TrendingUp }, ]; export const DiscoverFilters = ({ @@ -23,16 +22,20 @@ export const DiscoverFilters = ({ categories, selectedCategory, onCategoryChange, + minPrice, + setMinPrice, + maxPrice, + setMaxPrice, + minRating, + setMinRating, hasActiveFilters, onClearFilters, resultCount, }) => { const [sortOpen, setSortOpen] = useState(false); const dropdownRef = useRef(null); - const currentSort = SORT_OPTIONS.find((o) => o.value === sortBy) ?? SORT_OPTIONS[0]; - // Close dropdown on outside click useEffect(() => { const handler = (e) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { @@ -43,14 +46,10 @@ export const DiscoverFilters = ({ return () => document.removeEventListener('mousedown', handler); }, []); - const handleSortSelect = (value) => { - onSortChange(value); - setSortOpen(false); - }; - return ( - <> - {/* ── Tabs ─────────────────────────────────────────────────── */} +
+ + {/* ── Tab bar ──────────────────────────────────────────────────── */} {[ @@ -77,52 +76,57 @@ export const DiscoverFilters = ({ - {/* ── Search + Sort ─────────────────────────────────────────── */} -
- {/* Search input */} + {/* ── Search + Sort row ─────────────────────────────────────────── */} +
+ + {/* Search */}
- - + +
+ onSearchChange(e.target.value)} - className="pl-9 pr-8 py-2 text-xs" + className="input-search rounded-full" /> {searchQuery && ( )}
+ {/* Sort dropdown */}
{sortOpen && ( -
+
{SORT_OPTIONS.map((opt) => (
- {/* ── Category pills ────────────────────────────────────────── */} -
- {categories.map((cat) => ( - - ))} + {/* ── Category filter chips ─────────────────────────────────────── */} + {categories?.length > 0 && ( +
+ {categories.map((cat) => ( + + ))} - {hasActiveFilters && ( - + {hasActiveFilters && ( + + )} +
+ )} + + {/* ── Advanced Filters: Price & Rating ───────────────────────────── */} +
+ + {/* Price Range (Only for Products) */} + {activeTab === 'products' && ( +
+ Price Range + setMinPrice(e.target.value)} + className="w-20 px-2.5 py-1.5 text-xs rounded-lg border border-border/80 bg-card focus:outline-none focus:border-primary/50 transition-colors" + /> + - + setMaxPrice(e.target.value)} + className="w-20 px-2.5 py-1.5 text-xs rounded-lg border border-border/80 bg-card focus:outline-none focus:border-primary/50 transition-colors" + /> +
)} + + {/* Rating Filter */} +
+ Min Rating +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
- {/* ── Results count ─────────────────────────────────────────── */} -

+ {/* ── Result count ─────────────────────────────────────────────── */} +

{resultCount === 0 ? `No ${activeTab} found` : `${resultCount} ${activeTab} found${hasActiveFilters ? ' · filtered' : ''}`}

- +
); }; diff --git a/frontend/src/components/buyer/discover/DiscoverHero.jsx b/frontend/src/components/buyer/discover/DiscoverHero.jsx index a8238fa..1b9ebbf 100644 --- a/frontend/src/components/buyer/discover/DiscoverHero.jsx +++ b/frontend/src/components/buyer/discover/DiscoverHero.jsx @@ -1,57 +1,59 @@ import { Link } from 'react-router-dom'; -import { ArrowRight, Sparkles } from 'lucide-react'; +import { ArrowRight, Sparkles, Store } from 'lucide-react'; export const DiscoverHero = ({ onExploreShops }) => ( -
- {/* Background Image */} - + + {/* Decorative pattern */} +
- - {/* Gradient Overlay - Fixed direction for left-aligned text readability */} -
- {/* Content Container */} -
- - {/* Badge/Eyebrow */} - + {/* Content */} +
+ {/* Label chip */} + Artisan Marketplace {/* Headline */} -

- Discover Authentic
Filipino Craftsmanship +

+ Discover Authentic
+ Filipino Craftsmanship

{/* Description */} -

+

Connect with talented local artisans and find one-of-a-kind handmade goods that tell a story.

- {/* Call to Actions */} -
+ {/* CTAs */} +
- + View Cart
-
); \ No newline at end of file diff --git a/frontend/src/components/buyer/discover/DiscoverStats.jsx b/frontend/src/components/buyer/discover/DiscoverStats.jsx index 5641b86..038eca7 100644 --- a/frontend/src/components/buyer/discover/DiscoverStats.jsx +++ b/frontend/src/components/buyer/discover/DiscoverStats.jsx @@ -2,25 +2,25 @@ import { MapPin, Package, Heart, ShoppingBag } from 'lucide-react'; export const DiscoverStats = ({ shopCount, productCount, savedCount, ordersCount }) => { const stats = [ - { icon: MapPin, value: `${shopCount}+`, label: 'Artisan Shops', color: 'text-primary' }, - { icon: Package, value: `${productCount}+`, label: 'Handcrafted Items', color: 'text-secondary' }, - { icon: Heart, value: savedCount, label: 'Saved Favorites', color: 'text-destructive' }, - { icon: ShoppingBag, value: ordersCount, label: 'Your Orders', color: 'text-tertiary' }, + { icon: MapPin, value: `${shopCount}+`, label: 'Artisan Shops', bg: 'bg-primary/10', icon_color: 'text-primary' }, + { icon: Package, value: `${productCount}+`, label: 'Handcrafted Items', bg: 'bg-secondary/10', icon_color: 'text-secondary' }, + { icon: Heart, value: savedCount, label: 'Saved Favorites', bg: 'bg-destructive/10', icon_color: 'text-destructive' }, + { icon: ShoppingBag, value: ordersCount, label: 'Your Orders', bg: 'bg-amber-100', icon_color: 'text-amber-700' }, ]; return ( -
- {stats.map(({ icon: Icon, value, label, color }) => ( +
+ {stats.map(({ icon: Icon, value, label, bg, icon_color }) => (
-
- +
+
-
-

{value}

-

{label}

+
+

{value}

+

{label}

))} diff --git a/frontend/src/components/buyer/map/MapSearchBar.jsx b/frontend/src/components/buyer/map/MapSearchBar.jsx index 5e3da21..78a800a 100644 --- a/frontend/src/components/buyer/map/MapSearchBar.jsx +++ b/frontend/src/components/buyer/map/MapSearchBar.jsx @@ -10,7 +10,7 @@ export const MapSearchBar = ({ return (
- {/* Search Input */} +
{ return (
- {/* Header */} +

{title}

{shops.length} shops
- {/* Cards Scroll Container */} +
{shops.map((shop) => ( - {/* Image */} +
{shop.name} - {/* Rating pill */} +
{shop.rating} diff --git a/frontend/src/components/buyer/map/RouteLayer.jsx b/frontend/src/components/buyer/map/RouteLayer.jsx index d60d530..0d9e33f 100644 --- a/frontend/src/components/buyer/map/RouteLayer.jsx +++ b/frontend/src/components/buyer/map/RouteLayer.jsx @@ -23,7 +23,7 @@ export const RouteLayer = ({ geojson }) => { features: [] }; - // Source + if (map.getSource('route')) { map.getSource('route').setData(routeData); } else { @@ -32,7 +32,7 @@ export const RouteLayer = ({ geojson }) => { data: routeData }); - // Glow layer + map.addLayer({ id: 'route-glow', type: 'line', @@ -48,7 +48,7 @@ export const RouteLayer = ({ geojson }) => { } }); - // Main route + map.addLayer({ id: 'route-layer', type: 'line', diff --git a/frontend/src/components/buyer/map/ShopPopupContent.jsx b/frontend/src/components/buyer/map/ShopPopupContent.jsx index 2cad994..33905f5 100644 --- a/frontend/src/components/buyer/map/ShopPopupContent.jsx +++ b/frontend/src/components/buyer/map/ShopPopupContent.jsx @@ -12,7 +12,7 @@ export const ShopPopupContent = ({ return (
- {/* Image Section */} +