@mohasinac/appkit 4.56.0 → 4.58.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 +4 -4
- package/dist/_internal/server/features/beys/data.d.ts +44 -0
- package/dist/_internal/server/features/beys/data.js +107 -0
- package/dist/_internal/server/features/beys/og.d.ts +42 -0
- package/dist/_internal/server/features/beys/og.js +89 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
- package/dist/_internal/server/features/checkout/actions.js +117 -5
- package/dist/_internal/server/features/orders/actions.d.ts +30 -0
- package/dist/_internal/server/features/orders/actions.js +103 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +139 -0
- package/dist/_internal/shared/shipping/quote.js +5 -6
- package/dist/client.d.ts +23 -0
- package/dist/client.js +3 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +10 -0
- package/dist/constants/field-names.d.ts +46 -0
- package/dist/constants/field-names.js +46 -0
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/account/components/UserOrdersView.js +25 -3
- package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
- package/dist/features/admin/components/AdminBeysView.js +178 -0
- package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
- package/dist/features/admin/components/AdminPartsView.js +160 -0
- package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
- package/dist/features/admin/components/OwnerQuickActions.js +83 -8
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
- package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
- package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +12 -1
- package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
- package/dist/features/beys/components/BeyDetailPageView.js +98 -0
- package/dist/features/beys/schemas/firestore.d.ts +417 -0
- package/dist/features/beys/schemas/firestore.js +65 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +4 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
- package/dist/features/categories/components/CategoryGrid.js +24 -2
- package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/faq/components/FAQPageContent.js +17 -4
- package/dist/features/faq/constants/help-guides.d.ts +36 -0
- package/dist/features/faq/constants/help-guides.js +64 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
- package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +15 -0
- package/dist/features/parts/components/PartDetailPageView.js +5 -3
- package/dist/features/parts/components/PartsSearchBox.js +79 -4
- package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
- package/dist/features/parts/schemas/admin-edit.js +89 -0
- package/dist/features/parts/schemas/firestore.d.ts +2 -1
- package/dist/features/points/repository/points.repository.js +5 -5
- package/dist/features/points/schemas/firestore.d.ts +16 -0
- package/dist/features/points/schemas/firestore.js +17 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
- package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
- package/dist/features/products/components/ProductGrid.js +18 -2
- package/dist/features/products/schemas/firestore.d.ts +18 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
- package/dist/features/seller/components/SellerProductShell.js +31 -3
- package/dist/features/stores/components/StoreHeader.js +3 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +17 -1
- package/dist/next/routing/route-map.js +39 -0
- package/dist/schemas/registry.d.ts +8 -8
- package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
- package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
- package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
- package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
- package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
- package/dist/seed/_helpers/category-models.d.ts +9 -2
- package/dist/seed/_helpers/category-models.js +19 -1368
- package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
- package/dist/seed/_helpers/category-parts.js +39 -39
- package/dist/seed/bey-products-seed-data.d.ts +18 -0
- package/dist/seed/bey-products-seed-data.js +18011 -0
- package/dist/seed/beys/chunk0.d.ts +17 -0
- package/dist/seed/beys/chunk0.js +36733 -0
- package/dist/seed/beys/chunk1.d.ts +17 -0
- package/dist/seed/beys/chunk1.js +35728 -0
- package/dist/seed/beys/chunk2.d.ts +17 -0
- package/dist/seed/beys/chunk2.js +35857 -0
- package/dist/seed/beys/chunk3.d.ts +17 -0
- package/dist/seed/beys/chunk3.js +34476 -0
- package/dist/seed/beys-seed-data.d.ts +18 -0
- package/dist/seed/beys-seed-data.js +11 -0
- package/dist/seed/homepage-sections-seed-data.js +16 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/media-assets-seed-data.js +4 -4
- package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk0.js +7262 -0
- package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk1.js +7290 -0
- package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk10.js +7290 -0
- package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk11.js +7005 -0
- package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk12.js +7002 -0
- package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk13.js +7002 -0
- package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk14.js +7002 -0
- package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk15.js +7002 -0
- package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk16.js +7002 -0
- package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk17.js +7002 -0
- package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk18.js +4398 -0
- package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk2.js +7268 -0
- package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk3.js +7264 -0
- package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk4.js +7268 -0
- package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk5.js +7258 -0
- package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk6.js +7268 -0
- package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk7.js +7311 -0
- package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk8.js +7304 -0
- package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk9.js +7309 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
- package/dist/seed/products-catalogue-seed-data.js +58 -86382
- package/dist/seed/seed-purpose.js +9 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +19 -0
- package/dist/styles.css +468 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PosterCard.d.ts +99 -0
- package/dist/ui/components/PosterCard.js +109 -0
- package/dist/ui/components/PosterCard.style.css +466 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +108 -1
|
@@ -9,7 +9,7 @@ import { MediaImage } from "../../media/MediaImage.js";
|
|
|
9
9
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
10
10
|
import { computeBundleDiscount } from "../../../_internal/shared/features/categories/bundle-pricing.js";
|
|
11
11
|
import { CategoryGrid } from "./CategoryGrid.js";
|
|
12
|
-
import {
|
|
12
|
+
import { RecordOwnerQuickActions } from "../../../features/admin/components/RecordOwnerQuickActions.js";
|
|
13
13
|
const __O = {
|
|
14
14
|
hidden: "overflow-hidden",
|
|
15
15
|
};
|
|
@@ -26,7 +26,7 @@ export function BundleDetailView({ bundle, members = [], onBuyNow, onAddToCart,
|
|
|
26
26
|
? formatCurrency(bundle.bundlePrice, "INR")
|
|
27
27
|
: BUNDLE_COPY.detail.priceFallback;
|
|
28
28
|
const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
|
|
29
|
-
return (_jsxs(Main, { children: [_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "lg", direction: "md-row", align: "start", children: [_jsx(Div, { className: "w-full md:w-2/5", children: members.length > 0 ? (_jsx(BundleCollage, { members: members })) : (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, rounded: "2xl", surface: "subtle", children: _jsx(Div, { className: "h-full w-full", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "hero", fallback: PLACEHOLDER_EMOJI }) }) })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(
|
|
29
|
+
return (_jsxs(Main, { children: [_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "lg", direction: "md-row", align: "start", children: [_jsx(Div, { className: "w-full md:w-2/5", children: members.length > 0 ? (_jsx(BundleCollage, { members: members })) : (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, rounded: "2xl", surface: "subtle", children: _jsx(Div, { className: "h-full w-full", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "hero", fallback: PLACEHOLDER_EMOJI }) }) })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(RecordOwnerQuickActions, { resource: "BUNDLE", recordId: String(bundle.id), placement: "detail", recordLabel: String(bundle.name) }), _jsx(Heading, { level: 1, size: "3xl", weight: "semibold", color: "primary", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Text, { size: "lg", weight: "bold", children: _jsx(GatedPrice, { label: "Sign in to see the bundle price", children: priceLabel }) }), discount && (
|
|
30
30
|
// Both the struck-through total and the "N% off" badge are
|
|
31
31
|
// derived from the bundle price, so they disclose it even
|
|
32
32
|
// though the badge carries no currency symbol.
|
|
@@ -5,7 +5,8 @@ import { categoryCrumbs, withTrailHead } from "../utils/breadcrumb-chain.js";
|
|
|
5
5
|
import { ownTotalFor, descendantTotalFor } from "../schemas/category-counts.js";
|
|
6
6
|
import { categoriesRepository, productRepository, storeRepository, } from "../../../repositories/index.js";
|
|
7
7
|
import { Container, Div, Heading, ImageMosaic, Main, MOSAIC_MIN, Section, Span, SplitTile, Stack, Text } from "../../../ui/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
9
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
9
10
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
10
11
|
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
11
12
|
import { resolveTaxonomyVisual } from "../utils/taxonomy-visual.js";
|
|
@@ -18,6 +19,7 @@ import { LastCategoryBeacon } from "./LastCategoryBeacon.js";
|
|
|
18
19
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel.js";
|
|
19
20
|
import { ModelCompositionPanel } from "../../parts/components/ModelCompositionPanel.js";
|
|
20
21
|
import { getCompositionsForCategory } from "../../../_internal/server/features/parts/data.js";
|
|
22
|
+
import { getBeysForCategory } from "../../../_internal/server/features/beys/data.js";
|
|
21
23
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
22
24
|
import { getGroupsForCategory } from "../../../_internal/server/features/grouped/data.js";
|
|
23
25
|
import { listingTabCounts, } from "../../../_internal/server/features/products/listing-tab-counts.js";
|
|
@@ -158,7 +160,7 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
158
160
|
// tree deeper than a couple of levels would have turned into a silently blank
|
|
159
161
|
// page rather than an error (Root Cause #59).
|
|
160
162
|
const categoriesIn = category?.id ? [category.id] : null;
|
|
161
|
-
const [productsResult, tabCounts, bundlesResult, childCategories, rootSiblingCategories, groupedListings, settings, liveCounts, compositions] = await Promise.all([
|
|
163
|
+
const [productsResult, tabCounts, bundlesResult, childCategories, rootSiblingCategories, groupedListings, settings, liveCounts, compositions, pairedBeys] = await Promise.all([
|
|
162
164
|
categoriesIn
|
|
163
165
|
? safeRead(() => productRepository.list({
|
|
164
166
|
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "standard")),
|
|
@@ -273,6 +275,29 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
273
275
|
fallback: [],
|
|
274
276
|
})
|
|
275
277
|
: Promise.resolve([]),
|
|
278
|
+
/*
|
|
279
|
+
* B5 — the bey(s) this leaf names, so the reference page is REACHABLE.
|
|
280
|
+
*
|
|
281
|
+
* 🛑 `ROUTES.PUBLIC.BEY_DETAIL` existed with ZERO call sites: 1,988 bey
|
|
282
|
+
* pages were built, routable and unreachable from anywhere in the product.
|
|
283
|
+
* The link already ran the other way (`/beys/{slug}` -> its category leaf),
|
|
284
|
+
* so this closes the loop rather than inventing an edge.
|
|
285
|
+
*
|
|
286
|
+
* Same shape as the compositions read directly above — one indexed
|
|
287
|
+
* equality against this category's OWN id, in the SAME `Promise.all` so it
|
|
288
|
+
* costs no extra round trip, wrapped in `safeRead` so a missing index
|
|
289
|
+
* records a DEGRADED_READ instead of taking the page down.
|
|
290
|
+
*
|
|
291
|
+
* A line or generation node matches nothing and renders nothing, which is
|
|
292
|
+
* correct: "Burst GT" is not a bey, the 1,858 leaves under it are.
|
|
293
|
+
*/
|
|
294
|
+
category?.id
|
|
295
|
+
? safeRead(() => getBeysForCategory(category.id), {
|
|
296
|
+
route: "/categories/[slug]",
|
|
297
|
+
key: "beys.beysForCategory",
|
|
298
|
+
fallback: [],
|
|
299
|
+
})
|
|
300
|
+
: Promise.resolve([]),
|
|
276
301
|
]);
|
|
277
302
|
// 🛑 SAME TIER ONLY. `rootSiblingCategories` spans the whole root subtree —
|
|
278
303
|
// siblings AND cousins across every depth — so without a tier check this
|
|
@@ -573,7 +598,15 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
573
598
|
const ldFaq = category?.faqs && category.faqs.length > 0
|
|
574
599
|
? faqJsonLd(category.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
575
600
|
: null;
|
|
576
|
-
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), category?.id && (_jsx(
|
|
601
|
+
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), category?.id && (_jsx(RecordOwnerQuickActions, { resource: "CATEGORY", recordId: category.id, placement: "detail",
|
|
602
|
+
/*
|
|
603
|
+
* Same flag the rest of this hero branches on. Without it the
|
|
604
|
+
* pencil renders `ghost` (zinc-600) on the dark cover and is
|
|
605
|
+
* invisible — see Root Cause #67 and `OwnerQuickActions.onMedia`.
|
|
606
|
+
*/
|
|
607
|
+
onMedia: hasCover, className: "mb-3", recordLabel: category.name ?? slug })), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), pairedBeys.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: pairedBeys.length === 1 ? "Specifications" : "Releases" }), _jsx(Stack, { gap: "xs", className: "min-w-0", children: pairedBeys.map((bey) => (_jsxs(Row, { gap: "sm", align: "center", wrap: true, className: "min-w-0", children: [_jsx(TextLink, { href: String(ROUTES.PUBLIC.BEY_DETAIL(bey.slug)), children: bey.name }), [bey.system, bey.productCodes?.[0]]
|
|
608
|
+
.filter(Boolean)
|
|
609
|
+
.map((bit) => (_jsx(Text, { size: "sm", color: "muted", children: String(bit) }, String(bit))))] }, bey.id))) })] }) }) })), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
|
|
577
610
|
if (category?.tier !== 0)
|
|
578
611
|
return null;
|
|
579
612
|
const branches = childCategories.filter((c) => c.tier === 1);
|
|
@@ -5,6 +5,7 @@ import { Div, Grid, Row, Span, Stack, Text } from "../../../ui/index.js";
|
|
|
5
5
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
7
7
|
import { ImageMosaic, MOSAIC_MIN } from "../../../ui/components/ImageMosaic.js";
|
|
8
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
8
9
|
const __P = {
|
|
9
10
|
p3: "p-[var(--appkit-space-3)]",
|
|
10
11
|
};
|
|
@@ -35,13 +36,34 @@ export function CategoryCard({ category, href, onClick, className = "", renderOw
|
|
|
35
36
|
const card = (_jsx(Div, { role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
36
37
|
? (e) => (e.key === "Enter" || e.key === " ") && onClick(category)
|
|
37
38
|
: undefined, onClick: onClick ? () => onClick(category) : undefined, className: cardClass, children: inner }));
|
|
38
|
-
|
|
39
|
+
/*
|
|
40
|
+
* 🛑 The quick actions are the DEFAULT, not an opt-in.
|
|
41
|
+
*
|
|
42
|
+
* This was `if (!renderOwnerActions) return card;` and NOTHING in either
|
|
43
|
+
* repo ever passed `renderOwnerActions` — measured across all nine card
|
|
44
|
+
* components that declare it. So the pencil was unreachable on every grid in
|
|
45
|
+
* the app, and the slot read as wired because the prop, the docstring and
|
|
46
|
+
* the positioning were all present. Root Cause #8's shape: a slot shell
|
|
47
|
+
* rendered with zero render props.
|
|
48
|
+
*
|
|
49
|
+
* A card is resource-specific by construction — a CategoryCard is always a
|
|
50
|
+
* category — so it can name its own resource and needs no threading through
|
|
51
|
+
* the dozens of listing views that mount it. `RecordOwnerQuickActions` is a
|
|
52
|
+
* Client Component receiving only strings, which is the ordinary RSC
|
|
53
|
+
* boundary; the thing that cannot cross it is a FUNCTION prop, which is why
|
|
54
|
+
* the detail views could not pass `onDelete` directly.
|
|
55
|
+
*
|
|
56
|
+
* The override seam stays for a caller that genuinely needs different
|
|
57
|
+
* actions.
|
|
58
|
+
*/
|
|
59
|
+
const ownerActions = renderOwnerActions ? (renderOwnerActions()) : category.id ? (_jsx(RecordOwnerQuickActions, { resource: "CATEGORY", recordId: String(category.id), recordLabel: category.name, placement: "card" })) : null;
|
|
60
|
+
if (!ownerActions)
|
|
39
61
|
return card;
|
|
40
62
|
/*
|
|
41
63
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
42
64
|
* beats the featured dot at `left-2 top-2`.
|
|
43
65
|
*/
|
|
44
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children:
|
|
66
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
|
|
45
67
|
}
|
|
46
68
|
export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel = "No categories found", className = "", }) {
|
|
47
69
|
if (categories.length === 0) {
|
|
@@ -9,6 +9,7 @@ import { Heading, Text } from "../../../ui/components/Typography.js";
|
|
|
9
9
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
10
10
|
import { ProductGrid } from "../../products/components/ProductGrid.js";
|
|
11
11
|
import { Alert } from "../../../ui/components/Alert.js";
|
|
12
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
12
13
|
const PAGE_SIZE = 24;
|
|
13
14
|
export async function FranchiseDetailPageView({ slug, franchise, }) {
|
|
14
15
|
const result = await safeRead(() => listPublicProducts({
|
|
@@ -37,7 +38,7 @@ export async function FranchiseDetailPageView({ slug, franchise, }) {
|
|
|
37
38
|
const products = rows.map((r) => toProductItem(r));
|
|
38
39
|
const total = reportedTotal ?? products.length;
|
|
39
40
|
const cover = franchise.display?.coverImage;
|
|
40
|
-
return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsxs(Row, { align: "center", gap: "comfortable", className: "mb-8", children: [cover ? (_jsx(MediaImage, { src: cover, alt: franchise.name ?? slug, size: "thumbnail", rounded: "lg" })) : null, _jsxs(Stack, { gap: "dense", children: [_jsx(Heading, { level: 1, color: "primary", size: "3xl", weight: "semibold", children: franchise.name }), franchise.description ? (_jsx(Text, { color: "muted", className: "max-w-2xl", children: franchise.description })) : null] })] }), franchise.isActive === false ? (_jsxs(Alert, { variant: "info", className: "mb-6", children: ["We are not stocking ", franchise.name, " yet. This page is here so the link works \u2014 check back, or browse what we do carry."] })) : null, readFailed ? (_jsxs(Alert, { variant: "warning", children: ["We could not load ", franchise.name, " listings just now. Please try again."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "muted", className: "mb-4", children: total === 0
|
|
41
|
+
return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsxs(Row, { align: "center", gap: "comfortable", className: "mb-8", children: [cover ? (_jsx(MediaImage, { src: cover, alt: franchise.name ?? slug, size: "thumbnail", rounded: "lg" })) : null, _jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Heading, { level: 1, color: "primary", size: "3xl", weight: "semibold", children: franchise.name }), franchise.id ? (_jsx(RecordOwnerQuickActions, { resource: "FRANCHISE", recordId: franchise.id, placement: "detail", recordLabel: franchise.name ?? slug })) : null] }), franchise.description ? (_jsx(Text, { color: "muted", className: "max-w-2xl", children: franchise.description })) : null] })] }), franchise.isActive === false ? (_jsxs(Alert, { variant: "info", className: "mb-6", children: ["We are not stocking ", franchise.name, " yet. This page is here so the link works \u2014 check back, or browse what we do carry."] })) : null, readFailed ? (_jsxs(Alert, { variant: "warning", children: ["We could not load ", franchise.name, " listings just now. Please try again."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "muted", className: "mb-4", children: total === 0
|
|
41
42
|
? "No listings yet."
|
|
42
43
|
: `${total}${truncated ? "+" : ""} listing${total === 1 ? "" : "s"}` }), _jsx(ProductGrid, { products: products, emptyLabel: "No listings yet." })] }))] }) }) }));
|
|
43
44
|
}
|
|
@@ -8,6 +8,7 @@ import { EVENT_FIELDS } from "../schemas/index.js";
|
|
|
8
8
|
import { EventStatusBadge } from "./EventStatusBadge.js";
|
|
9
9
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
10
10
|
import { useLongPress } from "../../../react/hooks/useLongPress.js";
|
|
11
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
11
12
|
const __P = {
|
|
12
13
|
p4: "p-[var(--appkit-space-4)]",
|
|
13
14
|
};
|
|
@@ -34,11 +35,23 @@ export function EventCard({ event, labels = {}, onParticipate, className = "", s
|
|
|
34
35
|
const detailHref = String(ROUTES.PUBLIC.EVENT_DETAIL(event.slug ?? event.id));
|
|
35
36
|
const card = (_jsxs(Stack, { as: "article", border: "default", rounded: "xl", shadow: "hover-md", gap: "none", className: `group relative h-full overflow-hidden bg-[var(--appkit-color-surface)] ${isSelected ? "border-primary outline outline-2 outline-primary" : " "} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(event.id, !isSelected); }, label: isSelected ? "Deselect event" : "Select event", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsx(Link, { href: detailHref, className: "block flex-shrink-0", children: event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { className: `relative aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Row, { surface: "muted", className: "aspect-video", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-40", size: "5xl", "aria-hidden": "true", children: TYPE_ICONS[event.type] }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { className: "mb-2", align: "start", justify: "between", gap: "sm", children: [_jsx(Span, { size: "lg", "aria-hidden": "true", children: TYPE_ICONS[event.type] }), _jsx(EventStatusBadge, { status: event.status })] }), _jsx(Link, { href: detailHref, className: "block", children: _jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)] leading-snug mb-1 group-hover:text-primary transition-colors", size: "base", weight: "semibold", children: safeTitle }) }), _jsx(RichText, { html: normalizeRichTextHtml(event.description ?? ""), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mb-3 line-clamp-3 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]" }), _jsxs(Row, { color: "muted", textSize: "xs", className: "mb-3 mt-auto", align: "center", justify: "between", children: [event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE &&
|
|
36
37
|
daysLeft > 0 && _jsxs(Span, { children: ["\u23F1 ", daysLeft, "d remaining"] }), _jsxs(Span, { children: ["\uD83D\uDC65 ", event.stats.totalEntries, " ", labels.entries ?? "entries"] })] }), event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE && onParticipate ? (_jsx(Button, { rounded: "lg", type: "button", onClick: () => onParticipate(event), className: "w-full bg-primary py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-white transition-colors hover:bg-primary-600", children: labels.participate ?? "Participate" })) : (_jsxs(TextLink, { rounded: "lg", paddingX: "sm", paddingY: "xs", href: detailHref, layout: "inline-flex", align: "center", justify: "center", gap: "xs", className: "w-full border border-[var(--appkit-color-border)] transition-colors hover:bg-[var(--appkit-color-bg)]", color: "primary", size: "sm", weight: "medium", children: [labels.viewDetails ?? "View details", " \u2192"] }))] })] }));
|
|
37
|
-
|
|
38
|
+
/*
|
|
39
|
+
* 🛑 Default, not opt-in. Nothing in either repo ever passed
|
|
40
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
41
|
+
* mounts this card (Root Cause #8 — a slot shell rendered with zero render
|
|
42
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
43
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
44
|
+
*/
|
|
45
|
+
const ownerActions = renderOwnerActions
|
|
46
|
+
? renderOwnerActions()
|
|
47
|
+
: event.id
|
|
48
|
+
? (_jsx(RecordOwnerQuickActions, { resource: "EVENT", recordId: String(event.id), recordLabel: event.title, placement: "card" }))
|
|
49
|
+
: null;
|
|
50
|
+
if (!ownerActions)
|
|
38
51
|
return card;
|
|
39
52
|
/*
|
|
40
53
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
41
54
|
* beats this card's own overlays, which all sit at `z-10`.
|
|
42
55
|
*/
|
|
43
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children:
|
|
56
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
|
|
44
57
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Alert, Div, EmptyState, Heading, Input, SlottedListingView, Text } from "../../../ui/index.js";
|
|
3
|
+
import { Alert, Div, EmptyState, Heading, Input, Row, SlottedListingView, Text, TextLink } from "../../../ui/index.js";
|
|
4
4
|
import { sortBy } from "../../../constants/sort.js";
|
|
5
5
|
import { FAQCategorySidebar } from "./FAQCategorySidebar.js";
|
|
6
6
|
import { FAQSortDropdown } from "./FAQSortDropdown.js";
|
|
7
7
|
import { FAQAccordion } from "./FAQAccordion.js";
|
|
8
8
|
import { ContactCTA } from "./ContactCTA.js";
|
|
9
9
|
import { useFaqList } from "../hooks/useFaqList.js";
|
|
10
|
+
import { helpGuideFor } from "../constants/help-guides.js";
|
|
10
11
|
export function FAQPageContent({ initialCategory = "all", categories, routeHelpers, labels, contact, searchValue, onSearchChange, sortOption, onSortChange, page = 1, pageSize = 100, renderPagination, renderMobileCategoryTabs, renderAccordion, }) {
|
|
11
12
|
const selectedCategory = initialCategory;
|
|
12
13
|
const sorts = sortOption === "helpful"
|
|
@@ -49,9 +50,21 @@ export function FAQPageContent({ initialCategory = "all", categories, routeHelpe
|
|
|
49
50
|
allFaqs: labels.allFaqs,
|
|
50
51
|
stillHaveQuestions: labels.stillHaveQuestions,
|
|
51
52
|
contactSupport: labels.contactSupport,
|
|
52
|
-
} }) }), _jsxs(Div, { className: "col-span-1 lg:col-span-8 xl:col-span-9", children: [_jsx(SlottedListingView, { portal: "public", inlineToolbar: true, className: "space-y-6", renderHeader: () =>
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
} }) }), _jsxs(Div, { className: "col-span-1 lg:col-span-8 xl:col-span-9", children: [_jsx(SlottedListingView, { portal: "public", inlineToolbar: true, className: "space-y-6", renderHeader: () => {
|
|
54
|
+
/*
|
|
55
|
+
* The fuller answer, next to the short ones.
|
|
56
|
+
*
|
|
57
|
+
* Only on a chosen category: on "all" there is no single guide
|
|
58
|
+
* to offer, and a link to the Help Centre index here would
|
|
59
|
+
* duplicate the footer's for no gain. `helpGuideFor` falls back
|
|
60
|
+
* to the index for the two categories that span the product, so
|
|
61
|
+
* a selected category always has somewhere honest to point.
|
|
62
|
+
*/
|
|
63
|
+
const guide = selectedCategory !== "all" ? helpGuideFor(selectedCategory) : null;
|
|
64
|
+
return (_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", className: "min-w-0", children: [labels.resultCount(total), selectedCategory !== "all"
|
|
65
|
+
? ` ${labels.inCategory(categories.find((c) => c.key === selectedCategory)?.label ?? selectedCategory)}`
|
|
66
|
+
: ""] }), guide && (_jsxs(TextLink, { href: guide.path, size: "sm", variant: "muted", className: "flex-shrink-0", children: [guide.label, " \u2192"] }))] }));
|
|
67
|
+
}, renderSearch: () => (_jsx(Input, { value: searchValue, onChange: (event) => onSearchChange(event.target.value), placeholder: labels.searchPlaceholder })), renderSort: () => (_jsx(FAQSortDropdown, { selectedSort: sortOption, onSortChange: onSortChange, labels: {
|
|
55
68
|
label: labels.sortLabel,
|
|
56
69
|
helpful: labels.sortHelpful,
|
|
57
70
|
newest: labels.sortNewest,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FAQCategory } from "../types/index";
|
|
2
|
+
/**
|
|
3
|
+
* The Help Centre guide that answers a FAQ category at length.
|
|
4
|
+
*
|
|
5
|
+
* A FAQ is a short answer to one question; a help guide is the surrounding
|
|
6
|
+
* explanation. They were written as separate surfaces and never linked —
|
|
7
|
+
* measured 2026-10-20, the FAQ seed contained **zero** `/help/` links across
|
|
8
|
+
* 91 entries — so a reader who wanted the fuller version had to already know
|
|
9
|
+
* the Help Centre existed and find the right page in it.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 `Record<FAQCategory, …>`, deliberately, so adding a tenth category is a
|
|
12
|
+
* COMPILE error rather than a category that silently offers no guide. This is
|
|
13
|
+
* the same decision as `ALL_LISTING_TYPES_MAP` and `EMAIL_ELIGIBLE_TYPES`, and
|
|
14
|
+
* the reason is Root Cause #61: every hand-written enumeration of a union in
|
|
15
|
+
* this codebase has eventually fallen behind it.
|
|
16
|
+
*
|
|
17
|
+
* `null` is a real answer and not a gap. `technical_support` and `general`
|
|
18
|
+
* span the whole product, so pointing them at one guide would be arbitrary —
|
|
19
|
+
* they get the Help Centre index instead, via `HELP_INDEX_PATH`. Writing a
|
|
20
|
+
* guide to point them at later is a content decision, not a missing entry.
|
|
21
|
+
*
|
|
22
|
+
* 🛑 Paths, not `ROUTES`. The Help Centre pages are consumer routes under
|
|
23
|
+
* `src/app/[locale]/help/`, and `ROUTES` is appkit's own map — adding them
|
|
24
|
+
* there would make appkit declare pages it does not own. They are stable,
|
|
25
|
+
* linked from the footer, and covered by `audit-nav-page-wiring`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HELP_INDEX_PATH: "/help";
|
|
28
|
+
export declare const FAQ_CATEGORY_HELP_GUIDE: Record<FAQCategory, {
|
|
29
|
+
path: string;
|
|
30
|
+
label: string;
|
|
31
|
+
} | null>;
|
|
32
|
+
/** The guide for a category, or the Help Centre index when none is specific. */
|
|
33
|
+
export declare function helpGuideFor(category: FAQCategory): {
|
|
34
|
+
path: string;
|
|
35
|
+
label: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Help Centre guide that answers a FAQ category at length.
|
|
3
|
+
*
|
|
4
|
+
* A FAQ is a short answer to one question; a help guide is the surrounding
|
|
5
|
+
* explanation. They were written as separate surfaces and never linked —
|
|
6
|
+
* measured 2026-10-20, the FAQ seed contained **zero** `/help/` links across
|
|
7
|
+
* 91 entries — so a reader who wanted the fuller version had to already know
|
|
8
|
+
* the Help Centre existed and find the right page in it.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 `Record<FAQCategory, …>`, deliberately, so adding a tenth category is a
|
|
11
|
+
* COMPILE error rather than a category that silently offers no guide. This is
|
|
12
|
+
* the same decision as `ALL_LISTING_TYPES_MAP` and `EMAIL_ELIGIBLE_TYPES`, and
|
|
13
|
+
* the reason is Root Cause #61: every hand-written enumeration of a union in
|
|
14
|
+
* this codebase has eventually fallen behind it.
|
|
15
|
+
*
|
|
16
|
+
* `null` is a real answer and not a gap. `technical_support` and `general`
|
|
17
|
+
* span the whole product, so pointing them at one guide would be arbitrary —
|
|
18
|
+
* they get the Help Centre index instead, via `HELP_INDEX_PATH`. Writing a
|
|
19
|
+
* guide to point them at later is a content decision, not a missing entry.
|
|
20
|
+
*
|
|
21
|
+
* 🛑 Paths, not `ROUTES`. The Help Centre pages are consumer routes under
|
|
22
|
+
* `src/app/[locale]/help/`, and `ROUTES` is appkit's own map — adding them
|
|
23
|
+
* there would make appkit declare pages it does not own. They are stable,
|
|
24
|
+
* linked from the footer, and covered by `audit-nav-page-wiring`.
|
|
25
|
+
*/
|
|
26
|
+
export const HELP_INDEX_PATH = "/help";
|
|
27
|
+
/**
|
|
28
|
+
* The orders guide, named once because THREE categories share it.
|
|
29
|
+
*
|
|
30
|
+
* `orders_payment`, `shipping_delivery` and `returns_refunds` all resolve to
|
|
31
|
+
* the same page — see the per-entry notes below for why — so the path and the
|
|
32
|
+
* label were written out three times each. Extracted so a retitle is one edit
|
|
33
|
+
* rather than three, and so the three cannot drift into disagreeing labels for
|
|
34
|
+
* one destination.
|
|
35
|
+
*/
|
|
36
|
+
const ORDERS_GUIDE = {
|
|
37
|
+
path: "/help/orders",
|
|
38
|
+
label: "Orders, Returns & Support guide",
|
|
39
|
+
};
|
|
40
|
+
export const FAQ_CATEGORY_HELP_GUIDE = {
|
|
41
|
+
orders_payment: ORDERS_GUIDE,
|
|
42
|
+
// Shipping questions are about an order in flight, which is what the orders
|
|
43
|
+
// guide covers end to end. A separate shipping guide does not exist, and a
|
|
44
|
+
// link to one that did not would be worse than this.
|
|
45
|
+
shipping_delivery: ORDERS_GUIDE,
|
|
46
|
+
returns_refunds: ORDERS_GUIDE,
|
|
47
|
+
// "Is my Beyblade original?" — the vocabulary behind most questions about
|
|
48
|
+
// what an item actually is.
|
|
49
|
+
product_information: { path: "/help/authenticity", label: "Is my Beyblade original?" },
|
|
50
|
+
account_security: { path: "/help/account", label: "Account & Safety guide" },
|
|
51
|
+
// Safety, not shopping: the account guide is where reporting and blocking
|
|
52
|
+
// live.
|
|
53
|
+
scam_awareness: { path: "/help/account", label: "Account & Safety guide" },
|
|
54
|
+
fees_and_taxes: { path: "/help/shopping", label: "Shopping & Checkout guide" },
|
|
55
|
+
technical_support: null,
|
|
56
|
+
general: null,
|
|
57
|
+
};
|
|
58
|
+
/** The guide for a category, or the Help Centre index when none is specific. */
|
|
59
|
+
export function helpGuideFor(category) {
|
|
60
|
+
return (FAQ_CATEGORY_HELP_GUIDE[category] ?? {
|
|
61
|
+
path: HELP_INDEX_PATH,
|
|
62
|
+
label: "Help Centre",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -4,8 +4,7 @@ import { Container, Div, Heading, Section, Span, Stack, Text, } from "../../../u
|
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
5
5
|
import { GroupMemberPicker } from "../../products/components/GroupMemberPicker.js";
|
|
6
6
|
import { readGroupRules } from "../../../_internal/shared/features/products/group-rules.js";
|
|
7
|
-
import {
|
|
8
|
-
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
|
+
import { RecordOwnerQuickActions } from "../../../features/admin/components/RecordOwnerQuickActions.js";
|
|
9
8
|
const __O = { hidden: "overflow-hidden" };
|
|
10
9
|
/** Human label per theme — mirrors GROUP_THEME_TITLE on the carousel. */
|
|
11
10
|
const THEME_LABEL = {
|
|
@@ -16,7 +15,7 @@ const THEME_LABEL = {
|
|
|
16
15
|
generic: "Grouped listing",
|
|
17
16
|
};
|
|
18
17
|
export function GroupedListingDetailView({ group, members }) {
|
|
19
|
-
return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(
|
|
18
|
+
return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(RecordOwnerQuickActions, { resource: "GROUP", recordId: String(group.id), placement: "detail", recordLabel: String(group.title), storeId: group.storeId ? String(group.storeId) : undefined }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members,
|
|
20
19
|
/*
|
|
21
20
|
* Off the group document this view already loaded. The picker
|
|
22
21
|
* and `addGroupLineToCart` run the SAME `checkGroupRules`, so
|
|
@@ -1,61 +1,73 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
|
|
3
|
+
import Link from "next/link.js";
|
|
4
|
+
import { Div, Heading, HorizontalScroller, PlainCard, PLAIN_CLASS, PosterCard, PosterGrid, POSTER_CLASS, Section, } from "../../../ui/index.js";
|
|
6
5
|
import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed.js";
|
|
6
|
+
/* Not re-exported from `ui/index` — import from the defining module (Root Cause #18). */
|
|
7
|
+
import { Iframe } from "../../../ui/components/Iframe.js";
|
|
7
8
|
import { SectionCarousel } from "./SectionCarousel.js";
|
|
8
9
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
|
-
const __P = {
|
|
10
|
-
p4: "p-[var(--appkit-space-4)]",
|
|
11
|
-
};
|
|
12
|
-
const __O = {
|
|
13
|
-
hidden: "overflow-hidden",
|
|
14
|
-
};
|
|
15
10
|
// --- Constants ---------------------------------------------------------------
|
|
16
11
|
const CLS_CONTAINER = "w-full max-w-7xl mx-auto px-[var(--appkit-space-4)]";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
12
|
+
function isKeyChip(variant) {
|
|
13
|
+
return variant !== "ghost";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 🛑 `next/link` for an internal href, a plain `<a>` only for an external one.
|
|
17
|
+
*
|
|
18
|
+
* A bare anchor on an internal route does a FULL PAGE LOAD — the regression
|
|
19
|
+
* documented on `OverflowRow.linkAs`, and invisible apart from the flash. The
|
|
20
|
+
* hrefs here are admin-authored, so both kinds genuinely occur.
|
|
21
|
+
*/
|
|
22
|
+
function Chip({ button, className, }) {
|
|
23
|
+
const external = button.target === "_blank" || /^https?:\/\//.test(button.href);
|
|
24
|
+
if (external) {
|
|
25
|
+
return (_jsx("a", { href: button.href, className: className, target: button.target ?? "_blank", rel: "noopener noreferrer", children: button.label }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(Link, { href: button.href, className: className, children: button.label }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Peers first, destinations last.
|
|
31
|
+
*
|
|
32
|
+
* The seeded order interleaves them ("… Heavy Metal System, Parts, Launchers"
|
|
33
|
+
* on one card, "Parts" mid-row on another), so the solid chips landed in a
|
|
34
|
+
* different place on every card and the row had no readable shape. Sorting is
|
|
35
|
+
* stable, so the authored order within each group is preserved.
|
|
36
|
+
*/
|
|
37
|
+
function orderedButtons(buttons) {
|
|
38
|
+
return [...buttons].sort((a, b) => Number(isKeyChip(a.variant)) - Number(isKeyChip(b.variant)));
|
|
39
|
+
}
|
|
44
40
|
// --- Card Item ---------------------------------------------------------------
|
|
45
41
|
function CardItem({ card }) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const buttons = card.buttons?.length ? orderedButtons(card.buttons) : [];
|
|
43
|
+
/*
|
|
44
|
+
* With artwork → the poster treatment: the image fills the card and the
|
|
45
|
+
* scrim carries the type. Without → the plain card. Branching on the data
|
|
46
|
+
* rather than on a config flag keeps every OTHER `custom-cards` section an
|
|
47
|
+
* admin has authored rendering exactly as before.
|
|
48
|
+
*/
|
|
49
|
+
if (card.image) {
|
|
50
|
+
return (_jsx(PosterCard, { height: "tall", media: _jsx(MediaImage, { src: card.image, alt: card.imageAlt ?? "", size: "card", objectFit: "cover" }), eyebrow: card.eyebrow, title: card.title, text: card.body, chips: buttons.length > 0
|
|
51
|
+
? buttons.map((btn, i) => (_jsx(Chip, { button: btn, className: isKeyChip(btn.variant)
|
|
52
|
+
? `${POSTER_CLASS.chip} ${POSTER_CLASS.chipKey}`
|
|
53
|
+
: POSTER_CLASS.chip }, `${btn.href}-${i}`)))
|
|
54
|
+
: undefined }));
|
|
55
|
+
}
|
|
56
|
+
return (_jsx(PlainCard, { eyebrow: card.eyebrow, title: card.title, text: card.body, chips: buttons.length > 0
|
|
57
|
+
? buttons.map((btn, i) => (_jsx(Chip, { button: btn, className: isKeyChip(btn.variant)
|
|
58
|
+
? `${PLAIN_CLASS.chip} ${PLAIN_CLASS.chipKey}`
|
|
59
|
+
: PLAIN_CLASS.chip }, `${btn.href}-${i}`)))
|
|
60
|
+
: undefined, footer: card.formEmbed ? (_jsx(Iframe, { src: card.formEmbed, title: "Embedded form", sandbox: "allow-forms allow-scripts" })) : undefined }));
|
|
49
61
|
}
|
|
50
62
|
// --- Section Header ----------------------------------------------------------
|
|
51
63
|
function SectionHeader({ title }) {
|
|
52
64
|
const themed = { textPrimary: THEMED_TEXT_PRIMARY };
|
|
53
65
|
if (!title)
|
|
54
66
|
return null;
|
|
55
|
-
return (_jsx(Div, { className: "mb-6", children: _jsx(Heading, { level: 2, className: themed.textPrimary, children: title }) }));
|
|
67
|
+
return (_jsx(Div, { className: "mb-6 text-left", children: _jsx(Heading, { level: 2, className: themed.textPrimary, size: "2xl", mdSize: "3xl", weight: "bold", children: title }) }));
|
|
56
68
|
}
|
|
57
69
|
export function CustomCardsSection(config) {
|
|
58
|
-
const { title, layout, columns = 3, cards, autoScroll, scrollIntervalMs, loop = true } = config;
|
|
70
|
+
const { title, layout, columns = 3, cards, autoScroll, scrollIntervalMs, loop = true, } = config;
|
|
59
71
|
if (!cards?.length)
|
|
60
72
|
return null;
|
|
61
73
|
// autoScroll: wrap all cards in SectionCarousel (client carousel)
|
|
@@ -66,16 +78,21 @@ export function CustomCardsSection(config) {
|
|
|
66
78
|
if (layout === "row") {
|
|
67
79
|
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, loop: loop, items: cards, keyExtractor: (card) => card.id, renderItem: (card) => (_jsx(Div, { className: "w-72 flex-shrink-0", children: _jsx(CardItem, { card: card }) })) })] }) }));
|
|
68
80
|
}
|
|
69
|
-
|
|
81
|
+
/*
|
|
82
|
+
* Masonry. `columns-*` on a container whose children are poster cards would
|
|
83
|
+
* fragment them across column boundaries, so the cards still carry
|
|
84
|
+
* `break-inside-avoid` — unchanged behaviour, just the new card shapes.
|
|
85
|
+
*/
|
|
70
86
|
if (layout === "masonry") {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
const masonryClass = columns === 4
|
|
88
|
+
? "columns-1 sm:columns-2 lg:columns-4"
|
|
89
|
+
: columns === 2
|
|
90
|
+
? "columns-1 sm:columns-2"
|
|
91
|
+
: columns === 1
|
|
92
|
+
? "columns-1"
|
|
93
|
+
: "columns-1 sm:columns-2 lg:columns-3";
|
|
94
|
+
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(Div, { gap: "4", className: masonryClass, children: cards.map((card) => (_jsx(Div, { className: "break-inside-avoid mb-4", children: _jsx(CardItem, { card: card }) }, card.id))) })] }) }));
|
|
77
95
|
}
|
|
78
96
|
// Grid layout (default)
|
|
79
|
-
|
|
80
|
-
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(Div, { layout: "grid", gap: "4", className: `${colsClass}`, children: cards.map((card) => (_jsx(CardItem, { card: card }, card.id))) })] }) }));
|
|
97
|
+
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(PosterGrid, { columns: columns, children: cards.map((card) => (_jsx(CardItem, { card: card }, card.id))) })] }) }));
|
|
81
98
|
}
|
|
@@ -10,6 +10,8 @@ import { POSITION_FILL, FLEX_CENTER, HERO_PLACEHOLDER_BG } from "../constants/he
|
|
|
10
10
|
/* The one carousel position/navigation control. The dot and arrow class
|
|
11
11
|
strings this file used to import are deleted — see CarouselNav.style.css. */
|
|
12
12
|
import { CarouselNav } from "../../../ui/components/CarouselNav.js";
|
|
13
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
14
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
15
|
function makeButtonClickHandler(href, openInNewTab, push) {
|
|
14
16
|
return () => {
|
|
15
17
|
if (openInNewTab) {
|
|
@@ -200,7 +202,7 @@ export function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelay
|
|
|
200
202
|
onMouseEnter: pauseOnHover ? () => setIsPaused(true) : undefined, onMouseLeave: pauseOnHover ? () => setIsPaused(false) : undefined, onFocus: () => setIsPaused(true), onBlur: () => setIsPaused(false), tabIndex: 0, children: [_jsx(Div, { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: `Slide ${currentSlide + 1} of ${slides.length}` }), _jsx(Div, { className: POSITION_FILL, children: _jsx(Div, { layout: "flex", ref: slidesRef, onScroll: handleSlidesScroll, className: "h-full overflow-x-auto scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { gap: 0 }, children: slides.map((slide, slideIndex) => {
|
|
201
203
|
const slideHeightClass = getSlideHeightClass(slide.settings?.height);
|
|
202
204
|
const hasCards = slide.cards && slide.cards.length > 0;
|
|
203
|
-
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 opacity-80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
|
|
205
|
+
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: "absolute bottom-2 left-2 z-10", children: _jsx(OwnerQuickActions, { resource: "CAROUSEL", placement: "card", recordLabel: slide.title ?? `Slide ${slideIndex + 1}`, editHref: String(ROUTES.ADMIN.CAROUSEL_EDIT(slide.id)) }) }), _jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 opacity-80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
|
|
204
206
|
// level={2}, NOT 1. The welcome section above this
|
|
205
207
|
// carousel owns the page's single <h1>; a carousel that
|
|
206
208
|
// emits one <h1> per slide gives the document as many
|