@mohasinac/appkit 2.7.34 → 2.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/_internal/server/features/auctions/og.d.ts +1 -0
  2. package/dist/_internal/server/features/auctions/og.js +2 -1
  3. package/dist/_internal/server/features/blog/og.d.ts +1 -0
  4. package/dist/_internal/server/features/blog/og.js +3 -2
  5. package/dist/_internal/server/features/brands/og.d.ts +1 -0
  6. package/dist/_internal/server/features/brands/og.js +2 -1
  7. package/dist/_internal/server/features/bundles/og.d.ts +1 -0
  8. package/dist/_internal/server/features/bundles/og.js +2 -1
  9. package/dist/_internal/server/features/categories/og.d.ts +1 -0
  10. package/dist/_internal/server/features/categories/og.js +2 -1
  11. package/dist/_internal/server/features/checkout/actions.js +13 -5
  12. package/dist/_internal/server/features/classified/og.d.ts +2 -0
  13. package/dist/_internal/server/features/classified/og.js +2 -1
  14. package/dist/_internal/server/features/digital-code/og.d.ts +2 -0
  15. package/dist/_internal/server/features/digital-code/og.js +2 -1
  16. package/dist/_internal/server/features/events/og.d.ts +1 -0
  17. package/dist/_internal/server/features/events/og.js +3 -1
  18. package/dist/_internal/server/features/live/og.d.ts +2 -0
  19. package/dist/_internal/server/features/live/og.js +2 -1
  20. package/dist/_internal/server/features/pre-orders/og.d.ts +1 -0
  21. package/dist/_internal/server/features/pre-orders/og.js +2 -1
  22. package/dist/_internal/server/features/products/og.d.ts +3 -1
  23. package/dist/_internal/server/features/products/og.js +2 -1
  24. package/dist/_internal/server/features/seo/index.d.ts +1 -1
  25. package/dist/_internal/server/features/seo/index.js +1 -1
  26. package/dist/_internal/server/features/seo/og.d.ts +8 -1
  27. package/dist/_internal/server/features/seo/og.js +28 -8
  28. package/dist/_internal/server/features/stores/og.d.ts +1 -0
  29. package/dist/_internal/server/features/stores/og.js +3 -2
  30. package/dist/_internal/server/features/sublisting-categories/og.d.ts +1 -0
  31. package/dist/_internal/server/features/sublisting-categories/og.js +2 -1
  32. package/dist/_internal/server/jobs/core/onScamReportCreate.js +18 -14
  33. package/dist/_internal/server/jobs/core/onScamReportRejected.js +10 -7
  34. package/dist/_internal/server/jobs/core/onScamReportVerified.js +10 -7
  35. package/dist/_internal/shared/actions/action-registry.js +48 -0
  36. package/dist/client.d.ts +6 -0
  37. package/dist/client.js +3 -0
  38. package/dist/constants/api-endpoints.d.ts +9 -0
  39. package/dist/constants/api-endpoints.js +3 -0
  40. package/dist/features/admin/components/AdminBundlesView.js +20 -2
  41. package/dist/features/admin/components/AdminPayoutsView.js +2 -1
  42. package/dist/features/admin/components/AdminProductsView.js +13 -0
  43. package/dist/features/admin/components/AdminStoresView.js +43 -5
  44. package/dist/features/admin/components/AdminUsersView.js +53 -6
  45. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  46. package/dist/features/blog/components/BlogPostForm.js +1 -0
  47. package/dist/features/categories/components/CategoryForm.js +1 -0
  48. package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +4 -1
  49. package/dist/features/homepage/components/FeaturedAuctionsSection.js +2 -2
  50. package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +4 -1
  51. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +2 -2
  52. package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -6
  53. package/dist/features/homepage/components/FeaturedProductsSection.js +3 -47
  54. package/dist/features/homepage/components/SectionCarousel.js +1 -8
  55. package/dist/features/homepage/lib/section-renderer.js +3 -3
  56. package/dist/seed/homepage-sections-seed-data.js +10 -10
  57. package/dist/server.d.ts +1 -1
  58. package/dist/server.js +1 -1
  59. package/dist/tailwind-utilities.css +1 -1
  60. package/dist/ui/components/DateInput.d.ts +27 -0
  61. package/dist/ui/components/DateInput.js +11 -0
  62. package/dist/ui/components/FormField.d.ts +2 -1
  63. package/dist/ui/components/FormField.js +7 -4
  64. package/dist/ui/components/FormField.style.css +13 -0
  65. package/dist/ui/components/HorizontalScroller.js +25 -30
  66. package/dist/ui/components/OtpInput.d.ts +14 -0
  67. package/dist/ui/components/OtpInput.js +55 -0
  68. package/dist/ui/components/OtpInput.style.css +13 -0
  69. package/dist/ui/components/Select.style.css +23 -6
  70. package/dist/ui/components/Textarea.style.css +31 -4
  71. package/dist/ui/components/index.style.css +1 -0
  72. package/dist/ui/index.d.ts +4 -0
  73. package/dist/ui/index.js +2 -0
  74. package/package.json +1 -1
@@ -620,6 +620,54 @@ export const ACTIONS = {
620
620
  description: "Submit the order with the chosen payment method.",
621
621
  kind: "primary",
622
622
  },
623
+ "continue-to-verification": {
624
+ id: "checkout.continue-to-verification",
625
+ label: "Continue to Verification",
626
+ description: "Advance from address selection to identity verification step.",
627
+ kind: "primary",
628
+ },
629
+ "send-otp": {
630
+ id: "checkout.send-otp",
631
+ label: "Send verification code",
632
+ description: "Send a one-time code to the buyer's registered email to verify identity before checkout.",
633
+ kind: "primary",
634
+ },
635
+ "verify-otp": {
636
+ id: "checkout.verify-otp",
637
+ label: "Verify & Continue",
638
+ description: "Submit the one-time code and proceed to payment selection.",
639
+ kind: "primary",
640
+ },
641
+ "resend-otp": {
642
+ id: "checkout.resend-otp",
643
+ label: "Resend code",
644
+ description: "Re-send the verification code to the buyer's registered email.",
645
+ kind: "ghost",
646
+ },
647
+ "pay-online": {
648
+ id: "checkout.pay-online",
649
+ label: "Pay Online (Razorpay)",
650
+ description: "Initiate an online payment via Razorpay UPI/Card/NetBanking.",
651
+ kind: "primary",
652
+ },
653
+ "pay-cod": {
654
+ id: "checkout.pay-cod",
655
+ label: "Cash on Delivery",
656
+ description: "Place the order for cash-on-delivery payment.",
657
+ kind: "secondary",
658
+ },
659
+ "admin-bypass": {
660
+ id: "checkout.admin-bypass",
661
+ label: "Skip Verification — Admin Bypass",
662
+ description: "Admin test mode: skip identity verification and place a test order without payment.",
663
+ kind: "secondary",
664
+ },
665
+ "admin-bypass-payment": {
666
+ id: "checkout.admin-bypass-payment",
667
+ label: "No Payment — Admin Bypass Order",
668
+ description: "Admin test mode: place a real order record without charging any payment.",
669
+ kind: "secondary",
670
+ },
623
671
  },
624
672
  NAV: {
625
673
  "sign-in": {
package/dist/client.d.ts CHANGED
@@ -53,10 +53,16 @@ export { Badge } from "./ui/components/Badge";
53
53
  export { Button } from "./ui/components/Button";
54
54
  export { Checkbox } from "./ui/components/Checkbox";
55
55
  export { Input } from "./ui/components/Input";
56
+ export { OtpInput } from "./ui/components/OtpInput";
57
+ export type { OtpInputProps } from "./ui/components/OtpInput";
58
+ export { DateInput, DateRangeInput } from "./ui/components/DateInput";
59
+ export type { DateInputProps, DateRangeInputProps } from "./ui/components/DateInput";
56
60
  export { Select } from "./ui/components/Select";
57
61
  export type { SelectOption, SelectProps } from "./ui/components/Select";
58
62
  export { Heading } from "./ui/components/Typography";
59
63
  export { Label, Text } from "./ui/components/Typography";
64
+ export { TextLink } from "./ui/components/TextLink";
65
+ export type { TextLinkProps } from "./ui/components/TextLink";
60
66
  export { Textarea } from "./ui/components/Textarea";
61
67
  export { GlobalError } from "./next/components/GlobalError";
62
68
  export { AppLayoutShell, LocaleSwitcher, useDashboardNav, BottomActionsProvider, DashboardNavProvider, LayoutClient, ListingLayout } from "./features/layout/index";
package/dist/client.js CHANGED
@@ -121,9 +121,12 @@ export { Badge } from "./ui/components/Badge";
121
121
  export { Button } from "./ui/components/Button";
122
122
  export { Checkbox } from "./ui/components/Checkbox";
123
123
  export { Input } from "./ui/components/Input";
124
+ export { OtpInput } from "./ui/components/OtpInput";
125
+ export { DateInput, DateRangeInput } from "./ui/components/DateInput";
124
126
  export { Select } from "./ui/components/Select";
125
127
  export { Heading } from "./ui/components/Typography";
126
128
  export { Label, Text } from "./ui/components/Typography";
129
+ export { TextLink } from "./ui/components/TextLink";
127
130
  export { Textarea } from "./ui/components/Textarea";
128
131
  export { GlobalError } from "./next/components/GlobalError";
129
132
  export { AppLayoutShell, LocaleSwitcher, useDashboardNav, BottomActionsProvider, DashboardNavProvider, LayoutClient, ListingLayout } from "./features/layout/index";
@@ -71,6 +71,9 @@ export declare const ADMIN_ENDPOINTS: {
71
71
  readonly BID_BY_ID: (id: string) => string;
72
72
  readonly BLOG: "/api/admin/blog";
73
73
  readonly BLOG_BY_ID: (id: string) => string;
74
+ readonly BUNDLES: "/api/admin/bundles";
75
+ readonly BUNDLE_BY_ID: (id: string) => string;
76
+ readonly BUNDLE_REBUILD: (id: string) => string;
74
77
  readonly CATEGORIES: "/api/admin/categories";
75
78
  readonly CATEGORY_BY_ID: (id: string) => string;
76
79
  readonly BRANDS: "/api/admin/brands";
@@ -371,6 +374,9 @@ export declare const API_ENDPOINTS: {
371
374
  readonly BID_BY_ID: (id: string) => string;
372
375
  readonly BLOG: "/api/admin/blog";
373
376
  readonly BLOG_BY_ID: (id: string) => string;
377
+ readonly BUNDLES: "/api/admin/bundles";
378
+ readonly BUNDLE_BY_ID: (id: string) => string;
379
+ readonly BUNDLE_REBUILD: (id: string) => string;
374
380
  readonly CATEGORIES: "/api/admin/categories";
375
381
  readonly CATEGORY_BY_ID: (id: string) => string;
376
382
  readonly BRANDS: "/api/admin/brands";
@@ -673,6 +679,9 @@ export declare const API_ROUTES: {
673
679
  readonly BID_BY_ID: (id: string) => string;
674
680
  readonly BLOG: "/api/admin/blog";
675
681
  readonly BLOG_BY_ID: (id: string) => string;
682
+ readonly BUNDLES: "/api/admin/bundles";
683
+ readonly BUNDLE_BY_ID: (id: string) => string;
684
+ readonly BUNDLE_REBUILD: (id: string) => string;
676
685
  readonly CATEGORIES: "/api/admin/categories";
677
686
  readonly CATEGORY_BY_ID: (id: string) => string;
678
687
  readonly BRANDS: "/api/admin/brands";
@@ -89,6 +89,9 @@ export const ADMIN_ENDPOINTS = {
89
89
  BID_BY_ID: (id) => `/api/admin/bids/${id}`,
90
90
  BLOG: "/api/admin/blog",
91
91
  BLOG_BY_ID: (id) => `/api/admin/blog/${id}`,
92
+ BUNDLES: "/api/admin/bundles",
93
+ BUNDLE_BY_ID: (id) => `/api/admin/bundles/${id}`,
94
+ BUNDLE_REBUILD: (id) => `/api/admin/bundles/${id}/rebuild`,
92
95
  CATEGORIES: "/api/admin/categories",
93
96
  CATEGORY_BY_ID: (id) => `/api/admin/categories/${id}`,
94
97
  BRANDS: "/api/admin/brands",
@@ -13,8 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  */
14
14
  import { useCallback, useEffect, useState } from "react";
15
15
  import Link from "next/link";
16
- import { Badge, Button, Container, Div, Heading, Row, Section, Stack, Text, } from "../../../ui";
16
+ import { Badge, Button, Container, Div, Heading, Row, Section, Stack, Text, useToast, } from "../../../ui";
17
17
  import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
18
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
18
19
  const STOCK_LIST_LABEL = {
19
20
  in_stock: BUNDLE_COPY.stockBadge.listVariantInStock,
20
21
  out_of_stock: BUNDLE_COPY.stockBadge.listVariantOutOfStock,
@@ -28,6 +29,23 @@ export function AdminBundlesView({ getEditHref, newHref, }) {
28
29
  const [bundles, setBundles] = useState([]);
29
30
  const [loading, setLoading] = useState(true);
30
31
  const [error, setError] = useState(null);
32
+ const [rebuildingId, setRebuildingId] = useState(null);
33
+ const toast = useToast();
34
+ const handleRebuild = useCallback(async (bundleId) => {
35
+ setRebuildingId(bundleId);
36
+ try {
37
+ const res = await fetch(`/api/admin/bundles/${encodeURIComponent(bundleId)}/rebuild`, { method: "POST" });
38
+ if (!res.ok)
39
+ throw new Error("Rebuild failed");
40
+ toast.showToast("Bundle stock rebuilt.", "success");
41
+ }
42
+ catch {
43
+ toast.showToast("Failed to rebuild bundle stock.", "error");
44
+ }
45
+ finally {
46
+ setRebuildingId(null);
47
+ }
48
+ }, [toast]);
31
49
  const load = useCallback(async () => {
32
50
  setLoading(true);
33
51
  setError(null);
@@ -53,6 +71,6 @@ export function AdminBundlesView({ getEditHref, newHref, }) {
53
71
  const memberCount = b.bundleProductIds?.length ?? 0;
54
72
  return (_jsxs("tr", { className: "border-t border-zinc-100 dark:border-zinc-800", children: [_jsx("td", { className: "px-3 py-2", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: b.name }), _jsx(Text, { size: "xs", color: "muted", children: b.slug })] }) }), _jsx("td", { className: "px-3 py-2", children: formatPrice(b.bundlePriceInPaise) }), _jsx("td", { className: "px-3 py-2", children: memberCount }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stockKey], children: STOCK_LIST_LABEL[stockKey] }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: b.isActive ? "success" : "default", children: b.isActive
55
73
  ? BUNDLE_COPY.adminList.activeBadge
56
- : BUNDLE_COPY.adminList.inactiveBadge }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(Link, { href: getEditHref({ id: b.id }), children: BUNDLE_COPY.adminList.editLabel }) }) })] }, b.id));
74
+ : BUNDLE_COPY.adminList.inactiveBadge }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs(Row, { gap: "xs", justify: "end", children: [_jsx(Button, { variant: "ghost", size: "sm", isLoading: rebuildingId === b.id, disabled: rebuildingId === b.id, onClick: () => handleRebuild(b.id), children: ACTIONS.ADMIN["rebuild-bundle"].label }), _jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(Link, { href: getEditHref({ id: b.id }), children: BUNDLE_COPY.adminList.editLabel }) })] }) })] }, b.id));
57
75
  }) })] }) }))] }) }) }));
58
76
  }
@@ -8,6 +8,7 @@ import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
8
  import { BulkActionBar, Button, Form, FormActions, FilterChipGroup, Input, ListingToolbar, ListingViewShell, Modal, Pagination, RowActionMenu, useToast, } from "../../../ui";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { ADMIN_PAYOUT_STATUS_TABS } from "../constants/filter-tabs";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
12
  import { toRecordArray, toRelativeDate, toRupees, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
12
13
  import { DataTable } from "./DataTable";
13
14
  import { apiClient } from "../../../http";
@@ -135,7 +136,7 @@ export function AdminPayoutsView({ children, ...props }) {
135
136
  const pr = row;
136
137
  return (_jsx(RowActionMenu, { actions: [
137
138
  {
138
- label: "Mark paid",
139
+ label: ACTIONS.ADMIN["grant-payout"].label,
139
140
  onClick: () => {
140
141
  setSelectedPayoutId(pr.id);
141
142
  setMarkPaidOpen(true);
@@ -12,6 +12,7 @@ import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } fr
12
12
  import { DataTable } from "./DataTable";
13
13
  import { AdminViewCards } from "./AdminViewCards";
14
14
  import { apiClient } from "../../../http";
15
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
15
16
  import { AdminProductEditorView } from "./AdminProductEditorView";
16
17
  import { QuickEditMenu } from "./QuickEditMenu";
17
18
  const PAGE_SIZE = 25;
@@ -183,8 +184,20 @@ export function AdminProductsView({ children, actionHref, getRowHref, ...props }
183
184
  { id: "sale", label: "Toggle On Sale", variant: "secondary", onClick: () => { for (const id of selection.selectedIds)
184
185
  void handleToggle(id, "isOnSale", !rows.find(r => r.id === id)?.isOnSale); selection.clearSelection(); } },
185
186
  ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, columns: [...buildBaseColumns(), flagColumn], isLoading: isLoading, emptyLabel: "No products found", onRowClick: (row) => openEditPanel(row.id), selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => (_jsx(QuickEditMenu, { actions: [
187
+ {
188
+ label: ACTIONS.ADMIN["approve-product"].label,
189
+ onClick: () => handleQuickEdit(row.id, { status: "published" }),
190
+ disabled: row.status === "published",
191
+ },
192
+ {
193
+ label: ACTIONS.ADMIN["reject-product"].label,
194
+ destructive: true,
195
+ onClick: () => handleQuickEdit(row.id, { status: "rejected" }),
196
+ disabled: row.status === "rejected",
197
+ },
186
198
  {
187
199
  label: "Quick edit",
200
+ separator: true,
188
201
  formTitle: "Quick Edit Product",
189
202
  fields: [
190
203
  { name: "status", label: "Status", type: "select", required: true,
@@ -2,14 +2,17 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
4
  import { X } from "lucide-react";
5
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
6
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
7
  import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
7
8
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
- import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu } from "../../../ui";
9
+ import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, useToast } from "../../../ui";
9
10
  import { AdminViewCards } from "./AdminViewCards";
10
11
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
12
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
13
  import { ADMIN_STORE_STATUS_TABS } from "../constants/filter-tabs";
12
14
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
15
+ import { apiClient } from "../../../http";
13
16
  import { DataTable } from "./DataTable";
14
17
  import { AdminStoreEditorView } from "./AdminStoreEditorView";
15
18
  const PAGE_SIZE = 25;
@@ -29,8 +32,26 @@ function StoresFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clea
29
32
  export function AdminStoresView({ children, ...props }) {
30
33
  const hasChildren = React.Children.count(children) > 0;
31
34
  const [view, setView] = useState("table");
35
+ const toast = useToast();
36
+ const queryClient = useQueryClient();
32
37
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
33
38
  const { openEditPanel, closePanel, isEditOpen, editId } = usePanelUrlSync();
39
+ const verifyStore = useMutation({
40
+ mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { isVerified: true }),
41
+ onSuccess: () => {
42
+ toast.showToast("Store verified.", "success");
43
+ void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
44
+ },
45
+ onError: () => { toast.showToast("Failed to verify store.", "error"); },
46
+ });
47
+ const suspendStore = useMutation({
48
+ mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { storeStatus: "suspended" }),
49
+ onSuccess: () => {
50
+ toast.showToast("Store suspended.", "success");
51
+ void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
52
+ },
53
+ onError: () => { toast.showToast("Failed to suspend store.", "error"); },
54
+ });
34
55
  const [searchInput, setSearchInput] = useState(table.get("q") || "");
35
56
  const [filterOpen, setFilterOpen] = useState(false);
36
57
  const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
@@ -93,10 +114,27 @@ export function AdminStoresView({ children, ...props }) {
93
114
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search stores, slugs, or owner names", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
94
115
  { id: "manage", label: "Manage Store", variant: "primary", onClick: () => { const id = selection.selectedIds[0]; if (id)
95
116
  openEditPanel(id); selection.clearSelection(); } },
96
- ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No stores found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [{
97
- label: "Manage",
98
- onClick: () => openEditPanel(row.id),
99
- }] })) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No stores found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(StoresFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminStoreEditorView, { open: isEditOpen, onClose: closePanel, storeId: editId ?? undefined, storeName: panelRow?.primary, currentStatus: panelRow?.status?.toLowerCase(), currentIsVerified: Boolean(panelRow?._raw?.isVerified), currentCapabilities: Array.isArray(panelRow?._raw?.capabilities)
117
+ ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No stores found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => {
118
+ const sr = row;
119
+ const isSuspended = sr.status?.toLowerCase() === "suspended";
120
+ const isVerified = Boolean(sr._raw?.isVerified);
121
+ return (_jsx(RowActionMenu, { actions: [
122
+ {
123
+ label: "Manage",
124
+ onClick: () => openEditPanel(sr.id),
125
+ },
126
+ {
127
+ label: ACTIONS.ADMIN["verify-store"].label,
128
+ onClick: () => verifyStore.mutate(sr.id),
129
+ disabled: isVerified || verifyStore.isPending,
130
+ },
131
+ {
132
+ label: ACTIONS.ADMIN["suspend-store"].label,
133
+ onClick: () => suspendStore.mutate(sr.id),
134
+ disabled: isSuspended || suspendStore.isPending,
135
+ },
136
+ ] }));
137
+ } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No stores found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(StoresFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminStoreEditorView, { open: isEditOpen, onClose: closePanel, storeId: editId ?? undefined, storeName: panelRow?.primary, currentStatus: panelRow?.status?.toLowerCase(), currentIsVerified: Boolean(panelRow?._raw?.isVerified), currentCapabilities: Array.isArray(panelRow?._raw?.capabilities)
100
138
  ? panelRow._raw.capabilities
101
139
  : undefined })] }));
102
140
  }
@@ -2,13 +2,16 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
4
  import { X } from "lucide-react";
5
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
6
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
7
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
- import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu } from "../../../ui";
8
+ import { BulkActionBar, Button, FilterChipGroup, Form, FormActions, Input, ListingToolbar, Modal, Pagination, ListingViewShell, RowActionMenu, Text as AppText, useToast } from "../../../ui";
8
9
  import { AdminViewCards } from "./AdminViewCards";
9
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
12
  import { ADMIN_USER_STATUS_TABS, ADMIN_USER_ROLE_TABS } from "../constants/filter-tabs";
11
13
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
14
+ import { apiClient } from "../../../http";
12
15
  import { DataTable } from "./DataTable";
13
16
  import { AdminUserEditorView } from "./AdminUserEditorView";
14
17
  const PAGE_SIZE = 25;
@@ -29,12 +32,40 @@ function UsersFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clear
29
32
  export function AdminUsersView({ children, ...props }) {
30
33
  const hasChildren = React.Children.count(children) > 0;
31
34
  const [view, setView] = useState("table");
35
+ const toast = useToast();
36
+ const queryClient = useQueryClient();
32
37
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
33
38
  const [searchInput, setSearchInput] = useState(table.get("q") || "");
34
39
  const [filterOpen, setFilterOpen] = useState(false);
35
40
  const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
36
41
  const [drawerOpen, setDrawerOpen] = useState(false);
37
42
  const [selectedRow, setSelectedRow] = useState(null);
43
+ const [banModalOpen, setBanModalOpen] = useState(false);
44
+ const [banTargetId, setBanTargetId] = useState(null);
45
+ const [banReason, setBanReason] = useState("");
46
+ const banUser = useMutation({
47
+ mutationFn: () => {
48
+ if (!banTargetId)
49
+ throw new Error("No user selected");
50
+ return apiClient.post(ADMIN_ENDPOINTS.USER_HARD_BAN(banTargetId), { reason: banReason.trim() });
51
+ },
52
+ onSuccess: () => {
53
+ toast.showToast("User has been banned.", "success");
54
+ setBanModalOpen(false);
55
+ setBanTargetId(null);
56
+ setBanReason("");
57
+ void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
58
+ },
59
+ onError: () => { toast.showToast("Failed to ban user.", "error"); },
60
+ });
61
+ const unbanUser = useMutation({
62
+ mutationFn: (uid) => apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(uid), {}),
63
+ onSuccess: () => {
64
+ toast.showToast("Ban lifted.", "success");
65
+ void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
66
+ },
67
+ onError: () => { toast.showToast("Failed to lift ban.", "error"); },
68
+ });
38
69
  const openFilters = useCallback(() => {
39
70
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
40
71
  setFilterOpen(true);
@@ -117,10 +148,26 @@ export function AdminUsersView({ children, ...props }) {
117
148
  }
118
149
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search users, email, or seller handles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
119
150
  { id: "manage", label: "Manage Selected", variant: "primary", onClick: () => { setSelectedRow(rows.find(r => r.id === selection.selectedIds[0]) ?? null); setDrawerOpen(true); selection.clearSelection(); } },
120
- ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No users found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [{
121
- label: "Manage",
122
- onClick: () => { setSelectedRow(row); setDrawerOpen(true); },
123
- }] })) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No users found", onRowClick: (row) => { setSelectedRow(row); setDrawerOpen(true); }, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(UsersFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
151
+ ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No users found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => {
152
+ const ur = row;
153
+ const isBanned = ur.status === "Hard banned";
154
+ return (_jsx(RowActionMenu, { actions: [
155
+ {
156
+ label: "Manage",
157
+ onClick: () => { setSelectedRow(ur); setDrawerOpen(true); },
158
+ },
159
+ {
160
+ label: ACTIONS.ADMIN["ban-user"].label,
161
+ onClick: () => { setBanTargetId(ur.id); setBanReason(""); setBanModalOpen(true); },
162
+ disabled: isBanned,
163
+ },
164
+ {
165
+ label: ACTIONS.ADMIN["unban-user"].label,
166
+ onClick: () => unbanUser.mutate(ur.id),
167
+ disabled: !isBanned || unbanUser.isPending,
168
+ },
169
+ ] }));
170
+ } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No users found", onRowClick: (row) => { setSelectedRow(row); setDrawerOpen(true); }, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(UsersFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
124
171
  selectedRow?._raw?.hardBanReason), currentHardBanReason: toStringValue(selectedRow?._raw?.hardBanReason, "") || undefined, currentSoftBans: Array.isArray(selectedRow?._raw?.softBans)
125
172
  ? selectedRow._raw.softBans.map((b) => ({
126
173
  action: toStringValue(b.action, ""),
@@ -135,5 +182,5 @@ export function AdminUsersView({ children, ...props }) {
135
182
  : null,
136
183
  bannedBy: toStringValue(b.bannedBy, ""),
137
184
  }))
138
- : undefined })] }));
185
+ : undefined }), _jsxs(Modal, { isOpen: banModalOpen, onClose: () => { setBanModalOpen(false); setBanTargetId(null); setBanReason(""); }, title: ACTIONS.ADMIN["ban-user"].confirmation.title, children: [_jsx(AppText, { size: "sm", color: "muted", className: "mb-4", children: ACTIONS.ADMIN["ban-user"].confirmation.body }), _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); banUser.mutate(); }, children: [_jsx(Input, { label: "Reason", value: banReason, onChange: (e) => setBanReason(e.target.value), placeholder: "e.g. Repeated fraud, scam activity\u2026", required: true }), _jsxs(FormActions, { children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => { setBanModalOpen(false); setBanTargetId(null); setBanReason(""); }, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "danger", isLoading: banUser.isPending, disabled: !banReason.trim() || banUser.isPending, children: ACTIONS.ADMIN["ban-user"].confirmation.confirmLabel })] })] })] })] }));
139
186
  }
@@ -17,7 +17,7 @@ export interface NotificationDocument {
17
17
  isRead: boolean;
18
18
  readAt?: Date;
19
19
  relatedId?: string;
20
- relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket";
20
+ relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket" | "scammer";
21
21
  createdAt: Date;
22
22
  updatedAt: Date;
23
23
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { Div, Input, Label, RichTextEditor, Select, Stack, TagInput, Text, Textarea, } from "../../../ui";
3
4
  import { Checkbox } from "../../forms/index";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { FormGroup } from "../../../ui";
3
4
  import { Label, Checkbox, RichTextEditor, Text } from "../../../ui";
@@ -7,5 +7,8 @@ export interface FeaturedAuctionsSectionProps {
7
7
  className?: string;
8
8
  filterByBrand?: string;
9
9
  initialItems?: ProductItem[];
10
+ rows?: number;
11
+ autoScroll?: boolean;
12
+ scrollInterval?: number;
10
13
  }
11
- export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
4
4
  import { SectionCarousel } from "./SectionCarousel";
5
5
  import { useFeaturedAuctions } from "../hooks/useFeaturedAuctions";
6
6
  import { MarketplaceAuctionCard } from "../../auctions/components/MarketplaceAuctionCard";
7
- export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, }) {
7
+ export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
8
8
  const { data: items = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
9
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
9
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
10
10
  }
@@ -7,5 +7,8 @@ export interface FeaturedPreOrdersSectionProps {
7
7
  className?: string;
8
8
  filterByBrand?: string;
9
9
  initialItems?: ProductItem[];
10
+ rows?: number;
11
+ autoScroll?: boolean;
12
+ scrollInterval?: number;
10
13
  }
11
- export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
4
4
  import { SectionCarousel } from "./SectionCarousel";
5
5
  import { useFeaturedPreOrders } from "../hooks/useFeaturedPreOrders";
6
6
  import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
7
- export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, }) {
7
+ export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
8
8
  const { data: items = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
9
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
9
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
10
10
  }
@@ -1,5 +1,4 @@
1
1
  import type { ProductItem } from "../../products/types";
2
- import type { SectionPagination } from "../schemas/firestore";
3
2
  export interface FeaturedProductsSectionProps {
4
3
  title?: string;
5
4
  description?: string;
@@ -8,11 +7,9 @@ export interface FeaturedProductsSectionProps {
8
7
  className?: string;
9
8
  filterByBrand?: string;
10
9
  initialItems?: ProductItem[];
11
- /** Number of grid rows (1–4). When 1 (default), renders the existing horizontal carousel. */
12
10
  rows?: number;
13
- /** Maximum items to show. Clamped to 5–20. */
14
11
  maxItems?: number;
15
- /** Pagination strategy for rows > 1. */
16
- pagination?: SectionPagination;
12
+ autoScroll?: boolean;
13
+ scrollInterval?: number;
17
14
  }
18
- export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, maxItems, pagination, }: FeaturedProductsSectionProps): import("react/jsx-runtime").JSX.Element | null;
15
+ export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedProductsSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -1,48 +1,11 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef } from "react";
4
- import Link from "next/link";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
5
3
  import { SectionCarousel } from "./SectionCarousel";
6
4
  import { useFeaturedProducts } from "../hooks/useFeaturedProducts";
7
- import { useMediaQuery } from "../../../react";
8
5
  import { InteractiveProductCard } from "../../products/components/InteractiveProductCard";
9
6
  import { ROUTES } from "../../../next";
10
- import { Section, Heading, Text } from "../../../ui";
11
7
  import { THEME_CONSTANTS } from "../../../tokens";
12
- // --- Multi-row grid + pagination -----------------------------------------------
13
- function ProductGrid({ items, rows, maxItems, pagination, viewMoreHref, viewMoreLabel, title, description, }) {
14
- const { themed } = THEME_CONSTANTS;
15
- // On mobile (<sm, 2-col grid) a second row doubles the section height to ~2 screens.
16
- // Collapse to 1 row automatically so the section fits within one viewport.
17
- const isMobile = useMediaQuery("(max-width: 639px)");
18
- const effectiveRows = isMobile ? 1 : rows;
19
- const pageSize = Math.min(effectiveRows * 5, maxItems);
20
- const [offset, setOffset] = useState(0);
21
- const timerRef = useRef(null);
22
- const totalItems = Math.min(items.length, maxItems);
23
- const visible = items.slice(offset, Math.min(offset + pageSize, totalItems));
24
- const canNext = offset + pageSize < totalItems;
25
- const canPrev = offset > 0;
26
- const next = () => setOffset((o) => Math.min(o + pageSize, totalItems - pageSize));
27
- const prev = () => setOffset((o) => Math.max(o - pageSize, 0));
28
- useEffect(() => {
29
- if (pagination !== "auto-scroll")
30
- return;
31
- timerRef.current = setInterval(() => {
32
- setOffset((o) => {
33
- const nextOffset = o + pageSize;
34
- return nextOffset >= totalItems ? 0 : nextOffset;
35
- });
36
- }, 5000);
37
- return () => { if (timerRef.current)
38
- clearInterval(timerRef.current); };
39
- }, [pagination, pageSize, totalItems]);
40
- return (_jsx(Section, { className: `py-12 px-4 ${themed.bgPrimary}`, children: _jsxs("div", { className: "mx-auto max-w-7xl", children: [_jsxs("div", { className: "mb-6 flex items-end justify-between gap-4", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: `text-2xl font-bold md:text-3xl ${themed.textPrimary}`, children: title }), description && (_jsx(Text, { variant: "muted", className: "mt-1 text-sm", children: description }))] }), pagination === "arrows" && (_jsxs("div", { className: "flex shrink-0 gap-2", children: [_jsx("button", { type: "button", onClick: prev, disabled: !canPrev, "aria-label": "Previous", className: "flex h-9 w-9 items-center justify-center rounded-full border border-zinc-200 bg-white text-zinc-600 hover:border-[var(--appkit-color-primary)] hover:text-[var(--appkit-color-primary)] disabled:opacity-40 dark:border-slate-700 dark:bg-slate-900 dark:text-zinc-300", children: "\u2039" }), _jsx("button", { type: "button", onClick: next, disabled: !canNext, "aria-label": "Next", className: "flex h-9 w-9 items-center justify-center rounded-full border border-zinc-200 bg-white text-zinc-600 hover:border-[var(--appkit-color-primary)] hover:text-[var(--appkit-color-primary)] disabled:opacity-40 dark:border-slate-700 dark:bg-slate-900 dark:text-zinc-300", children: "\u203A" })] }))] }), _jsx("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", children: visible.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(product.slug ?? product.id ?? "")) }, product.id))) }), pagination === "load-more" && canNext && (_jsx("div", { className: "mt-8 text-center", children: _jsx("button", { type: "button", onClick: next, className: "rounded-lg border border-zinc-300 bg-white px-6 py-2 text-sm font-medium text-zinc-700 hover:border-[var(--appkit-color-primary)] hover:text-[var(--appkit-color-primary)] dark:border-slate-600 dark:bg-slate-900 dark:text-zinc-300", children: "Load more" }) })), viewMoreHref && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: viewMoreHref, className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
41
- }
42
- // --- Main component -----------------------------------------------------------
43
- export function FeaturedProductsSection({ title = "Featured Products", description, viewMoreHref, viewMoreLabel = "View all products →", className = "", filterByBrand, initialItems, rows = 1, maxItems, pagination = "load-more", }) {
44
- const effectiveRows = Math.min(Math.max(rows, 1), 4);
45
- const effectiveMaxItems = Math.min(Math.max(maxItems ?? effectiveRows * 5, 5), 20);
8
+ export function FeaturedProductsSection({ title = "Featured Products", description, viewMoreHref, viewMoreLabel = "View all products →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
46
9
  const { data, isLoading } = useFeaturedProducts({
47
10
  filterByBrand,
48
11
  initialData: initialItems?.length
@@ -50,12 +13,5 @@ export function FeaturedProductsSection({ title = "Featured Products", descripti
50
13
  : undefined,
51
14
  });
52
15
  const items = data?.items ?? [];
53
- // Multi-row mode render grid with pagination
54
- if (effectiveRows > 1) {
55
- if (isLoading)
56
- return null;
57
- return (_jsx(ProductGrid, { items: items, rows: effectiveRows, maxItems: effectiveMaxItems, pagination: pagination, viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, title: title, description: description }));
58
- }
59
- // Single-row mode — original horizontal carousel (backward-compat)
60
- return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Featured Products", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(product.slug ?? product.id ?? "")) })), className: className }));
16
+ return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Featured Products", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(product.slug ?? product.id ?? "")) })), className: className }));
61
17
  }
@@ -36,14 +36,7 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
36
36
  headingClass,
37
37
  ]
38
38
  .filter(Boolean)
39
- .join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth,
40
- /* Default behaviour for homepage sections using cards:
41
- - show arrows for easier navigation
42
- - snap to items for a tidy carousel feel
43
- - show fade edges for visual affordance
44
- - hide native scrollbar for cleaner appearance
45
- */
46
- showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
39
+ .join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop: autoScroll && rows === 1, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
47
40
  ? "border-white/40 text-white hover:bg-white/10"
48
41
  : "border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800"}`, children: viewMoreLabel }) }))] })] }));
49
42
  }