@mohasinac/appkit 2.7.45 → 2.7.47

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 (88) hide show
  1. package/dist/client.d.ts +2 -2
  2. package/dist/client.js +1 -1
  3. package/dist/constants/field-names.d.ts +1 -0
  4. package/dist/constants/field-names.js +1 -0
  5. package/dist/core/unit-of-work.js +3 -4
  6. package/dist/features/account/components/AddressForm.js +23 -1
  7. package/dist/features/admin/components/AdminBlogEditorView.js +18 -11
  8. package/dist/features/admin/components/AdminBrandEditorView.js +18 -11
  9. package/dist/features/admin/components/AdminCarouselEditorView.js +18 -8
  10. package/dist/features/admin/components/AdminCategoryEditorView.js +18 -11
  11. package/dist/features/admin/components/AdminCouponEditorView.js +18 -11
  12. package/dist/features/admin/components/AdminFaqEditorView.js +18 -11
  13. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +18 -11
  14. package/dist/features/auth/actions/profile-actions.js +4 -1
  15. package/dist/features/auth/repository/user.repository.d.ts +1 -0
  16. package/dist/features/auth/repository/user.repository.js +3 -0
  17. package/dist/features/blog/api/route.js +3 -0
  18. package/dist/features/blog/components/BlogIndexListing.js +2 -0
  19. package/dist/features/blog/components/BlogPostView.js +2 -1
  20. package/dist/features/categories/api/route.js +1 -1
  21. package/dist/features/categories/components/BundleDetailView.js +5 -3
  22. package/dist/features/categories/components/CategoryBundlesListing.js +77 -41
  23. package/dist/features/categories/components/CategoryDetailPageView.js +12 -9
  24. package/dist/features/categories/components/CategoryProductsListing.js +3 -5
  25. package/dist/features/classified/components/ClassifiedIndexListing.js +3 -4
  26. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +3 -4
  27. package/dist/features/events/actions/event-actions.d.ts +2 -3
  28. package/dist/features/events/actions/event-actions.js +3 -5
  29. package/dist/features/events/api/route.js +7 -7
  30. package/dist/features/events/components/AdminEventEditorView.js +80 -6
  31. package/dist/features/events/components/AdminEventEntriesView.js +33 -4
  32. package/dist/features/events/components/EventFilters.d.ts +3 -0
  33. package/dist/features/events/components/EventFilters.js +1 -0
  34. package/dist/features/events/components/EventLeaderboard.js +1 -1
  35. package/dist/features/events/components/EventsIndexListing.js +10 -7
  36. package/dist/features/events/repository/event-entry.repository.d.ts +2 -2
  37. package/dist/features/events/repository/event-entry.repository.js +32 -7
  38. package/dist/features/events/types/index.d.ts +3 -1
  39. package/dist/features/homepage/components/BrandsSection.js +2 -2
  40. package/dist/features/homepage/components/CustomCardsSection.js +1 -1
  41. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  42. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  43. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  44. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  45. package/dist/features/live/components/LiveItemsIndexListing.js +3 -4
  46. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -4
  47. package/dist/features/products/components/AuctionsIndexListing.js +3 -4
  48. package/dist/features/products/components/PrizeDrawsIndexListing.js +3 -8
  49. package/dist/features/products/components/ProductDetailPageView.js +17 -2
  50. package/dist/features/products/components/ProductsIndexListing.js +10 -6
  51. package/dist/features/products/schemas/firestore.d.ts +2 -0
  52. package/dist/features/reviews/actions/review-actions.js +2 -0
  53. package/dist/features/reviews/components/ReviewDetailShell.js +3 -2
  54. package/dist/features/reviews/components/ReviewFilters.js +1 -1
  55. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -2
  56. package/dist/features/reviews/hooks/useReviews.js +2 -0
  57. package/dist/features/reviews/schemas/firestore.d.ts +4 -0
  58. package/dist/features/reviews/types/index.d.ts +3 -0
  59. package/dist/features/scams/components/ScamRegistryView.js +41 -8
  60. package/dist/features/seller/components/SellerProductShell.js +27 -3
  61. package/dist/features/shell/StepForm.d.ts +6 -2
  62. package/dist/features/shell/StepForm.js +13 -10
  63. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  64. package/dist/features/stores/components/StoreAuctionsListing.js +3 -4
  65. package/dist/features/stores/components/StoreDetailLayoutView.js +13 -5
  66. package/dist/features/stores/components/StorePreOrdersListing.js +3 -4
  67. package/dist/features/stores/components/StoreProductsListing.js +3 -4
  68. package/dist/index.d.ts +1 -0
  69. package/dist/providers/db-firebase/helpers.d.ts +4 -6
  70. package/dist/providers/db-firebase/helpers.js +18 -5
  71. package/dist/providers/db-firebase/sieve.js +26 -1
  72. package/dist/react/hooks/usePendingFilters.d.ts +2 -0
  73. package/dist/react/hooks/usePendingFilters.js +9 -0
  74. package/dist/react/hooks/usePendingTable.d.ts +5 -0
  75. package/dist/react/hooks/usePendingTable.js +1 -0
  76. package/dist/seed/homepage-sections-seed-data.js +2 -2
  77. package/dist/seed/products-standard-seed-data.js +1 -0
  78. package/dist/tailwind-utilities.css +1 -1
  79. package/dist/ui/components/Drawer.style.css +13 -0
  80. package/dist/ui/components/FilterDrawer.d.ts +4 -2
  81. package/dist/ui/components/FilterDrawer.js +2 -2
  82. package/dist/ui/components/Toast.d.ts +4 -1
  83. package/dist/ui/components/Toast.js +3 -3
  84. package/dist/ui/forms/FormShell.d.ts +19 -1
  85. package/dist/ui/forms/FormShell.js +92 -0
  86. package/dist/ui/forms/index.d.ts +2 -2
  87. package/dist/ui/forms/index.js +1 -1
  88. package/package.json +2 -1
@@ -123,6 +123,13 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
123
123
  const reviewCount = typeof p.reviewCount === "number" ? p.reviewCount : null;
124
124
  const category = typeof p.category === "string" ? p.category : null;
125
125
  const categoryName = typeof p.categoryName === "string" ? p.categoryName : null;
126
+ // categorySlugs/categoryNames: multi-category support (S-uni-formshell-prep)
127
+ const categorySlugs = Array.isArray(p.categorySlugs)
128
+ ? p.categorySlugs
129
+ : category ? [category] : [];
130
+ const categoryNames = Array.isArray(p.categoryNames)
131
+ ? p.categoryNames
132
+ : categoryName ? [categoryName] : [];
126
133
  const subcategory = typeof p.subcategory === "string" ? p.subcategory : null;
127
134
  const brand = typeof p.brand === "string" ? p.brand : null;
128
135
  const brandSlug = typeof p.brandSlug === "string" ? p.brandSlug : null;
@@ -152,6 +159,14 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
152
159
  const specs = Array.isArray(p.specifications)
153
160
  ? p.specifications
154
161
  : [];
162
+ const customFields = Array.isArray(p.customFields)
163
+ ? p.customFields.map((f) => ({
164
+ name: f.key,
165
+ value: String(f.value),
166
+ unit: f.unit,
167
+ }))
168
+ : [];
169
+ const allSpecs = [...specs, ...customFields];
155
170
  const customSections = Array.isArray(p.customSections)
156
171
  ? p.customSections
157
172
  : [];
@@ -234,7 +249,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
234
249
  ? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
235
250
  : "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400"}`, children: [inStock ? "✓ In Stock" : "✗ Out of Stock", inStock && effectiveStock !== null && effectiveStock <= 10
236
251
  ? ` — only ${effectiveStock} left`
237
- : ""] })), (categoryName || category || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category })), subcategory && (_jsx(Span, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 capitalize", children: subcategory })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400", children: brand }))] })), bundleMemberships.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, children: bundleMemberships.map((b) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: "inline-flex items-center gap-1 rounded-full border border-teal-300 bg-teal-50 px-2.5 py-1 text-xs font-semibold text-teal-700 transition-colors hover:border-teal-500 hover:bg-teal-100 dark:border-teal-800/60 dark:bg-teal-900/30 dark:text-teal-300 dark:hover:border-teal-600 dark:hover:bg-teal-900/50", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), "In bundle: ", b.title] }, b.id))) })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: returnPolicy != null && returnPolicy.length > 0, labels: {
252
+ : ""] })), (categorySlugs.length > 0 || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [categorySlugs.map((slug, i) => (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryNames[i] ?? slug }, slug))), subcategory && (_jsx(Span, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 capitalize", children: subcategory })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400", children: brand }))] })), bundleMemberships.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, children: bundleMemberships.map((b) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: "inline-flex items-center gap-1 rounded-full border border-teal-300 bg-teal-50 px-2.5 py-1 text-xs font-semibold text-teal-700 transition-colors hover:border-teal-500 hover:bg-teal-100 dark:border-teal-800/60 dark:bg-teal-900/30 dark:text-teal-300 dark:hover:border-teal-600 dark:hover:bg-teal-900/50", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), "In bundle: ", b.title] }, b.id))) })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: returnPolicy != null && returnPolicy.length > 0, labels: {
238
253
  featured: "Featured",
239
254
  fasterDelivery: "Faster Delivery",
240
255
  ratedSeller: "Rated Seller",
@@ -270,7 +285,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
270
285
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
271
286
  : undefined, renderGroupSection: groupId
272
287
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
273
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
288
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: allSpecs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
274
289
  id: s.id,
275
290
  label: s.title,
276
291
  content: _jsx(CustomSectionTabContent, { section: s }),
@@ -17,11 +17,13 @@ import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
17
17
  import { pushCartOp, pushWishlistOp } from "../../cart/utils/pending-ops";
18
18
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
19
19
  import { useCategoryTree, categoriesToFacetOptions } from "../../categories/hooks/useCategoryTree";
20
+ import { useBrands } from "../hooks/useBrands";
21
+ import { useProductFeatures } from "./ProductFeaturesContext";
20
22
  import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
21
23
  import { sortBy } from "../../../constants/sort";
22
24
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
23
25
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
24
- const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.CONDITION, TABLE_KEYS.MIN_PRICE, TABLE_KEYS.MAX_PRICE, TABLE_KEYS.BRAND, TABLE_KEYS.STORE_ID, TABLE_KEYS.FREE_SHIPPING, TABLE_KEYS.TAGS];
26
+ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.CONDITION, TABLE_KEYS.MIN_PRICE, TABLE_KEYS.MAX_PRICE, TABLE_KEYS.BRAND, TABLE_KEYS.STORE_ID, TABLE_KEYS.FREE_SHIPPING, TABLE_KEYS.TAGS, TABLE_KEYS.FEATURES, TABLE_KEYS.IS_PART_OF_BUNDLE];
25
27
  export function ProductsIndexListing({ initialData }) {
26
28
  const router = useRouter();
27
29
  const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
@@ -32,7 +34,7 @@ export function ProductsIndexListing({ initialData }) {
32
34
  const [compareIds, setCompareIds] = useState([]);
33
35
  const showSold = table.get(TABLE_KEYS.SHOW_SOLD) === "true";
34
36
  const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
35
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
37
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
36
38
  const openFilters = useCallback(() => {
37
39
  onFilterReset();
38
40
  setFilterOpen(true);
@@ -42,10 +44,9 @@ export function ProductsIndexListing({ initialData }) {
42
44
  setFilterOpen(false);
43
45
  }, [onFilterApply]);
44
46
  const resetAll = useCallback(() => {
45
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_SOLD]: "" });
46
- onFilterClear();
47
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_SOLD]: "" });
47
48
  setSearchInput("");
48
- }, [table, onFilterClear]);
49
+ }, [onResetAll]);
49
50
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
50
51
  table.get(TABLE_KEYS.SHOW_SOLD) === "true" ||
51
52
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
@@ -55,6 +56,9 @@ export function ProductsIndexListing({ initialData }) {
55
56
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "product").map((i) => i.itemId));
56
57
  const { categories } = useCategoryTree();
57
58
  const categoryOptions = categoriesToFacetOptions(categories);
59
+ const { brandOptions } = useBrands();
60
+ const features = useProductFeatures() ?? [];
61
+ const featureOptions = features.map((f) => ({ value: f.id, label: f.label }));
58
62
  const params = {
59
63
  q: table.get(TABLE_KEYS.QUERY) || undefined,
60
64
  category: table.get(TABLE_KEYS.CATEGORY) || undefined,
@@ -169,5 +173,5 @@ export function ProductsIndexListing({ initialData }) {
169
173
  ] }), 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view === "grid" ? "card" : "list", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart, onBuyNow: handleBuyNow, selectionMode: selection.isSelecting, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "product", onClose: () => {
170
174
  setCompareIds([]);
171
175
  selection.clearSelection();
172
- }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
176
+ }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions, featureOptions: featureOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
173
177
  }
@@ -206,6 +206,8 @@ export interface ProductDocument {
206
206
  /** Reverse pointers — bundle ids that include this product. */
207
207
  partOfBundleIds?: string[];
208
208
  partOfBundleTitles?: string[];
209
+ /** Denormalised flag — true when partOfBundleIds has at least one entry. Enables Firestore isPartOfBundle==true query. */
210
+ isPartOfBundle?: boolean;
209
211
  prizeDrawItems?: PrizeDrawItem[];
210
212
  pricePerEntry?: number;
211
213
  prizeMaxEntries?: number;
@@ -33,12 +33,14 @@ export async function createReview(userId, input) {
33
33
  storeId: product.storeId,
34
34
  storeName: product.storeName,
35
35
  userId,
36
+ userSlug: profile?.id,
36
37
  userName: profile?.displayName ?? "Anonymous",
37
38
  userAvatar: profile?.photoURL ?? "",
38
39
  rating: input.rating,
39
40
  title: input.title,
40
41
  comment: input.comment,
41
42
  images,
43
+ hasImages: images.length > 0,
42
44
  video: finalVideoUrl
43
45
  ? {
44
46
  url: finalVideoUrl,
@@ -72,8 +72,9 @@ export function ReviewDetailShell({ review, storeHref }) {
72
72
  ? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
73
73
  : null;
74
74
  const sellerHref = storeHref ?? null;
75
- const reviewerHref = !review.isAnonymous && review.userId
76
- ? String(ROUTES.PUBLIC.PROFILE(review.userId))
75
+ const reviewerProfileId = review.userSlug ?? review.userId;
76
+ const reviewerHref = !review.isAnonymous && reviewerProfileId
77
+ ? String(ROUTES.PUBLIC.PROFILE(reviewerProfileId))
77
78
  : null;
78
79
  const currentImage = lightboxIdx !== null ? images[lightboxIdx] : null;
79
80
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-neutral-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 pb-8 pt-10", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx(StarRating, { value: review.rating, size: "lg", readOnly: true }), _jsxs("span", { className: "text-2xl font-bold text-neutral-900 dark:text-white", children: [review.rating, ".0"] }), review.verified && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "\u2713 Verified Purchase" })), review.featured && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", children: "\u2605 Featured" }))] }), review.title && (_jsx(Heading, { level: 1, className: "text-2xl font-bold text-neutral-900 dark:text-white mb-4 leading-snug", children: review.title })), _jsxs("div", { className: "flex items-center gap-3", children: [review.userAvatar ? (_jsx("div", { role: "img", "aria-label": displayName, className: "h-11 w-11 flex-shrink-0 rounded-full bg-center bg-cover ring-2 ring-white dark:ring-zinc-800", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-primary/10 text-base font-bold text-primary ring-2 ring-white dark:ring-zinc-800", children: initials })), _jsxs("div", { className: "min-w-0", children: [reviewerHref ? (_jsx(Link, { href: reviewerHref, className: "text-sm font-semibold text-neutral-900 dark:text-white hover:text-primary transition-colors", children: displayName })) : (_jsx("span", { className: "text-sm font-semibold text-neutral-900 dark:text-white", children: review.isAnonymous ? "Anonymous" : displayName })), date && (_jsx(Text, { className: "text-xs text-neutral-400 dark:text-zinc-500 mt-0.5", children: date }))] })] })] }) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-8 space-y-8", children: [review.comment && (_jsx(Section, { children: _jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:font-semibold prose-img:rounded-lg prose-a:text-primary", className: "text-neutral-700 dark:text-zinc-300" }) })), images.length > 0 && (_jsxs(Section, { children: [_jsxs(Heading, { level: 2, className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: ["Photos (", images.length, ")"] }), _jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: images.map((img, i) => (_jsxs("button", { type: "button", onClick: () => setLightboxIdx(i), "aria-label": `View photo ${i + 1}`, className: "group relative aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-neutral-100 dark:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: [_jsx("div", { className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30", children: _jsx("span", { className: "text-white text-xl", children: "\uD83D\uDD0D" }) })] }, i))) })] })), review.video && (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: "Video" }), _jsx("div", { className: "overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-black aspect-video", children: _jsx("video", { src: review.video.url, poster: review.video.thumbnailUrl, controls: true, className: "h-full w-full", preload: "metadata" }) })] })), _jsxs(Section, { className: "flex items-center gap-4 py-4 border-t border-neutral-100 dark:border-zinc-800", children: [_jsx("div", { className: "text-sm text-neutral-500 dark:text-zinc-400", children: helpfulCount > 0 && (_jsxs("span", { children: [_jsx("strong", { className: "text-neutral-900 dark:text-white", children: helpfulCount }), " ", helpfulCount === 1 ? "person" : "people", " found this helpful"] })) }), _jsxs("button", { type: "button", onClick: handleVote, disabled: voted || voting, className: `ml-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors ${voted
@@ -60,5 +60,5 @@ export function ReviewFilters({ table, variant = "admin", brandOptions, }) {
60
60
  : [];
61
61
  const isAdmin = variant === "admin";
62
62
  const showStatus = variant !== "public";
63
- return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), _jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" }), brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: true }), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : "") }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : "") })] })), _jsx(RangeFilter, { title: t("votesRange"), minValue: table.get("minVotes"), maxValue: table.get("maxVotes"), onMinChange: (v) => table.set("minVotes", v), onMaxChange: (v) => table.set("maxVotes", v), minBound: 0, maxBound: 10000, step: 1, minPlaceholder: t("minVotes"), maxPlaceholder: t("maxVotes"), defaultCollapsed: true })] }));
63
+ return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), _jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" }), brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: true }), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : "") }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : "") })] })), _jsx(RangeFilter, { title: t("votesRange"), minValue: table.get("minVotes"), maxValue: table.get("maxVotes"), onMinChange: (v) => table.set("minVotes", v), onMaxChange: (v) => table.set("maxVotes", v), minBound: 0, maxBound: 10000, step: 1, minPlaceholder: t("minVotes"), maxPlaceholder: t("maxVotes"), defaultCollapsed: true }), _jsx(SwitchFilter, { title: "Media", label: "Show reviews with photos only", checked: table.get("hasImages") === "true", onChange: (v) => table.set("hasImages", v ? "true" : "") })] }));
64
64
  }
@@ -12,7 +12,13 @@ import { sortBy } from "../../../constants/sort";
12
12
  import { REVIEW_FIELDS } from "../../../constants/field-names";
13
13
  const PAGE_SIZE = 12;
14
14
  const DEFAULT_SORT = sortBy(REVIEW_FIELDS.CREATED_AT);
15
- const FILTER_KEYS = ["rating", TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO, "minVotes", "maxVotes"];
15
+ const FILTER_KEYS = ["rating", TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO, "minVotes", "maxVotes", "hasImages"];
16
+ const SORT_OPTION_LABELS = {
17
+ sortNewest: "Newest First",
18
+ sortOldest: "Oldest First",
19
+ sortHighestRated: "Highest Rated",
20
+ sortLowestRated: "Lowest Rated",
21
+ };
16
22
  export function ReviewsIndexListing({ initialData, variant = "public", }) {
17
23
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
18
24
  const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
@@ -88,13 +94,14 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
88
94
  dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
89
95
  minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
90
96
  maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
97
+ hasImages: table.get("hasImages") === "true" ? true : undefined,
91
98
  sort,
92
99
  page: currentPage,
93
100
  perPage: PAGE_SIZE,
94
101
  }, { initialData });
95
102
  const sortOptions = REVIEW_PUBLIC_SORT_OPTIONS.map((opt) => ({
96
103
  value: opt.value,
97
- label: opt.key,
104
+ label: SORT_OPTION_LABELS[opt.key] ?? opt.key,
98
105
  }));
99
106
  return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), 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) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx(Text, { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
100
107
  }
@@ -29,6 +29,8 @@ export function useReviews(params = {}, opts) {
29
29
  sp.set("minVotes", String(params.minVotes));
30
30
  if (params.maxVotes !== undefined)
31
31
  sp.set("maxVotes", String(params.maxVotes));
32
+ if (params.hasImages)
33
+ sp.set("hasImages", "true");
32
34
  // General listing mode (no productId)
33
35
  if (!params.productId && !params.userId && !params.storeId) {
34
36
  sp.set("latest", "true");
@@ -22,12 +22,16 @@ export interface ReviewDocument {
22
22
  storeId?: string;
23
23
  storeName?: string;
24
24
  userId: string;
25
+ /** Firestore document ID of the reviewer (slug like `user-mohsin-c`). Populated on create; used to build profile links. */
26
+ userSlug?: string;
25
27
  userName: string;
26
28
  userAvatar?: string;
27
29
  rating: number;
28
30
  title: string;
29
31
  comment: string;
30
32
  images?: string[];
33
+ /** Denormalized flag — true when images array is non-empty. Enables hasImages==true Firestore query. */
34
+ hasImages?: boolean;
31
35
  video?: ReviewVideoField;
32
36
  status: ReviewStatus;
33
37
  moderatorId?: string;
@@ -17,6 +17,8 @@ export interface Review {
17
17
  storeSlug?: string;
18
18
  storeName?: string;
19
19
  userId: string;
20
+ /** Firestore document slug for the reviewer — use for profile links instead of userId (Auth UID). */
21
+ userSlug?: string;
20
22
  userName: string;
21
23
  userAvatar?: string;
22
24
  rating: 1 | 2 | 3 | 4 | 5;
@@ -58,6 +60,7 @@ export interface ReviewListParams {
58
60
  dateTo?: string;
59
61
  minVotes?: number;
60
62
  maxVotes?: number;
63
+ hasImages?: boolean;
61
64
  }
62
65
  export interface CreateReviewInput {
63
66
  productId: string;
@@ -5,8 +5,14 @@ import { Shield, Phone, Wallet, Mail, ChevronRight, Search } from "lucide-react"
5
5
  import { Container, Heading, Main, Section, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui";
6
6
  import { listVerifiedScammers } from "../actions/scam-actions";
7
7
  import { SCAM_PLATFORM_LABELS } from "../schemas/firestore";
8
- import { SCAM_TYPE_LABELS } from "../constants/scam-types";
8
+ import { SCAM_TYPES, SCAM_TYPE_LABELS } from "../constants/scam-types";
9
9
  import { ROUTES } from "../../../next/routing/route-map";
10
+ const SORT_OPTIONS = [
11
+ { value: "-createdAt", label: "Newest Reports" },
12
+ { value: "createdAt", label: "Oldest Reports" },
13
+ { value: "-incidentCount", label: "Most Victims" },
14
+ { value: "-amountLost", label: "Highest Loss" },
15
+ ];
10
16
  function sp(params, key) {
11
17
  const v = params[key];
12
18
  return Array.isArray(v) ? (v[0] ?? "") : (v ?? "");
@@ -26,17 +32,44 @@ function ScammerCard({ scammer }) {
26
32
  }
27
33
  export async function ScamRegistryView({ searchParams = {} }) {
28
34
  const query = sp(searchParams, "q");
29
- const result = await listVerifiedScammers(searchParams).catch(() => ({
35
+ const sort = sp(searchParams, "sort") || "-createdAt";
36
+ const scamType = sp(searchParams, "scamType");
37
+ const pageSize = 20;
38
+ const result = await listVerifiedScammers({ ...searchParams, sort, pageSize: String(pageSize) }).catch(() => ({
30
39
  items: [],
31
40
  total: 0,
32
41
  page: 1,
33
- pageSize: 20,
42
+ pageSize,
34
43
  hasMore: false,
35
44
  }));
45
+ const totalPages = Math.max(1, Math.ceil(result.total / pageSize));
36
46
  const reportHref = String(ROUTES.PUBLIC.SCAM_REPORT);
37
- return (_jsxs(Main, { children: [_jsx("div", { className: "border-b appkit-alert--warning rounded-none", children: _jsx(Container, { size: "xl", className: "py-2.5", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, className: "text-3xl font-semibold", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", className: "text-sm", children: "Verified scammers active in India's collectibles community. Search by name, phone, or UPI." })] }), _jsxs(Link, { href: reportHref, className: "appkit-button appkit-button--danger appkit-button--md shrink-0", children: [_jsx(Shield, { className: "h-4 w-4" }), "Report a Scammer"] })] }), _jsx("form", { method: "GET", className: "max-w-xl", children: _jsx(Input, { type: "search", name: "q", defaultValue: query, placeholder: "Search name, phone number, UPI ID, or email\u2026", icon: _jsx(Search, { className: "h-4 w-4" }) }) }), result.items.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Shield, { className: "h-12 w-12" }), title: query
38
- ? "No verified scammers matched your search"
39
- : "No verified scammers in the registry yet", description: query
40
- ? "Try a different name, phone number, or UPI ID."
41
- : "Verified reports will appear here once reviewed by our moderation team." })) : (_jsxs(Stack, { gap: "md", children: [result.total > 0 && (_jsxs(Text, { variant: "secondary", className: "text-sm", children: [result.total, " verified profile", result.total !== 1 ? "s" : ""] })), _jsx(Grid, { cols: 3, gap: "md", children: result.items.map((scammer) => (_jsx(ScammerCard, { scammer: scammer }, scammer.id))) })] })), result.hasMore && (_jsx(Row, { justify: "center", children: _jsx(Link, { href: `/scams?page=${result.page + 1}${query ? `&q=${encodeURIComponent(query)}` : ""}`, className: "appkit-button appkit-button--outline appkit-button--md", children: "Load more" }) }))] }) }) })] }));
47
+ function buildHref(overrides) {
48
+ const p = new URLSearchParams();
49
+ if (query)
50
+ p.set("q", query);
51
+ if (sort !== "-createdAt")
52
+ p.set("sort", sort);
53
+ if (scamType)
54
+ p.set("scamType", scamType);
55
+ p.set("page", String(result.page));
56
+ for (const [k, v] of Object.entries(overrides)) {
57
+ if (v)
58
+ p.set(k, v);
59
+ else
60
+ p.delete(k);
61
+ }
62
+ const qs = p.toString();
63
+ return `/scams${qs ? `?${qs}` : ""}`;
64
+ }
65
+ return (_jsxs(Main, { children: [_jsx("div", { className: "border-b appkit-alert--warning rounded-none", children: _jsx(Container, { size: "xl", className: "py-2.5", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, className: "text-3xl font-semibold", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", className: "text-sm", children: "Verified scammers active in India's collectibles community. Search by name, phone, or UPI." })] }), _jsxs(Link, { href: reportHref, className: "appkit-button appkit-button--danger appkit-button--md shrink-0", children: [_jsx(Shield, { className: "h-4 w-4" }), "Report a Scammer"] })] }), _jsxs("form", { method: "GET", className: "flex flex-wrap gap-3", children: [_jsx("div", { className: "flex-1 min-w-48", children: _jsx(Input, { type: "search", name: "q", defaultValue: query, placeholder: "Search name, phone, UPI ID, or email\u2026", icon: _jsx(Search, { className: "h-4 w-4" }) }) }), _jsxs("select", { name: "scamType", defaultValue: scamType, className: "rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300 focus:outline-none focus:ring-2 focus:ring-primary", children: [_jsx("option", { value: "", children: "All scam types" }), SCAM_TYPES.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id)))] }), _jsx("select", { name: "sort", defaultValue: sort, className: "rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300 focus:outline-none focus:ring-2 focus:ring-primary", children: SORT_OPTIONS.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) }), _jsx("button", { type: "submit", className: "rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Search" }), (query || scamType || sort !== "-createdAt") && (_jsx(Link, { href: "/scams", className: "rounded-lg border border-zinc-300 dark:border-slate-600 px-4 py-2 text-sm text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: "Clear" }))] }), scamType && SCAM_TYPE_LABELS[scamType] && (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { variant: "secondary", className: "text-xs", children: "Filtering by:" }), _jsxs(Link, { href: buildHref({ scamType: "" }), className: "inline-flex items-center gap-1 rounded-full bg-rose-100 dark:bg-rose-900/30 px-2.5 py-0.5 text-xs font-medium text-rose-700 dark:text-rose-300 hover:bg-rose-200 transition-colors", children: [SCAM_TYPE_LABELS[scamType], _jsx("span", { "aria-hidden": "true", children: "\u00D7" })] })] })), result.items.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Shield, { className: "h-12 w-12" }), title: query || scamType
66
+ ? "No verified scammers matched your filters"
67
+ : "No verified scammers in the registry yet", description: query || scamType
68
+ ? "Try a different search or clear your filters."
69
+ : "Verified reports will appear here once reviewed by our moderation team." })) : (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { variant: "secondary", className: "text-sm", children: [result.total, " verified profile", result.total !== 1 ? "s" : "", scamType && SCAM_TYPE_LABELS[scamType]
70
+ ? ` · ${SCAM_TYPE_LABELS[scamType]}`
71
+ : ""] }), _jsx(Grid, { cols: 3, gap: "md", children: result.items.map((scammer) => (_jsx(ScammerCard, { scammer: scammer }, scammer.id))) })] })), totalPages > 1 && (_jsxs(Row, { justify: "center", gap: "sm", className: "flex-wrap", children: [result.page > 1 && (_jsx(Link, { href: buildHref({ page: String(result.page - 1) }), className: "appkit-button appkit-button--outline appkit-button--sm", children: "\u2190 Prev" })), Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
72
+ const p = i + 1;
73
+ return (_jsx(Link, { href: buildHref({ page: String(p) }), className: `appkit-button appkit-button--sm ${p === result.page ? "appkit-button--primary" : "appkit-button--outline"}`, children: p }, p));
74
+ }), result.page < totalPages && (_jsx(Link, { href: buildHref({ page: String(result.page + 1) }), className: "appkit-button appkit-button--outline appkit-button--sm", children: "Next \u2192" }))] }))] }) }) })] }));
42
75
  }
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { FormShell, StepForm, StepFormActions, useFormShell } from "../../shell";
6
+ import { FormShellProvider } from "../../../ui/forms";
6
7
  import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, Text, Toggle } from "../../../ui";
7
8
  import { ImageUpload, MediaUploadField, MediaUploadList, useMediaUpload } from "../../media";
8
9
  import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
@@ -146,6 +147,25 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
146
147
  const [stepError, setStepError] = useState(null);
147
148
  const { isDirty, markDirty, markClean } = useFormShell();
148
149
  const router = useRouter();
150
+ // Auto-save in create mode — debounce 2s on any draft change
151
+ const autoSaveTimer = useRef(null);
152
+ const draftRef = useRef(draft);
153
+ draftRef.current = draft;
154
+ const onSaveRef = useRef(onSave);
155
+ onSaveRef.current = onSave;
156
+ useEffect(() => {
157
+ if (mode !== "create" || !isDirty)
158
+ return;
159
+ if (autoSaveTimer.current)
160
+ clearTimeout(autoSaveTimer.current);
161
+ autoSaveTimer.current = setTimeout(() => {
162
+ const result = onSaveRef.current(draftRef.current);
163
+ if (result && typeof result.catch === "function")
164
+ result.catch(() => { });
165
+ }, 2000);
166
+ return () => { if (autoSaveTimer.current)
167
+ clearTimeout(autoSaveTimer.current); };
168
+ }, [draft, isDirty, mode]);
149
169
  const update = useCallback((partial) => {
150
170
  setDraft((prev) => ({ ...prev, ...partial }));
151
171
  markDirty();
@@ -247,12 +267,16 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
247
267
  await handlePublish();
248
268
  }
249
269
  }, [currentStep, steps, draft, handlePublish]);
270
+ // Step error badges — run each step's validate against current draft
271
+ const stepValidationErrors = useMemo(() => steps.map((s) => (s.validate ? Boolean(s.validate(draft)) : false)),
272
+ // eslint-disable-next-line react-hooks/exhaustive-deps
273
+ [draft]);
250
274
  const breadcrumb = mode === "create" ? `Store / ${listingTypeLabel}s / New` : `Store / ${listingTypeLabel}s / Edit`;
251
275
  const title = mode === "create"
252
276
  ? `New ${listingTypeLabel}`
253
277
  : draft.title ?? `Edit ${listingTypeLabel}`;
254
278
  if (mode === "create") {
255
- return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true }) }));
279
+ return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true, stepErrors: stepValidationErrors }) }) }));
256
280
  }
257
281
  // Edit mode — FormShell with section nav + full form
258
282
  const editSections = [
@@ -263,5 +287,5 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
263
287
  ...(listingType === "digital-code" ? [{ id: "digitalcode", label: "Code Details" }] : []),
264
288
  ...(listingType === "live" ? [{ id: "live", label: "Live Item" }] : []),
265
289
  ];
266
- return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), listingType === "auction" && (_jsxs(Section, { id: "auction", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Auction Settings" }), _jsx(StepAuctionSettings, { values: draft, onChange: update })] })), listingType === "pre-order" && (_jsxs(Section, { id: "preorder", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pre-Order Settings" }), _jsx(StepPreOrderSettings, { values: draft, onChange: update })] })), listingType === "classified" && (_jsxs(Section, { id: "classified", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Meetup Details" }), _jsx(StepClassifiedSettings, { values: draft, onChange: update })] })), listingType === "digital-code" && (_jsxs(Section, { id: "digitalcode", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Code Details" }), _jsx(StepDigitalCodeSettings, { values: draft, onChange: update })] })), listingType === "live" && (_jsxs(Section, { id: "live", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Live Item Details" }), _jsx(StepLiveItemSettings, { values: draft, onChange: update })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)] pt-4", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-xs text-[var(--appkit-color-secondary-text)]", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }));
290
+ return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), listingType === "auction" && (_jsxs(Section, { id: "auction", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Auction Settings" }), _jsx(StepAuctionSettings, { values: draft, onChange: update })] })), listingType === "pre-order" && (_jsxs(Section, { id: "preorder", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pre-Order Settings" }), _jsx(StepPreOrderSettings, { values: draft, onChange: update })] })), listingType === "classified" && (_jsxs(Section, { id: "classified", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Meetup Details" }), _jsx(StepClassifiedSettings, { values: draft, onChange: update })] })), listingType === "digital-code" && (_jsxs(Section, { id: "digitalcode", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Code Details" }), _jsx(StepDigitalCodeSettings, { values: draft, onChange: update })] })), listingType === "live" && (_jsxs(Section, { id: "live", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Live Item Details" }), _jsx(StepLiveItemSettings, { values: draft, onChange: update })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)] pt-4", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-xs text-[var(--appkit-color-secondary-text)]", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
267
291
  }
@@ -23,6 +23,8 @@ export interface StepFormProps<T extends object = Record<string, unknown>> {
23
23
  onStepChange: (step: number) => void;
24
24
  /** Suppress the built-in action bar. Use FormShell's renderBottomBar instead. */
25
25
  hideActions?: boolean;
26
+ /** Per-step error flags — when true, step button shows a red error badge */
27
+ stepErrors?: boolean[];
26
28
  }
27
29
  export interface StepFormActionsProps {
28
30
  currentStep: number;
@@ -36,11 +38,13 @@ export interface StepFormActionsProps {
36
38
  /** Standalone step nav bar — use as `renderBottomBar` in FormShell. */
37
39
  export declare function StepFormActions({ currentStep, totalSteps, onNext, onPrev, completeLabel, isLoading, disabled, }: StepFormActionsProps): import("react/jsx-runtime").JSX.Element;
38
40
  /** Step indicator bar shown above step content. */
39
- export declare function StepIndicator({ steps, currentStep, onStepClick, }: {
41
+ export declare function StepIndicator({ steps, currentStep, onStepClick, stepErrors, }: {
40
42
  steps: {
41
43
  label: string;
42
44
  }[];
43
45
  currentStep: number;
44
46
  onStepClick?: (index: number) => void;
47
+ /** True for each step that has a validation error — renders a red dot badge */
48
+ stepErrors?: boolean[];
45
49
  }): import("react/jsx-runtime").JSX.Element;
46
- export declare function StepForm<T extends object = Record<string, unknown>>({ steps, values, onChange, onComplete, completeLabel, isLoading, formId, currentStep, onStepChange, hideActions, }: StepFormProps<T>): import("react/jsx-runtime").JSX.Element;
50
+ export declare function StepForm<T extends object = Record<string, unknown>>({ steps, values, onChange, onComplete, completeLabel, isLoading, formId, currentStep, onStepChange, hideActions, stepErrors, }: StepFormProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -13,25 +13,28 @@ export function StepFormActions({ currentStep, totalSteps, onNext, onPrev, compl
13
13
  return (_jsxs("div", { className: "flex items-center justify-between gap-2 px-5 py-3 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(_Fragment, { children: !isFirst && onPrev && (_jsx(Button, { variant: "outline", size: "sm", onClick: onPrev, disabled: disabled || isLoading, children: "\u2190 Back" })) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: [currentStep + 1, " / ", totalSteps] }), _jsx(Button, { variant: "primary", size: "sm", onClick: onNext, disabled: disabled || isLoading, isLoading: isLoading && isLast, children: isLast ? completeLabel : nextStepLabel })] })] }));
14
14
  }
15
15
  /** Step indicator bar shown above step content. */
16
- export function StepIndicator({ steps, currentStep, onStepClick, }) {
16
+ export function StepIndicator({ steps, currentStep, onStepClick, stepErrors, }) {
17
17
  return (_jsx("nav", { "aria-label": "Form steps", className: "flex items-center gap-0 mb-6 overflow-x-auto", children: steps.map((step, i) => {
18
18
  const isDone = i < currentStep;
19
19
  const isActive = i === currentStep;
20
+ const hasError = stepErrors?.[i] === true;
20
21
  return (_jsxs("div", { className: "flex items-center gap-0 flex-shrink-0", children: [_jsxs("button", { type: "button", disabled: !isDone && !isActive, onClick: () => isDone && onStepClick?.(i), className: classNames("flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors", isActive
21
22
  ? "font-semibold text-[var(--appkit-color-primary)]"
22
23
  : isDone
23
24
  ? "text-[var(--appkit-color-text-muted)] cursor-pointer hover:text-[var(--appkit-color-text)]"
24
- : "text-[var(--appkit-color-text-faint)] cursor-default"), children: [_jsx("span", { className: classNames("flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold border-2", isActive
25
- ? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
26
- : isDone
27
- ? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
28
- : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-faint)]"), children: isDone ? _jsx(Check, { className: "w-3 h-3" }) : i + 1 }), _jsx("span", { className: "hidden sm:inline", children: step.label })] }), i < steps.length - 1 && (_jsx("div", { className: classNames("w-6 h-px flex-shrink-0 mx-1", i < currentStep
25
+ : "text-[var(--appkit-color-text-faint)] cursor-default"), children: [_jsxs("span", { className: "relative flex-shrink-0", children: [hasError && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-[var(--appkit-color-error)] z-10", "aria-label": "This step has errors" })), _jsx("span", { className: classNames("flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold border-2", hasError
26
+ ? "border-[var(--appkit-color-error)] bg-[var(--appkit-color-error)] text-white"
27
+ : isActive
28
+ ? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
29
+ : isDone
30
+ ? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
31
+ : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-faint)]"), children: isDone && !hasError ? _jsx(Check, { className: "w-3 h-3" }) : hasError ? "!" : i + 1 })] }), _jsx("span", { className: "hidden sm:inline", children: step.label })] }), i < steps.length - 1 && (_jsx("div", { className: classNames("w-6 h-px flex-shrink-0 mx-1", i < currentStep
29
32
  ? "bg-[var(--appkit-color-primary)]"
30
33
  : "bg-[var(--appkit-color-border)]") }))] }, i));
31
34
  }) }));
32
35
  }
33
- export function StepForm({ steps, values, onChange, onComplete, completeLabel = "Publish Now", isLoading = false, formId, currentStep, onStepChange, hideActions = false, }) {
34
- const [stepErrors, setStepErrors] = useState({});
36
+ export function StepForm({ steps, values, onChange, onComplete, completeLabel = "Publish Now", isLoading = false, formId, currentStep, onStepChange, hideActions = false, stepErrors, }) {
37
+ const [fieldErrors, setFieldErrors] = useState({});
35
38
  const [stepError, setStepError] = useState(null);
36
39
  // Persist step to localStorage if formId provided
37
40
  useEffect(() => {
@@ -75,8 +78,8 @@ export function StepForm({ steps, values, onChange, onComplete, completeLabel =
75
78
  }, [currentStep, onStepChange]);
76
79
  const currentStepDef = steps[currentStep];
77
80
  const isLast = currentStep === steps.length - 1;
78
- return (_jsxs("div", { className: "flex flex-col", children: [_jsx(StepIndicator, { steps: steps, currentStep: currentStep, onStepClick: (i) => { if (i < currentStep) {
81
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(StepIndicator, { steps: steps, currentStep: currentStep, stepErrors: stepErrors, onStepClick: (i) => { if (i < currentStep) {
79
82
  setStepError(null);
80
83
  onStepChange(i);
81
- } } }), _jsx("div", { className: "flex-1", children: currentStepDef?.render({ values, onChange, errors: stepErrors }) }), !hideActions && stepError && (_jsx(Text, { className: "mt-3 text-sm text-[var(--appkit-color-error)]", children: stepError })), !hideActions && (_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void goNext(), onPrev: currentStep > 0 ? goPrev : undefined, completeLabel: completeLabel, isLoading: isLoading && isLast, disabled: isLoading }))] }));
84
+ } } }), _jsx("div", { className: "flex-1", children: currentStepDef?.render({ values, onChange, errors: fieldErrors }) }), !hideActions && stepError && (_jsx(Text, { className: "mt-3 text-sm text-[var(--appkit-color-error)]", children: stepError })), !hideActions && (_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void goNext(), onPrev: currentStep > 0 ? goPrev : undefined, completeLabel: completeLabel, isLoading: isLoading && isLast, disabled: isLoading }))] }));
82
85
  }
@@ -19,5 +19,5 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
19
19
  ].join(" "), onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
20
20
  e.preventDefault();
21
21
  onSelect(store.id, !isSelected);
22
- }, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { className: "relative aspect-video w-full overflow-hidden bg-gradient-to-br from-zinc-100 via-zinc-200 to-zinc-300 dark:from-zinc-800 dark:via-zinc-700 dark:to-zinc-900 flex-shrink-0", children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Span, { className: "text-5xl opacity-20 select-none", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Div, { className: "absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/40 to-transparent pointer-events-none" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: "absolute top-2 right-2 flex items-center gap-1 rounded-full bg-black/50 backdrop-blur-sm px-2 py-0.5 text-xs font-semibold text-yellow-300", children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Div, { className: "flex flex-col flex-1 px-4 pb-4", children: [_jsx(Div, { className: "-mt-5 mb-2 flex items-end justify-between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar", className: "h-10 w-10 rounded-lg border-2 border-white dark:border-zinc-800 shadow-md object-cover" })) : (_jsx(Div, { className: "flex h-10 w-10 items-center justify-center rounded-lg border-2 border-white dark:border-zinc-800 bg-primary/10 dark:bg-primary/20 text-base font-bold text-primary shadow-md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `${THEME_CONSTANTS.utilities.textClamp1} text-sm font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-primary transition-colors`, children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: `mt-1 ${THEME_CONSTANTS.utilities.textClamp2} text-xs text-zinc-500 dark:text-zinc-400 flex-1` })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { gap: "sm", className: "mt-2.5 flex-wrap text-xs text-zinc-500 dark:text-zinc-400", children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { className: "flex items-center gap-0.5", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { className: "flex items-center gap-0.5", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Div, { className: "mt-3 pt-2.5 border-t border-zinc-100 dark:border-zinc-800 flex items-center justify-between", children: [_jsxs(Span, { className: "text-xs font-semibold text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
22
+ }, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { className: "relative aspect-video w-full overflow-hidden bg-gradient-to-br from-zinc-100 via-zinc-200 to-zinc-300 dark:from-zinc-800 dark:via-zinc-700 dark:to-zinc-900 flex-shrink-0", children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Span, { className: "text-5xl opacity-20 select-none", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Div, { className: "absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/40 to-transparent pointer-events-none" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: "absolute top-2 right-2 flex items-center gap-1 rounded-full bg-black/50 backdrop-blur-sm px-2 py-0.5 text-xs font-semibold text-yellow-300", children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Div, { className: "flex flex-col flex-1 px-4 pb-4", children: [_jsx(Div, { className: "-mt-5 mb-2 flex items-end justify-between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar", className: "h-10 w-10 rounded-lg border-2 border-white dark:border-zinc-800 shadow-md object-cover" })) : (_jsx(Div, { className: "flex h-10 w-10 items-center justify-center rounded-lg border-2 border-white dark:border-zinc-800 bg-primary/10 dark:bg-primary/20 text-base font-bold text-primary shadow-md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `${THEME_CONSTANTS.utilities.textClamp1} text-sm font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-primary transition-colors`, children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: `mt-1 ${THEME_CONSTANTS.utilities.textClamp2} text-xs text-zinc-500 dark:text-zinc-400 flex-1` })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { gap: "sm", className: "mt-2.5 flex-wrap text-xs text-zinc-500 dark:text-zinc-400", children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { className: "flex items-center gap-0.5", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { className: "flex items-center gap-0.5", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Div, { className: "mt-3 pt-2.5 border-t border-zinc-100 dark:border-zinc-800 flex items-center justify-between", children: [_jsxs(Span, { className: "text-xs font-semibold text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
23
23
  }
@@ -32,7 +32,7 @@ export function StoreAuctionsListing({ storeId, initialData }) {
32
32
  const [view, setView] = useState(table.get("view") || "grid");
33
33
  const localWishlist = useGuestWishlist();
34
34
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "auction").map((i) => i.itemId));
35
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
35
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
36
36
  const openFilters = useCallback(() => {
37
37
  onFilterReset();
38
38
  setFilterOpen(true);
@@ -42,10 +42,9 @@ export function StoreAuctionsListing({ storeId, initialData }) {
42
42
  setFilterOpen(false);
43
43
  }, [onFilterApply]);
44
44
  const resetAll = useCallback(() => {
45
- table.setMany({ q: "", sort: "" });
46
- onFilterClear();
45
+ onResetAll({ q: "", sort: "" });
47
46
  setSearchInput("");
48
- }, [table, onFilterClear]);
47
+ }, [onResetAll]);
49
48
  const hasActiveState = !!table.get("q") ||
50
49
  table.get("sort") !== DEFAULT_SORT ||
51
50
  filterActiveCount > 0;