@mohasinac/appkit 4.64.1 → 4.65.0
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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.js +18 -1
- package/dist/_internal/server/features/search/index-build/sources.js +27 -12
- package/dist/_internal/server/features/seo/sitemap.js +78 -12
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +16 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
- package/dist/_internal/server/features/stores/tabs.js +20 -21
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
- package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
- package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
- package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +8 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +14 -0
- package/dist/features/account/api/[userId]/addresses/route.js +1 -5
- package/dist/features/addresses/repository/addresses.repository.js +1 -1
- package/dist/features/admin/api/bids/route.js +1 -5
- package/dist/features/admin/api/coupons/route.js +1 -5
- package/dist/features/admin/api/products/route.js +1 -5
- package/dist/features/admin/api/reviews/route.js +1 -5
- package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +4 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
- package/dist/features/admin/components/OwnerQuickActions.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +52 -0
- package/dist/features/admin/schemas/firestore.js +20 -0
- package/dist/features/auctions/api/route.js +1 -8
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
- package/dist/features/auth/hooks/useAuth.js +1 -1
- package/dist/features/before-after/api/route.js +1 -5
- package/dist/features/blog/api/route.js +1 -8
- package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
- package/dist/features/blog/components/BlogIndexPageView.js +6 -9
- package/dist/features/cart/actions/cart-actions.js +1 -1
- package/dist/features/cart/api/route.js +1 -5
- package/dist/features/cart/utils/guest-cart.js +1 -5
- package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
- package/dist/features/categories/api/route.js +5 -23
- package/dist/features/categories/components/BundlesListView.d.ts +2 -3
- package/dist/features/categories/components/BundlesListView.js +2 -5
- package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
- package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
- package/dist/features/categories/constants/category-tabs.d.ts +1 -1
- package/dist/features/categories/constants/category-tabs.js +7 -0
- package/dist/features/categories/hooks/useCategories.d.ts +6 -0
- package/dist/features/categories/hooks/useCategories.js +73 -2
- package/dist/features/categories/types/index.d.ts +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
- package/dist/features/collections/api/route.js +1 -5
- package/dist/features/consultation/api/route.js +1 -5
- package/dist/features/contact/email.js +2 -5
- package/dist/features/corporate/api/route.js +1 -5
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
- package/dist/features/events/components/AdminEventEditorView.js +1 -1
- package/dist/features/events/components/EventsListPageView.d.ts +2 -3
- package/dist/features/events/components/EventsListPageView.js +8 -11
- package/dist/features/faq/constants/help-guides.d.ts +8 -5
- package/dist/features/faq/constants/help-guides.js +9 -5
- package/dist/features/filters/AsyncFacetSection.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +2 -2
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/filters/filterUtils.d.ts +0 -2
- package/dist/features/filters/filterUtils.js +0 -4
- package/dist/features/filters/index.d.ts +1 -1
- package/dist/features/filters/index.js +1 -1
- package/dist/features/forms/Checkbox.js +4 -3
- package/dist/features/forms/Input.js +5 -4
- package/dist/features/forms/Radio.js +6 -5
- package/dist/features/forms/Select.js +3 -2
- package/dist/features/forms/Slider.js +3 -2
- package/dist/features/forms/Textarea.js +3 -2
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/index.d.ts +1 -1
- package/dist/features/forms/index.js +1 -1
- package/dist/features/forms/utils.d.ts +0 -2
- package/dist/features/forms/utils.js +0 -4
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/history/utils/guest-history.js +1 -5
- package/dist/features/homepage/components/CustomCardsSection.js +8 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +23 -1
- package/dist/features/layout/AppLayoutShell.js +13 -12
- package/dist/features/layout/BottomNavItem.d.ts +9 -1
- package/dist/features/layout/BottomNavItem.js +18 -4
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
- package/dist/features/media/MediaLightbox.js +3 -5
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
- package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
- package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
- package/dist/features/orders/components/RefundRequestView.js +1 -1
- package/dist/features/parts/schemas/firestore.d.ts +274 -0
- package/dist/features/parts/schemas/firestore.js +19 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
- package/dist/features/pre-orders/api/admin/route.js +1 -5
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/CustomSectionTabContent.js +0 -5
- package/dist/features/products/components/GroupMemberPicker.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -7
- package/dist/features/products/repository/products.repository.js +0 -1
- package/dist/features/promotions/api/route.js +1 -8
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
- package/dist/features/seller/api/coupons/route.js +1 -5
- package/dist/features/seller/api/products/route.js +1 -5
- package/dist/features/seller/components/SellerProductShell.js +1 -2
- package/dist/features/seller/hooks/useSellerListingData.js +0 -30
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
- package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
- package/dist/features/stores/components/StoresIndexPageView.js +6 -9
- package/dist/features/stores/schemas/firestore.d.ts +27 -0
- package/dist/features/stores/schemas/firestore.js +2 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/providers/db-firebase/admin-app-lite.js +14 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
- package/dist/react/hooks/useBrowsePageFlags.js +18 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/security/csp.d.ts +32 -2
- package/dist/security/csp.js +32 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.js +1 -1
- package/dist/ui/components/Chart.js +0 -1
- package/dist/ui/rich-text/RichText.js +1 -3
- package/dist/ui/rich-text/RichTextRenderer.js +1 -3
- package/dist/utils/browser-storage.d.ts +14 -0
- package/dist/utils/browser-storage.js +18 -0
- package/dist/utils/classnames.d.ts +19 -0
- package/dist/utils/classnames.js +21 -0
- package/dist/utils/search-params.d.ts +44 -0
- package/dist/utils/search-params.js +46 -0
- package/dist/utils/sieve-builder.d.ts +13 -0
- package/dist/utils/sieve-builder.js +22 -0
- package/dist/utils/string.formatter.d.ts +12 -0
- package/dist/utils/string.formatter.js +17 -0
- package/package.json +2 -2
|
@@ -13,14 +13,7 @@ import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
|
|
|
13
13
|
import { NextResponse } from "next/server.js";
|
|
14
14
|
import { getProviders } from "../../../contracts/index.js";
|
|
15
15
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
16
|
-
|
|
17
|
-
return url.searchParams.get(key);
|
|
18
|
-
}
|
|
19
|
-
function numParam(url, key, fallback) {
|
|
20
|
-
const v = url.searchParams.get(key);
|
|
21
|
-
const n = v !== null ? Number(v) : NaN;
|
|
22
|
-
return Number.isFinite(n) ? n : fallback;
|
|
23
|
-
}
|
|
16
|
+
import { numParam, param } from "../../../utils/search-params.js";
|
|
24
17
|
// --- GET /api/blog ------------------------------------------------------------
|
|
25
18
|
export async function GET(request) {
|
|
26
19
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface BlogIndexPageViewProps {
|
|
4
|
-
searchParams?:
|
|
4
|
+
searchParams?: SearchParamsRecord;
|
|
5
5
|
}
|
|
6
6
|
export declare function BlogIndexPageView({ searchParams }: BlogIndexPageViewProps): Promise<React.JSX.Element>;
|
|
7
|
-
export {};
|
|
@@ -6,15 +6,12 @@ import { BlogIndexListing } from "./BlogIndexListing.js";
|
|
|
6
6
|
import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
|
|
7
7
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
8
8
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
9
|
-
|
|
10
|
-
const v = params[key];
|
|
11
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
12
|
-
}
|
|
9
|
+
import { firstParam } from "../../../utils/search-params.js";
|
|
13
10
|
export async function BlogIndexPageView({ searchParams = {} }) {
|
|
14
|
-
const sort =
|
|
15
|
-
const page = Number(
|
|
16
|
-
const pageSize = Number(
|
|
17
|
-
const category =
|
|
11
|
+
const sort = firstParam(searchParams, "sort") || BLOG_DEFAULT_PUBLIC_SORT;
|
|
12
|
+
const page = Number(firstParam(searchParams, "page")) || 1;
|
|
13
|
+
const pageSize = Number(firstParam(searchParams, "pageSize")) || 24;
|
|
14
|
+
const category = firstParam(searchParams, "category");
|
|
18
15
|
/*
|
|
19
16
|
* 🛑 `q` MUST be read and forwarded. Without it /blog?q=anything rendered the
|
|
20
17
|
* FULL published list — the same failure the API route's own header records
|
|
@@ -23,7 +20,7 @@ export async function BlogIndexPageView({ searchParams = {} }) {
|
|
|
23
20
|
* `initialData`, and the listing hook sets `staleTime: Infinity` when given
|
|
24
21
|
* SSR data, so the unfiltered first paint is frozen for that query key.
|
|
25
22
|
*/
|
|
26
|
-
const q =
|
|
23
|
+
const q = firstParam(searchParams, "q").trim();
|
|
27
24
|
const sieveResult = await safeRead(() => blogRepository.listPublished({ ...(category ? { category } : {}), ...(q ? { search: q } : {}) }, { sorts: sort, page, pageSize }), { route: "/blog", key: "blogPosts.listPublished", fallback: null });
|
|
28
25
|
// Sandbox posts are public rows by design — not on someone else's first paint.
|
|
29
26
|
const publicSieve = sieveResult
|
|
@@ -11,7 +11,7 @@ import { productRepository } from "../../products/repository/products.repository
|
|
|
11
11
|
import { storeRepository } from "../../stores/repository/store.repository.js";
|
|
12
12
|
import { normalizeListingType } from "../../products/utils/listing-type.js";
|
|
13
13
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
14
|
-
import { CART_LANE, activeLane, canAddNewItems
|
|
14
|
+
import { CART_LANE, activeLane, canAddNewItems } from "../../../_internal/shared/checkout/lanes.js";
|
|
15
15
|
import { CART_MAX_ITEMS } from "../../../constants/limits.js";
|
|
16
16
|
import { getExpandedDecrements, validateCartItemStock, } from "../../../_internal/server/features/checkout/bundle-expansion.js";
|
|
17
17
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { getProviders } from "../../../contracts/index.js";
|
|
13
13
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
14
|
-
|
|
15
|
-
const v = url.searchParams.get(key);
|
|
16
|
-
const n = v !== null ? Number(v) : NaN;
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
14
|
+
import { numParam } from "../../../utils/search-params.js";
|
|
19
15
|
export async function GET(request) {
|
|
20
16
|
try {
|
|
21
17
|
const url = new URL(request.url);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
2
2
|
import { CART_MAX_ITEMS } from "../../../constants/limits.js";
|
|
3
|
+
import { getDefaultStorage } from "../../../utils/browser-storage.js";
|
|
3
4
|
const DEFAULT_GUEST_CART_KEY = process.env.NEXT_PUBLIC_APP_ID
|
|
4
5
|
? `${process.env.NEXT_PUBLIC_APP_ID}_guest_cart`
|
|
5
6
|
: "guest_cart";
|
|
@@ -24,11 +25,6 @@ export class CartFullError extends Error {
|
|
|
24
25
|
this.current = current;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
function getDefaultStorage() {
|
|
28
|
-
if (typeof window === "undefined")
|
|
29
|
-
return null;
|
|
30
|
-
return window.localStorage;
|
|
31
|
-
}
|
|
32
28
|
function readItems(storage, key) {
|
|
33
29
|
if (!storage)
|
|
34
30
|
return [];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Personal Catalogue Repository (Feature B).
|
|
3
3
|
*/
|
|
4
4
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
|
|
5
|
-
import { CATALOGUE_COLLECTION, CATALOGUE_FIELDS, CATALOGUE_TRACKED_FIELDS, CATALOGUE_HISTORY_PII_FIELDS, createCatalogueItemId
|
|
5
|
+
import { CATALOGUE_COLLECTION, CATALOGUE_FIELDS, CATALOGUE_TRACKED_FIELDS, CATALOGUE_HISTORY_PII_FIELDS, createCatalogueItemId } from "../schemas/firestore.js";
|
|
6
6
|
import { withHistory } from "../../../_internal/shared/history/index.js";
|
|
7
7
|
export class CatalogueRepository extends BaseRepository {
|
|
8
8
|
/** Decrypt on read so callers see the shape they wrote. */
|
|
@@ -23,6 +23,8 @@ import { isListingCategory } from "../constants/listing-categories.js";
|
|
|
23
23
|
import { planSearchTxt } from "../../../utils/search-txt-query.js";
|
|
24
24
|
import { matchesAllSearchTerms } from "../../../utils/search-txt.js";
|
|
25
25
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
26
|
+
import { intParamOrNull, param } from "../../../utils/search-params.js";
|
|
27
|
+
import { validateSieveFilters } from "../../../utils/sieve-builder.js";
|
|
26
28
|
const CACHE_CONTROL_PUBLIC = "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400";
|
|
27
29
|
/**
|
|
28
30
|
* Overlay the LIVE rolled-up count onto a projected page of categories.
|
|
@@ -80,30 +82,10 @@ function buildTreeFromFlat(items, rootId) {
|
|
|
80
82
|
}
|
|
81
83
|
return roots.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map(nest);
|
|
82
84
|
}
|
|
83
|
-
function param(url, key) {
|
|
84
|
-
return url.searchParams.get(key);
|
|
85
|
-
}
|
|
86
|
-
function numParam(url, key) {
|
|
87
|
-
const v = url.searchParams.get(key);
|
|
88
|
-
if (v === null)
|
|
89
|
-
return null;
|
|
90
|
-
const n = Number.parseInt(v, 10);
|
|
91
|
-
return Number.isFinite(n) ? n : null;
|
|
92
|
-
}
|
|
93
85
|
const SAFE_CATEGORY_FILTER_FIELDS = new Set([
|
|
94
86
|
"categoryType", "parentIds", "isFeatured", "showOnHomepage",
|
|
95
87
|
"tier", "isActive", "isLeaf", "isBrand", "isSearchable",
|
|
96
88
|
]);
|
|
97
|
-
function validateSieveFilters(raw, allowedFields) {
|
|
98
|
-
return raw
|
|
99
|
-
.split(",")
|
|
100
|
-
.map((c) => c.trim())
|
|
101
|
-
.filter((c) => {
|
|
102
|
-
const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
|
|
103
|
-
return m ? allowedFields.has(m[1].trim()) : false;
|
|
104
|
-
})
|
|
105
|
-
.join(",");
|
|
106
|
-
}
|
|
107
89
|
// --- GET /api/categories ------------------------------------------------------
|
|
108
90
|
export async function GET(request) {
|
|
109
91
|
try {
|
|
@@ -118,8 +100,8 @@ export async function GET(request) {
|
|
|
118
100
|
const flat = param(url, "flat");
|
|
119
101
|
const tree = param(url, "tree");
|
|
120
102
|
const rawFilters = param(url, "filters");
|
|
121
|
-
const tier =
|
|
122
|
-
const pageSizeParam =
|
|
103
|
+
const tier = intParamOrNull(url, "tier");
|
|
104
|
+
const pageSizeParam = intParamOrNull(url, "pageSize");
|
|
123
105
|
/*
|
|
124
106
|
* 🛑 `?page=` MEANS "give me a paged flat list", and reading it fixes two
|
|
125
107
|
* live bugs rather than adding a feature.
|
|
@@ -139,7 +121,7 @@ export async function GET(request) {
|
|
|
139
121
|
* were permanently empty. Same shape as the `useCategoryTree` bug:
|
|
140
122
|
* `isFiltered` did not include the params real callers send.
|
|
141
123
|
*/
|
|
142
|
-
const pageParam =
|
|
124
|
+
const pageParam = intParamOrNull(url, "page");
|
|
143
125
|
const isPaged = pageParam !== null && pageParam > 0;
|
|
144
126
|
/*
|
|
145
127
|
* `?listingOnly=true` drops brand / bundle / sublisting rows.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface BundlesListViewProps {
|
|
4
|
-
searchParams?:
|
|
4
|
+
searchParams?: SearchParamsRecord;
|
|
5
5
|
onBuyNow?: (input: {
|
|
6
6
|
bundleSlug: string;
|
|
7
7
|
quantity: number;
|
|
@@ -15,4 +15,3 @@ export interface BundlesListViewProps {
|
|
|
15
15
|
* Sort UI is owned by `CategoryBundlesListing` (client).
|
|
16
16
|
*/
|
|
17
17
|
export declare function BundlesListView({ searchParams, onBuyNow, }: BundlesListViewProps): Promise<React.JSX.Element>;
|
|
18
|
-
export {};
|
|
@@ -5,11 +5,8 @@ import { AdSlot } from "../../homepage/components/AdSlot.js";
|
|
|
5
5
|
import { CategoryBundlesListing } from "./CategoryBundlesListing.js";
|
|
6
6
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
7
7
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
8
|
+
import { firstParam } from "../../../utils/search-params.js";
|
|
8
9
|
const DEFAULT_LIMIT = 200;
|
|
9
|
-
function sp(params, key) {
|
|
10
|
-
const v = params[key];
|
|
11
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
12
|
-
}
|
|
13
10
|
/**
|
|
14
11
|
* Public `/bundles` index. Bundles live as `categoryType:"bundle"` rows on
|
|
15
12
|
* the categories collection (SB-UNI-D + V), so this fetches via
|
|
@@ -18,7 +15,7 @@ function sp(params, key) {
|
|
|
18
15
|
* Sort UI is owned by `CategoryBundlesListing` (client).
|
|
19
16
|
*/
|
|
20
17
|
export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
|
|
21
|
-
const storeId =
|
|
18
|
+
const storeId = firstParam(searchParams, "storeId");
|
|
22
19
|
const all = await safeRead(() => categoriesRepository.listByType("bundle", {
|
|
23
20
|
activeOnly: true,
|
|
24
21
|
limit: DEFAULT_LIMIT,
|
|
@@ -20,7 +20,6 @@ const __O = {
|
|
|
20
20
|
const PAGE_SIZE = 24;
|
|
21
21
|
function renderCategoryGrid(props) {
|
|
22
22
|
const { isLoading, categories, view, activeSearch, activeTab, brandsOnly } = props;
|
|
23
|
-
const isBrandView = activeTab === "brands" || brandsOnly;
|
|
24
23
|
/* The empty state names the tab the visitor is actually on. It said
|
|
25
24
|
* "categories" on all five, so an empty Parts tab reported the wrong thing. */
|
|
26
25
|
const noun = brandsOnly
|
|
@@ -155,12 +154,21 @@ export function CategoriesIndexListing({ brandsOnly = false }) {
|
|
|
155
154
|
const switchTab = (key) => {
|
|
156
155
|
table.set(TABLE_KEYS.TAB, key);
|
|
157
156
|
};
|
|
158
|
-
const { categories, totalPages, isLoading } = useCategoriesFiltered({
|
|
157
|
+
const { categories, allRows, totalPages, isLoading } = useCategoriesFiltered({
|
|
159
158
|
q: table.get(TABLE_KEYS.QUERY) || undefined,
|
|
160
159
|
isFeatured: table.get(TABLE_KEYS.IS_FEATURED) === "true" || undefined,
|
|
161
160
|
isBrand: isBrandParam,
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
/*
|
|
162
|
+
* The tab partition — applied before pagination inside the hook.
|
|
163
|
+
*
|
|
164
|
+
* 🛑 `useCallback` on `activeTab` is REQUIRED, not hygiene. The hook
|
|
165
|
+
* memoises its filter/sort/slice on this reference; an inline arrow is a
|
|
166
|
+
* new reference every render, so it would recompute on each one. (The
|
|
167
|
+
* converse was the actual bug: the predicate was missing from the hook's
|
|
168
|
+
* dependency list entirely, so a tab change recomputed NOTHING and the
|
|
169
|
+
* previous tab's rows stayed on screen.)
|
|
170
|
+
*/
|
|
171
|
+
predicate: useCallback((c) => matchesCategoryTab(c, activeTab), [activeTab]),
|
|
164
172
|
rootOnly: table.get("rootOnly") === "true" || undefined,
|
|
165
173
|
tier: getNumericParam(table, "tier"),
|
|
166
174
|
minProductCount: getNumericParam(table, "minItemCount"),
|
|
@@ -186,7 +194,41 @@ export function CategoriesIndexListing({ brandsOnly = false }) {
|
|
|
186
194
|
* navigate. That is also why the items carry `onSelect` rather than `href`,
|
|
187
195
|
* and therefore why `linkProps`/`scroll` do not apply here.
|
|
188
196
|
*/
|
|
189
|
-
|
|
197
|
+
/*
|
|
198
|
+
* 🛑 A TAB THAT CAN ONLY EVER BE EMPTY IS THE SAME DEAD FACET AS AN EMPTY CARD.
|
|
199
|
+
*
|
|
200
|
+
* This index already removed its Parts tab for being "the worse half of two
|
|
201
|
+
* better homes", and `category-tabs.ts` records that *"every tab answers a
|
|
202
|
+
* question; 'all' answered none"*. A tab whose answer is always "nothing"
|
|
203
|
+
* answers none either — it is a labelled promise of content that opens on
|
|
204
|
+
* "No characters found".
|
|
205
|
+
*
|
|
206
|
+
* It became load-bearing the moment 320 character rows were seeded `isActive:
|
|
207
|
+
* false`: the type is real, the rows exist and resolve, and NOT ONE is live
|
|
208
|
+
* until a product carries a character slug. Shipping the tab regardless
|
|
209
|
+
* would have put a permanently empty section in the site's main taxonomy
|
|
210
|
+
* index — and then quietly filled itself the day someone flipped a flag,
|
|
211
|
+
* with no second commit needed. That is the behaviour worth having, so the
|
|
212
|
+
* tab is DERIVED from the payload rather than from a list.
|
|
213
|
+
*
|
|
214
|
+
* 🛑 Counted from `allRows` — the whole cached response — never from
|
|
215
|
+
* `categories`, which is this tab's own paginated slice and would make every
|
|
216
|
+
* inactive tab hide itself. Root Cause #72 is the trap here ("a 'hide it when
|
|
217
|
+
* it's empty' rule is only as good as its count map"), and the reason this
|
|
218
|
+
* version is safe is that there IS no map: both the count and the render
|
|
219
|
+
* partition one payload through the same `matchesCategoryTab`, so a tab
|
|
220
|
+
* cannot be uncovered by construction.
|
|
221
|
+
*
|
|
222
|
+
* Two rows that must not be collapsed into the count:
|
|
223
|
+
* - WHILE LOADING, `allRows` is empty, so every tab would vanish and
|
|
224
|
+
* reappear. The bar renders in full until the payload lands.
|
|
225
|
+
* - THE ACTIVE TAB IS NEVER HIDDEN. A direct link to `?tab=characters`
|
|
226
|
+
* must not leave the visitor on a bar with no selected tab; it shows the
|
|
227
|
+
* tab and its honest empty state.
|
|
228
|
+
*/
|
|
229
|
+
const tabItems = CATEGORY_TABS.filter((tab) => isLoading ||
|
|
230
|
+
tab.key === activeTab ||
|
|
231
|
+
allRows.some((c) => matchesCategoryTab(c, tab.key))).map((tab) => ({
|
|
190
232
|
id: tab.key,
|
|
191
233
|
label: tab.label,
|
|
192
234
|
onSelect: () => switchTab(tab.key),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface CategoriesIndexPageViewProps {
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Accepted and ignored. Kept so the consumer page shim compiles
|
|
6
6
|
* either way; remove once no caller passes it.
|
|
7
7
|
*/
|
|
8
|
-
searchParams?:
|
|
8
|
+
searchParams?: SearchParamsRecord;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* The categories index.
|
|
@@ -30,4 +30,3 @@ export interface CategoriesIndexPageViewProps {
|
|
|
30
30
|
* lose (contrast `/products`, where the grid IS in the SSR HTML and must stay).
|
|
31
31
|
*/
|
|
32
32
|
export declare function CategoriesIndexPageView(_props?: CategoriesIndexPageViewProps): Promise<React.JSX.Element>;
|
|
33
|
-
export {};
|
|
@@ -1,39 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
2
|
import { Container, Heading, Main, Section } from "../../../ui/index.js";
|
|
4
3
|
import { AdSlot } from "../../homepage/components/AdSlot.js";
|
|
5
4
|
import { CategoriesIndexListing } from "./CategoriesIndexListing.js";
|
|
6
|
-
function sp(params, key) {
|
|
7
|
-
const v = params[key];
|
|
8
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
9
|
-
}
|
|
10
|
-
function buildCategoryFilters(params) {
|
|
11
|
-
const parts = [];
|
|
12
|
-
const isFeatured = sp(params, "isFeatured");
|
|
13
|
-
if (isFeatured === "true")
|
|
14
|
-
parts.push("isFeatured==true");
|
|
15
|
-
const isBrand = sp(params, "isBrand");
|
|
16
|
-
if (isBrand === "true")
|
|
17
|
-
parts.push("isBrand==true");
|
|
18
|
-
const rootOnly = sp(params, "rootOnly");
|
|
19
|
-
if (rootOnly === "true")
|
|
20
|
-
parts.push("tier==0");
|
|
21
|
-
const minItemCount = sp(params, "minItemCount");
|
|
22
|
-
const maxItemCount = sp(params, "maxItemCount");
|
|
23
|
-
if (minItemCount)
|
|
24
|
-
parts.push(sieveFilter("metrics.totalItemCount", SIEVE_OP.GTE, minItemCount));
|
|
25
|
-
if (maxItemCount)
|
|
26
|
-
parts.push(sieveFilter("metrics.totalItemCount", SIEVE_OP.LTE, maxItemCount));
|
|
27
|
-
const tier = sp(params, "tier");
|
|
28
|
-
if (tier) {
|
|
29
|
-
const values = tier.split("|").filter(Boolean);
|
|
30
|
-
if (values.length === 1)
|
|
31
|
-
parts.push(sieveFilter("tier", SIEVE_OP.EQ, values[0]));
|
|
32
|
-
else if (values.length > 1)
|
|
33
|
-
parts.push(sieveFilter("tier", SIEVE_OP.EQ, values.join("|")));
|
|
34
|
-
}
|
|
35
|
-
return parts.join(",");
|
|
36
|
-
}
|
|
37
5
|
/**
|
|
38
6
|
* The categories index.
|
|
39
7
|
*
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @tag pattern:shared-literal
|
|
40
40
|
* @tag sideEffects:none
|
|
41
41
|
*/
|
|
42
|
-
export type CategoryTabKey = "categories" | "brands" | "franchises" | "bundles";
|
|
42
|
+
export type CategoryTabKey = "categories" | "brands" | "franchises" | "bundles" | "characters";
|
|
43
43
|
export interface CategoryTabDef {
|
|
44
44
|
key: CategoryTabKey;
|
|
45
45
|
label: string;
|
|
@@ -48,6 +48,11 @@ export const CATEGORY_TABS = [
|
|
|
48
48
|
{ key: "categories", label: "Categories" },
|
|
49
49
|
{ key: "brands", label: "Brands" },
|
|
50
50
|
{ key: "franchises", label: "Franchises" },
|
|
51
|
+
/*
|
|
52
|
+
* Who USED the bey. Answers a question none of the others can and the one
|
|
53
|
+
* buyers actually ask — "Tyson's Dragoon", not "Attack Ring MA-04".
|
|
54
|
+
*/
|
|
55
|
+
{ key: "characters", label: "Characters" },
|
|
51
56
|
{ key: "bundles", label: "Bundles" },
|
|
52
57
|
];
|
|
53
58
|
export const DEFAULT_CATEGORY_TAB = "categories";
|
|
@@ -66,6 +71,8 @@ export function matchesCategoryTab(c, tab) {
|
|
|
66
71
|
return type === "brand";
|
|
67
72
|
case "franchises":
|
|
68
73
|
return type === "franchise";
|
|
74
|
+
case "characters":
|
|
75
|
+
return type === "character";
|
|
69
76
|
case "bundles":
|
|
70
77
|
return type === "bundle";
|
|
71
78
|
case "categories":
|
|
@@ -32,12 +32,18 @@ export interface CategoriesFilteredParams {
|
|
|
32
32
|
* Not part of the react-query key, deliberately — the FETCH is the same
|
|
33
33
|
* unfiltered `?flat=true` for every tab, so keying on it would mint five
|
|
34
34
|
* cache entries for one response.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 That applies to the QUERY KEY ONLY. It must still be a dependency of the
|
|
37
|
+
* in-memory filter/sort memo, and omitting it there was a live bug: a tab
|
|
38
|
+
* change altered nothing else the memo watched, so it returned the previous
|
|
39
|
+
* tab's rows. Caller supplies it via `useCallback` on the tab key.
|
|
35
40
|
*/
|
|
36
41
|
predicate?: (c: CategoryItem) => boolean;
|
|
37
42
|
}
|
|
38
43
|
/** API-driven for structured filters; text search + pagination done in hook (Firestore substring limitation). */
|
|
39
44
|
export declare function useCategoriesFiltered(params?: CategoriesFilteredParams): {
|
|
40
45
|
categories: CategoryItem[];
|
|
46
|
+
allRows: CategoryItem[];
|
|
41
47
|
total: number;
|
|
42
48
|
totalPages: number;
|
|
43
49
|
isLoading: boolean;
|
|
@@ -38,8 +38,57 @@ export function useCategoriesFiltered(params = {}) {
|
|
|
38
38
|
staleTime: 2 * 60 * 1000,
|
|
39
39
|
});
|
|
40
40
|
const { page = 1, pageSize = 24, sort = "name", q = "", minProductCount, maxProductCount } = params;
|
|
41
|
+
/*
|
|
42
|
+
* Every LIVE row in the payload — before any tab partition, search or
|
|
43
|
+
* pagination. Exposed because a caller that partitions this response into
|
|
44
|
+
* tabs needs to know whether a tab has anything in it at all, and its own
|
|
45
|
+
* `categories` slice cannot tell it that (it is one tab's page).
|
|
46
|
+
*
|
|
47
|
+
* 🛑 Deliberately NOT narrowed by `q`. A tab bar that loses tabs as the
|
|
48
|
+
* visitor types is worse than one that keeps a tab whose search found
|
|
49
|
+
* nothing — the second has an honest empty state, the first makes the
|
|
50
|
+
* section look deleted.
|
|
51
|
+
*/
|
|
52
|
+
const allRows = useMemo(() => {
|
|
53
|
+
const items = query.data ?? [];
|
|
54
|
+
/*
|
|
55
|
+
* 🛑 AN INACTIVE ROW IS A DEAD FACET, AND UNTIL 2026-10-24 NOTHING ENFORCED IT.
|
|
56
|
+
*
|
|
57
|
+
* `brand-rows.ts` trap 3 states the rule — *"a row with no product is a
|
|
58
|
+
* DEAD FACET, and worse than absent: it renders as a card in the grid that
|
|
59
|
+
* opens an empty page"* — and `franchise-rows.ts` claims its seven inactive
|
|
60
|
+
* rows are therefore *"excluded from the grid and from search"*.
|
|
61
|
+
*
|
|
62
|
+
* **They were not.** `GET /api/categories` builds its Sieve string from
|
|
63
|
+
* `type` / `parentIds` / `isFeatured` / `isBrand` / `showOnHomepage` /
|
|
64
|
+
* `tier` only, and `useCategoriesFiltered` sends nothing but `flat=true`,
|
|
65
|
+
* so no layer ever added an `isActive` clause. All seven inactive
|
|
66
|
+
* franchises have been rendering on the Franchises tab, each one a card
|
|
67
|
+
* opening an empty page. The promise was in a comment; the mechanism did
|
|
68
|
+
* not exist (Root Cause #106).
|
|
69
|
+
*
|
|
70
|
+
* It surfaced when 320 inactive CHARACTER rows were seeded on the strength
|
|
71
|
+
* of that promise — seven dead cards is easy to miss, 320 is not.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 IN MEMORY, AND ONLY ON AN EXPLICIT `false`. Two reasons, both of which
|
|
74
|
+
* a server-side clause would get wrong:
|
|
75
|
+
*
|
|
76
|
+
* - A Firestore inequality EXCLUDES every document that lacks the field.
|
|
77
|
+
* `isActive` is optional on `CategoryDocument`, so pushing this down
|
|
78
|
+
* would hide every row that never set it — which is most of the real
|
|
79
|
+
* taxonomy. That is the documented trap in CLAUDE.md's TS-patterns
|
|
80
|
+
* table, in the one shape that returns a plausible non-empty list.
|
|
81
|
+
* - The same reason `isActive?: boolean` is declared on `CategoryItem` at
|
|
82
|
+
* all: absent must mean "live", never "unknown".
|
|
83
|
+
*
|
|
84
|
+
* ADMIN IS NOT AFFECTED. `/admin/categories` reads this route through
|
|
85
|
+
* `useAdminListingData`, not this hook, and must keep seeing inactive rows
|
|
86
|
+
* — they are what it offers a Status column to flip.
|
|
87
|
+
*/
|
|
88
|
+
return items.filter((c) => c.isActive !== false);
|
|
89
|
+
}, [query.data]);
|
|
41
90
|
const paginated = useMemo(() => {
|
|
42
|
-
let items =
|
|
91
|
+
let items = allRows;
|
|
43
92
|
const lq = q.toLowerCase();
|
|
44
93
|
if (lq)
|
|
45
94
|
items = items.filter((c) => c.name.toLowerCase().includes(lq) || (c.description ?? "").toLowerCase().includes(lq));
|
|
@@ -83,9 +132,31 @@ export function useCategoriesFiltered(params = {}) {
|
|
|
83
132
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
84
133
|
const slice = items.slice((page - 1) * pageSize, page * pageSize);
|
|
85
134
|
return { items: slice, total, totalPages };
|
|
86
|
-
|
|
135
|
+
/*
|
|
136
|
+
* 🛑 `params.predicate` BELONGS IN THIS LIST, and leaving it out was a live
|
|
137
|
+
* bug on the categories index — not a theoretical lint complaint.
|
|
138
|
+
*
|
|
139
|
+
* The predicate IS the tab partition. `switchTab` writes only the `tab` URL
|
|
140
|
+
* param, so on a tab change `q`, `sort`, `page` and `pageSize` are all
|
|
141
|
+
* unchanged, and the request string is identical for every tab except
|
|
142
|
+
* brands (the one that sends `isBrand=true`) — so react-query hands back
|
|
143
|
+
* the SAME `query.data` reference. With the predicate absent from the deps,
|
|
144
|
+
* nothing in them changed, the memo returned its cached slice, and
|
|
145
|
+
* **switching Categories → Franchises → Bundles re-rendered the previous
|
|
146
|
+
* tab's rows.** Only the brands tab worked, by accident, because its query
|
|
147
|
+
* key differs.
|
|
148
|
+
*
|
|
149
|
+
* It is a changing reference by nature, so the call site must `useCallback`
|
|
150
|
+
* it on the tab key — `CategoriesIndexListing` now does. Were it ever
|
|
151
|
+
* passed inline again the memo would recompute every render, which is
|
|
152
|
+
* merely wasteful over ~1,500 in-memory rows and still CORRECT; the
|
|
153
|
+
* previous behaviour was neither.
|
|
154
|
+
*/
|
|
155
|
+
}, [allRows, q, sort, page, pageSize, minProductCount, maxProductCount, params.predicate, params.isBrand]);
|
|
87
156
|
return {
|
|
88
157
|
categories: paginated.items,
|
|
158
|
+
/* Every live row, unpartitioned — see `allRows` above. */
|
|
159
|
+
allRows,
|
|
89
160
|
total: paginated.total,
|
|
90
161
|
totalPages: paginated.totalPages,
|
|
91
162
|
isLoading: query.isLoading,
|
|
@@ -7,7 +7,7 @@ const CLS_CLASSIFIED_BADGE = "inline-block rounded-full bg-cyan-100 dark:bg-cyan
|
|
|
7
7
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
8
8
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
9
9
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
10
|
-
import {
|
|
10
|
+
import { toDescriptionHtml } from "../../../utils/string.formatter.js";
|
|
11
11
|
import { safeDisplayName } from "../../../security/index.js";
|
|
12
12
|
import { Container, Div, GatedPrice, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, SpecTable, } from "../../../ui/index.js";
|
|
13
13
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView.js";
|
|
@@ -25,12 +25,6 @@ import { safeRead } from "../../../errors/safe-read.js";
|
|
|
25
25
|
import { DEFAULT_MIN_OFFER_PERCENT, offersEnabledFor, resolveOfferBounds, } from "../../../_internal/shared/features/offers/config.js";
|
|
26
26
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
27
27
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
28
|
-
function toDescriptionHtml(raw) {
|
|
29
|
-
if (!raw)
|
|
30
|
-
return "";
|
|
31
|
-
const s = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
32
|
-
return normalizeRichTextHtml(s);
|
|
33
|
-
}
|
|
34
28
|
export async function ClassifiedDetailPageView({ slug, initialProduct, renderOfferAction }) {
|
|
35
29
|
const product = initialProduct !== undefined
|
|
36
30
|
? (initialProduct ?? undefined)
|
|
@@ -65,7 +59,7 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
65
59
|
const storeHref = storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null;
|
|
66
60
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
67
61
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
68
|
-
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups
|
|
62
|
+
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups] = await Promise.all([
|
|
69
63
|
computeRelatedItems(product),
|
|
70
64
|
getGroupsWithItemsForProduct(product.id),
|
|
71
65
|
safeRead(() => getSiteSettingsGlobal(), {
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { getProviders } from "../../../contracts/index.js";
|
|
13
13
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
14
|
-
|
|
15
|
-
const v = url.searchParams.get(key);
|
|
16
|
-
const n = v !== null ? Number(v) : NaN;
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
14
|
+
import { numParam } from "../../../utils/search-params.js";
|
|
19
15
|
export async function GET(request) {
|
|
20
16
|
try {
|
|
21
17
|
const url = new URL(request.url);
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { getProviders } from "../../../contracts/index.js";
|
|
13
13
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
14
|
-
|
|
15
|
-
const v = url.searchParams.get(key);
|
|
16
|
-
const n = v !== null ? Number(v) : NaN;
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
14
|
+
import { numParam } from "../../../utils/search-params.js";
|
|
19
15
|
export async function GET(request) {
|
|
20
16
|
try {
|
|
21
17
|
const url = new URL(request.url);
|
|
@@ -18,17 +18,14 @@ import { getProviders } from "../../contracts/index.js";
|
|
|
18
18
|
import { createResendProvider } from "../../providers/email-resend/provider.js";
|
|
19
19
|
import { siteSettingsRepository } from "../admin/repository/site-settings.repository.js";
|
|
20
20
|
import { serverLogger } from "../../monitoring/index.js";
|
|
21
|
-
import { currentYear, formatCurrency
|
|
22
|
-
import { EmailBold, EmailButton, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailRow
|
|
21
|
+
import { currentYear, formatCurrency } from "../../utils/index.js";
|
|
22
|
+
import { EmailBold, EmailButton, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailRow } from "../email/primitives.js";
|
|
23
23
|
function getSiteName() {
|
|
24
24
|
return process.env.NEXT_PUBLIC_SITE_NAME?.trim() || "App";
|
|
25
25
|
}
|
|
26
26
|
function getSiteUrl() {
|
|
27
27
|
return process.env.NEXTAUTH_URL?.trim() || "http://localhost:3000";
|
|
28
28
|
}
|
|
29
|
-
function getSupportEmail() {
|
|
30
|
-
return process.env.EMAIL_SUPPORT?.trim() || "support@example.com";
|
|
31
|
-
}
|
|
32
29
|
/**
|
|
33
30
|
* Resolve an email provider that works in BOTH runtimes.
|
|
34
31
|
*
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { getProviders } from "../../../contracts/index.js";
|
|
13
13
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
14
|
-
|
|
15
|
-
const v = url.searchParams.get(key);
|
|
16
|
-
const n = v !== null ? Number(v) : NaN;
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
14
|
+
import { numParam } from "../../../utils/search-params.js";
|
|
19
15
|
export async function GET(request) {
|
|
20
16
|
try {
|
|
21
17
|
const url = new URL(request.url);
|
|
@@ -7,7 +7,7 @@ const CLS_DIGITALCODE_BADGE = "inline-block rounded-full bg-violet-100 dark:bg-v
|
|
|
7
7
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
8
8
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
9
9
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
10
|
-
import {
|
|
10
|
+
import { toDescriptionHtml } from "../../../utils/string.formatter.js";
|
|
11
11
|
import { safeDisplayName } from "../../../security/index.js";
|
|
12
12
|
import { Div, Container, GatedPrice, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, SpecTable, } from "../../../ui/index.js";
|
|
13
13
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView.js";
|
|
@@ -24,12 +24,6 @@ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPane
|
|
|
24
24
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions.js";
|
|
25
25
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
26
26
|
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
27
|
-
function toDescriptionHtml(raw) {
|
|
28
|
-
if (!raw)
|
|
29
|
-
return "";
|
|
30
|
-
const s = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
31
|
-
return normalizeRichTextHtml(s);
|
|
32
|
-
}
|
|
33
27
|
export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPrimaryActions }) {
|
|
34
28
|
const product = initialProduct !== undefined
|
|
35
29
|
? (initialProduct ?? undefined)
|
|
@@ -503,7 +503,7 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
503
503
|
// Replaces the hand-rolled fieldToStepIndex + goToStep pair. The hook owns
|
|
504
504
|
// the expand-then-scroll ordering a raw setState cannot: a collapsed panel
|
|
505
505
|
// must MOUNT before scrollIntoView and focus can reach the offending field.
|
|
506
|
-
const { openIds, setOpenIds
|
|
506
|
+
const { openIds, setOpenIds } = useSectionFormNav(sections, draft);
|
|
507
507
|
const formContent = (_jsxs(Stack, { gap: "sm", children: [alertSection, _jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: update, onSubmit: () => { saveMutation.mutate(); }, schema: eventDraftSchema, openIds: openIds, onOpenChange: setOpenIds, submitLabel: isEdit ? "Save Changes" : "Create Event", isLoading: isLoading })] }), saveMessage && (_jsx(Alert, { variant: saveMessage.toLowerCase().includes("fail") || saveMessage.toLowerCase().includes("error") ? "error" : "success", title: "Save status", children: saveMessage }))] }));
|
|
508
508
|
if (embedded) {
|
|
509
509
|
return _jsx(Div, { className: `${__O.yAuto} ${__P.p4}`, children: formContent });
|