@mohasinac/appkit 4.57.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/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +86 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -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/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 +31 -3
- 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 -2
- 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 -3
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- 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/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/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/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +13 -1
- package/dist/next/routing/route-map.js +37 -0
- package/dist/schemas/registry.d.ts +6 -6
- 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 +2 -0
- package/dist/server.js +8 -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
|
@@ -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,8 +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 {
|
|
13
|
-
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
12
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
14
13
|
const PAGE_SIZE = 24;
|
|
15
14
|
export async function FranchiseDetailPageView({ slug, franchise, }) {
|
|
16
15
|
const result = await safeRead(() => listPublicProducts({
|
|
@@ -39,7 +38,7 @@ export async function FranchiseDetailPageView({ slug, franchise, }) {
|
|
|
39
38
|
const products = rows.map((r) => toProductItem(r));
|
|
40
39
|
const total = reportedTotal ?? products.length;
|
|
41
40
|
const cover = franchise.display?.coverImage;
|
|
42
|
-
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(
|
|
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
|
|
43
42
|
? "No listings yet."
|
|
44
43
|
: `${total}${truncated ? "+" : ""} listing${total === 1 ? "" : "s"}` }), _jsx(ProductGrid, { products: products, emptyLabel: "No listings yet." })] }))] }) }) }));
|
|
45
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
|
}
|
|
@@ -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
|
}
|
|
@@ -4,26 +4,39 @@ import { useState } from "react";
|
|
|
4
4
|
import Image from "next/image.js";
|
|
5
5
|
import Link from "next/link.js";
|
|
6
6
|
import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed.js";
|
|
7
|
-
import { Button, Div, Heading, HorizontalScroller, Row, Section, Span,
|
|
7
|
+
import { Button, Div, Heading, HorizontalScroller, POSTER_CLASS, Row, Section, Span, Text, } from "../../../ui/index.js";
|
|
8
8
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
9
9
|
import { resolveMediaUrl } from "../../../utils/media-url.js";
|
|
10
10
|
import { useTopCategories } from "../hooks/useTopCategories.js";
|
|
11
11
|
import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view.js";
|
|
12
|
-
const __P = {
|
|
13
|
-
p3: "p-[var(--appkit-space-3)]",
|
|
14
|
-
};
|
|
15
|
-
const __O = {
|
|
16
|
-
hidden: "overflow-hidden",
|
|
17
|
-
};
|
|
18
12
|
function isImageUrl(s) {
|
|
19
13
|
return s.startsWith("http") || s.startsWith("/") || s.startsWith("data:");
|
|
20
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* One category tile.
|
|
17
|
+
*
|
|
18
|
+
* 🛑 Image-LED, not image-on-top-of-a-body. The previous shape was a 16:9 crop
|
|
19
|
+
* stacked over a white block holding an icon square, the name, a count and a
|
|
20
|
+
* "Browse category →" line — four separate type sizes and a redundant CTA on a
|
|
21
|
+
* tile that is already entirely a link. It also letterboxed the artwork: every
|
|
22
|
+
* generation cover is portrait key art, so `aspect-video` cropped it to a
|
|
23
|
+
* horizontal band through the middle.
|
|
24
|
+
*
|
|
25
|
+
* Now it is the same `PosterCard` the systems matrix uses, so the two bands on
|
|
26
|
+
* the homepage read as one system rather than two designs.
|
|
27
|
+
*/
|
|
21
28
|
function CategoryChip({ category }) {
|
|
22
29
|
const iconSrc = category.display?.icon;
|
|
23
30
|
const coverImage = category.display?.coverImage;
|
|
24
|
-
const
|
|
31
|
+
const resolvedCover = coverImage && isImageUrl(coverImage) ? resolveMediaUrl(coverImage) : undefined;
|
|
25
32
|
const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? 0;
|
|
26
|
-
|
|
33
|
+
/*
|
|
34
|
+
* `display.icon` holds an EMOJI in the seed and a URL in other writers —
|
|
35
|
+
* the ambiguity `resolveTaxonomyVisual` exists for. Only the emoji form is
|
|
36
|
+
* used as an eyebrow glyph here; a URL would print as raw text.
|
|
37
|
+
*/
|
|
38
|
+
const emoji = iconSrc && !isImageUrl(iconSrc) ? iconSrc : undefined;
|
|
39
|
+
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: `${POSTER_CLASS.root} ${POSTER_CLASS.compact} w-full`, children: [resolvedCover ? (_jsx(Div, { className: POSTER_CLASS.media, children: _jsx(Image, { src: resolvedCover, alt: "", fill: true, sizes: "(min-width: 1024px) 20rem, (min-width: 640px) 45vw, 80vw", className: "h-full w-full object-cover" }) })) : null, _jsx(Div, { className: POSTER_CLASS.scrim, "aria-hidden": "true" }), _jsxs(Div, { className: POSTER_CLASS.body, children: [emoji && (_jsx(Span, { className: POSTER_CLASS.eyebrow, "aria-hidden": "true", children: emoji })), _jsx(Span, { className: POSTER_CLASS.title, children: category.name }), _jsxs(Span, { className: POSTER_CLASS.meta, children: [productCount.toLocaleString(), " items"] })] })] }));
|
|
27
40
|
}
|
|
28
41
|
const CTA_CLASSES = {
|
|
29
42
|
filled: "rounded-lg bg-[var(--appkit-color-primary)] px-[var(--appkit-space-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white hover:opacity-90",
|
|
@@ -69,5 +82,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
|
|
|
69
82
|
if (!cat)
|
|
70
83
|
return null;
|
|
71
84
|
return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
|
|
72
|
-
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3",
|
|
85
|
+
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", overflow: "hidden", className: "px-[0.25rem]", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel }) }))] }) }));
|
|
73
86
|
}
|
|
@@ -215,15 +215,15 @@ export declare const carouselCardSchema: z.ZodObject<{
|
|
|
215
215
|
description?: string | undefined;
|
|
216
216
|
textColor?: string | undefined;
|
|
217
217
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
218
|
-
subtitle?: string | undefined;
|
|
219
218
|
eyebrow?: string | undefined;
|
|
219
|
+
subtitle?: string | undefined;
|
|
220
220
|
}, {
|
|
221
221
|
title?: string | undefined;
|
|
222
222
|
description?: string | undefined;
|
|
223
223
|
textColor?: string | undefined;
|
|
224
224
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
225
|
-
subtitle?: string | undefined;
|
|
226
225
|
eyebrow?: string | undefined;
|
|
226
|
+
subtitle?: string | undefined;
|
|
227
227
|
}>>;
|
|
228
228
|
buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
229
229
|
id: z.ZodOptional<z.ZodString>;
|
|
@@ -282,8 +282,8 @@ export declare const carouselCardSchema: z.ZodObject<{
|
|
|
282
282
|
description?: string | undefined;
|
|
283
283
|
textColor?: string | undefined;
|
|
284
284
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
285
|
-
subtitle?: string | undefined;
|
|
286
285
|
eyebrow?: string | undefined;
|
|
286
|
+
subtitle?: string | undefined;
|
|
287
287
|
} | undefined;
|
|
288
288
|
hover?: {
|
|
289
289
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -323,8 +323,8 @@ export declare const carouselCardSchema: z.ZodObject<{
|
|
|
323
323
|
description?: string | undefined;
|
|
324
324
|
textColor?: string | undefined;
|
|
325
325
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
326
|
-
subtitle?: string | undefined;
|
|
327
326
|
eyebrow?: string | undefined;
|
|
327
|
+
subtitle?: string | undefined;
|
|
328
328
|
} | undefined;
|
|
329
329
|
hover?: {
|
|
330
330
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -555,15 +555,15 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
555
555
|
description?: string | undefined;
|
|
556
556
|
textColor?: string | undefined;
|
|
557
557
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
558
|
-
subtitle?: string | undefined;
|
|
559
558
|
eyebrow?: string | undefined;
|
|
559
|
+
subtitle?: string | undefined;
|
|
560
560
|
}, {
|
|
561
561
|
title?: string | undefined;
|
|
562
562
|
description?: string | undefined;
|
|
563
563
|
textColor?: string | undefined;
|
|
564
564
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
565
|
-
subtitle?: string | undefined;
|
|
566
565
|
eyebrow?: string | undefined;
|
|
566
|
+
subtitle?: string | undefined;
|
|
567
567
|
}>>;
|
|
568
568
|
buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
569
569
|
id: z.ZodOptional<z.ZodString>;
|
|
@@ -622,8 +622,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
622
622
|
description?: string | undefined;
|
|
623
623
|
textColor?: string | undefined;
|
|
624
624
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
625
|
-
subtitle?: string | undefined;
|
|
626
625
|
eyebrow?: string | undefined;
|
|
626
|
+
subtitle?: string | undefined;
|
|
627
627
|
} | undefined;
|
|
628
628
|
hover?: {
|
|
629
629
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -663,8 +663,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
663
663
|
description?: string | undefined;
|
|
664
664
|
textColor?: string | undefined;
|
|
665
665
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
666
|
-
subtitle?: string | undefined;
|
|
667
666
|
eyebrow?: string | undefined;
|
|
667
|
+
subtitle?: string | undefined;
|
|
668
668
|
} | undefined;
|
|
669
669
|
hover?: {
|
|
670
670
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -802,8 +802,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
802
802
|
description?: string | undefined;
|
|
803
803
|
textColor?: string | undefined;
|
|
804
804
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
805
|
-
subtitle?: string | undefined;
|
|
806
805
|
eyebrow?: string | undefined;
|
|
806
|
+
subtitle?: string | undefined;
|
|
807
807
|
} | undefined;
|
|
808
808
|
hover?: {
|
|
809
809
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -911,8 +911,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
911
911
|
description?: string | undefined;
|
|
912
912
|
textColor?: string | undefined;
|
|
913
913
|
textAlign?: "center" | "left" | "right" | undefined;
|
|
914
|
-
subtitle?: string | undefined;
|
|
915
914
|
eyebrow?: string | undefined;
|
|
915
|
+
subtitle?: string | undefined;
|
|
916
916
|
} | undefined;
|
|
917
917
|
hover?: {
|
|
918
918
|
effect: "none" | "color" | "scale" | "glow";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, } from "../../../ui/index.js";
|
|
2
|
+
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, Anchor, } from "../../../ui/index.js";
|
|
3
3
|
import { SpecTable } from "../../../ui/components/SpecTable.js";
|
|
4
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
5
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
4
6
|
import { PART_SLOT_LABEL, } from "../schemas/firestore.js";
|
|
5
7
|
/**
|
|
6
8
|
* The buyer-facing label for each WBO tournament format, in the order a
|
|
@@ -100,12 +102,12 @@ export function PartDetailPageView({ part, usedBy, listings, listingsTruncated,
|
|
|
100
102
|
: [];
|
|
101
103
|
/* Only the formats this part is actually ruled on — see the note below. */
|
|
102
104
|
const legalFormats = Object.entries(part.wboLegality ?? {}).filter(([, v]) => typeof v === "boolean");
|
|
103
|
-
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), specRows.length > 1 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), ratingRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Ratings" }), _jsx(SpecTable, { rows: ratingRows.map((r) => ({
|
|
105
|
+
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsx(RecordOwnerQuickActions, { resource: "PART", recordId: part.id, placement: "detail", recordLabel: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), specRows.length > 1 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), ratingRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Ratings" }), _jsx(SpecTable, { rows: ratingRows.map((r) => ({
|
|
104
106
|
name: r.label,
|
|
105
107
|
value: `${r.value} / 100`,
|
|
106
108
|
})) }), _jsxs(Text, { size: "xs", color: "faint", children: ["Rescaled to 0\u2013100 for comparison across generations", typeof part.ratingScale === "number"
|
|
107
109
|
? `; rated 0–${part.ratingScale} at source`
|
|
108
|
-
: "", "."] })] })), legalFormats.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Tournament legality" }), _jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: legalFormats.map(([format, allowed]) => (_jsxs(Badge, { variant: allowed ? "success" : "danger", children: [WBO_FORMAT_LABEL[format] ?? format, allowed ? " ✓" : " ✗"] }, format))) }), _jsx(Text, { size: "xs", color: "faint", children: "World Beyblade Organization formats this part is ruled on. A format not listed has no ruling for it." })] })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
|
|
110
|
+
: "", "."] })] })), legalFormats.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Tournament legality" }), _jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: legalFormats.map(([format, allowed]) => (_jsxs(Badge, { variant: allowed ? "success" : "danger", children: [WBO_FORMAT_LABEL[format] ?? format, allowed ? " ✓" : " ✗"] }, format))) }), _jsx(Text, { size: "xs", color: "faint", children: "World Beyblade Organization formats this part is ruled on. A format not listed has no ruling for it." })] })), part.sourceUrl && (_jsx(Div, { children: _jsxs(Text, { size: "xs", color: "faint", children: ["Facts on this page are drawn from a community wiki under CC\u00A0BY-SA.", " ", _jsx(Anchor, { href: part.sourceUrl, tone: "muted", underline: "always", children: "Source" }), " · ", _jsx(TextLink, { href: String(ROUTES.PUBLIC.CREDITS), children: "Credits" })] }) })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
|
|
109
111
|
? `Used by ${usedBy.length} ${usedBy.length === 1 ? "model" : "models"}`
|
|
110
112
|
: "Used by" }), usedBy.length === 0 ? (_jsx(Text, { color: "muted", size: "sm", children: "No model in our database records this part yet." })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: usedBy.map((m) => (_jsxs(Row, { gap: "sm", align: "center", padding: "inline", surface: "card", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [m.categoryId ? (_jsx(TextLink, { href: hrefForModel(m.categoryId), children: m.name })) : (
|
|
111
113
|
/* 49 of 259 compositions did not join to a category.
|