@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.
- package/dist/_internal/server/features/auctions/og.d.ts +1 -0
- package/dist/_internal/server/features/auctions/og.js +2 -1
- package/dist/_internal/server/features/blog/og.d.ts +1 -0
- package/dist/_internal/server/features/blog/og.js +3 -2
- package/dist/_internal/server/features/brands/og.d.ts +1 -0
- package/dist/_internal/server/features/brands/og.js +2 -1
- package/dist/_internal/server/features/bundles/og.d.ts +1 -0
- package/dist/_internal/server/features/bundles/og.js +2 -1
- package/dist/_internal/server/features/categories/og.d.ts +1 -0
- package/dist/_internal/server/features/categories/og.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +13 -5
- package/dist/_internal/server/features/classified/og.d.ts +2 -0
- package/dist/_internal/server/features/classified/og.js +2 -1
- package/dist/_internal/server/features/digital-code/og.d.ts +2 -0
- package/dist/_internal/server/features/digital-code/og.js +2 -1
- package/dist/_internal/server/features/events/og.d.ts +1 -0
- package/dist/_internal/server/features/events/og.js +3 -1
- package/dist/_internal/server/features/live/og.d.ts +2 -0
- package/dist/_internal/server/features/live/og.js +2 -1
- package/dist/_internal/server/features/pre-orders/og.d.ts +1 -0
- package/dist/_internal/server/features/pre-orders/og.js +2 -1
- package/dist/_internal/server/features/products/og.d.ts +3 -1
- package/dist/_internal/server/features/products/og.js +2 -1
- package/dist/_internal/server/features/seo/index.d.ts +1 -1
- package/dist/_internal/server/features/seo/index.js +1 -1
- package/dist/_internal/server/features/seo/og.d.ts +8 -1
- package/dist/_internal/server/features/seo/og.js +28 -8
- package/dist/_internal/server/features/stores/og.d.ts +1 -0
- package/dist/_internal/server/features/stores/og.js +3 -2
- package/dist/_internal/server/features/sublisting-categories/og.d.ts +1 -0
- package/dist/_internal/server/features/sublisting-categories/og.js +2 -1
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +18 -14
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +10 -7
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +10 -7
- package/dist/_internal/shared/actions/action-registry.js +48 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +3 -0
- package/dist/constants/api-endpoints.d.ts +9 -0
- package/dist/constants/api-endpoints.js +3 -0
- package/dist/features/admin/components/AdminBundlesView.js +20 -2
- package/dist/features/admin/components/AdminPayoutsView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +13 -0
- package/dist/features/admin/components/AdminStoresView.js +43 -5
- package/dist/features/admin/components/AdminUsersView.js +53 -6
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/blog/components/BlogPostForm.js +1 -0
- package/dist/features/categories/components/CategoryForm.js +1 -0
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +2 -2
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +2 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -6
- package/dist/features/homepage/components/FeaturedProductsSection.js +3 -47
- package/dist/features/homepage/components/SectionCarousel.js +1 -8
- package/dist/features/homepage/lib/section-renderer.js +3 -3
- package/dist/seed/homepage-sections-seed-data.js +10 -10
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/DateInput.d.ts +27 -0
- package/dist/ui/components/DateInput.js +11 -0
- package/dist/ui/components/FormField.d.ts +2 -1
- package/dist/ui/components/FormField.js +7 -4
- package/dist/ui/components/FormField.style.css +13 -0
- package/dist/ui/components/HorizontalScroller.js +25 -30
- package/dist/ui/components/OtpInput.d.ts +14 -0
- package/dist/ui/components/OtpInput.js +55 -0
- package/dist/ui/components/OtpInput.style.css +13 -0
- package/dist/ui/components/Select.style.css +23 -6
- package/dist/ui/components/Textarea.style.css +31 -4
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- 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: "
|
|
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) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
16
|
-
|
|
12
|
+
autoScroll?: boolean;
|
|
13
|
+
scrollInterval?: number;
|
|
17
14
|
}
|
|
18
|
-
export declare function FeaturedProductsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|