@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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `beys` and `beyProducts` collections — the BEY reference database.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from a crawled corpus by committed scripts and generated into the
|
|
5
|
+
* seed; nothing here is hand-typed. 1,988 beys and 366 products.
|
|
6
|
+
*
|
|
7
|
+
* ── 🛑 WHY THIS MIRRORS `partGlossary` RATHER THAN INVENTING A SHAPE ───────
|
|
8
|
+
*
|
|
9
|
+
* Parts already do all three halves of this and have for months: a
|
|
10
|
+
* `partGlossary` collection, a category leaf per part, and a
|
|
11
|
+
* `/parts/{classification}/{code}` reference page. Beys get the same three,
|
|
12
|
+
* so every decision that was already argued out there is inherited rather
|
|
13
|
+
* than re-litigated:
|
|
14
|
+
*
|
|
15
|
+
* · the ID IS THE SLUG, shared with the category leaf (`category-{slug}`),
|
|
16
|
+
* because one key across two collections is what keeps a reference page
|
|
17
|
+
* and a commerce shelf pointing at the same thing;
|
|
18
|
+
* · measured data lives HERE, not on `categories`, because
|
|
19
|
+
* `handleCategoryWrite` calls `notifyRevalidate` unconditionally — so a
|
|
20
|
+
* reseed of bey facts would drop every bey page's ISR entry on a project
|
|
21
|
+
* that has already been paused at 15.5x its ISR cap;
|
|
22
|
+
* · and a bey page is a REFERENCE surface, so it carries no money. Prices
|
|
23
|
+
* are gated for signed-out visitors, which would make the page a client
|
|
24
|
+
* component (see `PartDetailPageView`'s header).
|
|
25
|
+
*
|
|
26
|
+
* ── 🛑 NO `searchTxt` FIELD, DELIBERATELY ─────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* `partGlossary` had one and it was REMOVED; the collection is declared
|
|
29
|
+
* `INDEX_ARTIFACT_ONLY` in `audit-searchtxt-migration`, whose rule is that a
|
|
30
|
+
* member must not declare the field. Beys are the same kind of collection —
|
|
31
|
+
* discovery happens through the category leaf, which has its own `searchTxt`
|
|
32
|
+
* built from the leaf's name and aliases.
|
|
33
|
+
*/
|
|
34
|
+
import type { AdminEditableRecord } from "../../parts/schemas/admin-edit";
|
|
35
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
36
|
+
export declare const BEYS_COLLECTION: "beys";
|
|
37
|
+
export declare const BEY_PRODUCTS_COLLECTION: "beyProducts";
|
|
38
|
+
/**
|
|
39
|
+
* One slot of a bey's build.
|
|
40
|
+
*
|
|
41
|
+
* 🛑 `partName` is kept EVEN WHEN `partId` resolves, and that is not
|
|
42
|
+
* redundancy. The join reaches 88.9% of 7,688 links, so ~850 entries name a
|
|
43
|
+
* part we hold no row for — and a slot that renders nothing reads as a bey
|
|
44
|
+
* with an incomplete build rather than as a missing part page. The name is
|
|
45
|
+
* what the source stated; the id is what we managed to resolve.
|
|
46
|
+
*/
|
|
47
|
+
export interface BeyCompositionEntry {
|
|
48
|
+
/** Our canonical slot key — `fusion-wheel`, `performance-tip`, `driver`. */
|
|
49
|
+
slot: string;
|
|
50
|
+
/** The part as the source names it, which is sometimes an abbreviation. */
|
|
51
|
+
partName: string;
|
|
52
|
+
/** `partGlossary` id, when the name or one of its aliases resolved. */
|
|
53
|
+
partId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The raw infobox key this came from.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 Load-bearing for Synchrome, where `WarriorWheel2=Behemoth` is the
|
|
58
|
+
* OTHER HALF of a two-wheel combination rather than a duplicate key to
|
|
59
|
+
* dedupe. Two entries can share a `slot` and still be distinct parts.
|
|
60
|
+
*/
|
|
61
|
+
infoboxKey?: string;
|
|
62
|
+
}
|
|
63
|
+
/** The 0-100 rating vector. Same axes as `PartRatings`, same scale. */
|
|
64
|
+
export interface BeyRatings {
|
|
65
|
+
attack?: number;
|
|
66
|
+
defense?: number;
|
|
67
|
+
stamina?: number;
|
|
68
|
+
weight?: number;
|
|
69
|
+
speed?: number;
|
|
70
|
+
burst?: number;
|
|
71
|
+
burstResistance?: number;
|
|
72
|
+
dash?: number;
|
|
73
|
+
power?: number;
|
|
74
|
+
height?: number;
|
|
75
|
+
runTime?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How much of a derived figure is actually measured.
|
|
79
|
+
*
|
|
80
|
+
* 🛑 This rides WITH every derived value and is not optional decoration. A
|
|
81
|
+
* vector built from 2 of 5 parts is a different claim from one built from 5 of
|
|
82
|
+
* 5, and a mass summed from 3 weighed parts out of 5 is a floor rather than a
|
|
83
|
+
* total. Without this a consumer cannot tell them apart, and the honest
|
|
84
|
+
* rendering ("≈", or nothing at all) becomes impossible.
|
|
85
|
+
*/
|
|
86
|
+
export interface BeyDerivationCoverage {
|
|
87
|
+
/** Parts contributing a value. */
|
|
88
|
+
rated?: number;
|
|
89
|
+
weighed?: number;
|
|
90
|
+
/** Parts in the composition. */
|
|
91
|
+
total: number;
|
|
92
|
+
/** True only when every part contributed. */
|
|
93
|
+
complete?: boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One configuration a bey can be set to, or settles into.
|
|
97
|
+
*
|
|
98
|
+
* ── 🛑 THERE IS NO THRESHOLD FIELD, AND THAT IS DELIBERATE ────────────────
|
|
99
|
+
*
|
|
100
|
+
* Mode data exists only in the local case-study corpus (the wiki carries ZERO:
|
|
101
|
+
* 0 `Modes=` fields across 2,248 part pages and 2,011 bey pages), and every
|
|
102
|
+
* numeric threshold cross-checked there DISAGREED across sources. Cosmic
|
|
103
|
+
* Pegasus F:D is documented twice with the transition running in opposite
|
|
104
|
+
* directions and three different thresholds between the two cases and the plan.
|
|
105
|
+
*
|
|
106
|
+
* So a `thresholdRadPerSec` does not exist here. A field that is absent cannot
|
|
107
|
+
* be filled with a contested number by someone who assumes the gap was an
|
|
108
|
+
* oversight — which is why this is a schema decision and not a convention.
|
|
109
|
+
*
|
|
110
|
+
* 🛑 `trigger` is FREE TEXT, not an enum. The three authored shapes are
|
|
111
|
+
* already a pre-launch setting, passive spin decay, and what surface the tip
|
|
112
|
+
* is running on — and a three-value enum written before the fourth case was
|
|
113
|
+
* read would have forced the next one into the wrong bucket.
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* How a mode is selected.
|
|
117
|
+
*
|
|
118
|
+
* ── 🛑 THIS ENUM IS THE OWNER'S TAXONOMY, NOT MY GUESS ────────────────────
|
|
119
|
+
*
|
|
120
|
+
* Owner, 2026-10-21: *"most mode switches are either flips, config 1, 2 or 3
|
|
121
|
+
* and more."* Everything beyond that set is a mode that changes WITHOUT the
|
|
122
|
+
* owner doing anything, and those divide cleanly by what drives them.
|
|
123
|
+
*
|
|
124
|
+
* I deliberately did NOT define this when the first four modes were authored,
|
|
125
|
+
* because at that point I had seen two kinds and an enum built from two would
|
|
126
|
+
* have forced the next two into the wrong value. It is defined now that the
|
|
127
|
+
* real set is known — which is the difference between a taxonomy and a guess.
|
|
128
|
+
*/
|
|
129
|
+
export declare const BEY_MODE_SWITCH_KINDS: readonly ["flip", "config", "swap", "speed", "impact"];
|
|
130
|
+
export type BeyModeSwitchKind = (typeof BEY_MODE_SWITCH_KINDS)[number];
|
|
131
|
+
/** One configuration a bey can be set to, or settles into. */
|
|
132
|
+
export interface BeyMode {
|
|
133
|
+
/** "Attack", "Stamina", "Barrage". The part's own vocabulary where it has one. */
|
|
134
|
+
name: string;
|
|
135
|
+
/** What it does, in a sentence a buyer can read. No formulae. */
|
|
136
|
+
behaviour: string;
|
|
137
|
+
/**
|
|
138
|
+
* How it comes about, in prose. 🛑 Kept ALONGSIDE `switchKind` rather than
|
|
139
|
+
* replaced by it: the kind is what a filter or a game reads, the sentence is
|
|
140
|
+
* what a person reads, and "Assemble the wheel for six contact points" is
|
|
141
|
+
* not recoverable from `"config"`.
|
|
142
|
+
*/
|
|
143
|
+
trigger: string;
|
|
144
|
+
/** Which of the six selection mechanisms this is. */
|
|
145
|
+
switchKind?: BeyModeSwitchKind;
|
|
146
|
+
/** 1-based, for `switchKind: "config"` / `"swap"`. */
|
|
147
|
+
configIndex?: number;
|
|
148
|
+
/**
|
|
149
|
+
* The part that provides the mode — a fusion wheel or a tip in every case
|
|
150
|
+
* confirmed so far.
|
|
151
|
+
*
|
|
152
|
+
* 🛑 Modes are authored against the PART and fanned out to every bey whose
|
|
153
|
+
* composition uses it, so one wheel is described once rather than copied per
|
|
154
|
+
* bey (Root Cause #75). This field keeps that origin visible on the bey, and
|
|
155
|
+
* is what would let a later pass lift modes onto `partGlossary` mechanically.
|
|
156
|
+
*/
|
|
157
|
+
appliesToPart?: string;
|
|
158
|
+
/**
|
|
159
|
+
* A second part the mode depends on, when it is an ALIGNMENT between two.
|
|
160
|
+
*
|
|
161
|
+
* 🛑 Not every mode belongs to one part. Lightning and Meteo L-Drago change
|
|
162
|
+
* mode by how the energy ring's dragon heads line up with the wheel's teeth
|
|
163
|
+
* — a property of the PAIR. Without this the data would claim the wheel
|
|
164
|
+
* alone does it.
|
|
165
|
+
*/
|
|
166
|
+
requiresPart?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Who or what the claim rests on — `"owner-<date>"` for a confirmed one.
|
|
169
|
+
*
|
|
170
|
+
* 🛑 LOAD-BEARING, not metadata. The generator emits ONLY modes carrying
|
|
171
|
+
* this, because the case-study corpus's mode prose was wrong in four of four
|
|
172
|
+
* claims reviewed. See `scripts/lib/bey-modes-authored.mjs`.
|
|
173
|
+
*/
|
|
174
|
+
confirmed?: string;
|
|
175
|
+
/** The case-study case number, where one was read. A claim nobody can trace is a claim nobody can correct. */
|
|
176
|
+
sourceCase?: number;
|
|
177
|
+
}
|
|
178
|
+
export interface BeyDocument extends BaseDocument, AdminEditableRecord {
|
|
179
|
+
/** `id === slug`, and the same slug the category leaf uses. */
|
|
180
|
+
id: string;
|
|
181
|
+
slug: string;
|
|
182
|
+
name: string;
|
|
183
|
+
/** Battle type as stated — `Attack`, `Balance`, `Defense`, `Stamina`. */
|
|
184
|
+
type?: string;
|
|
185
|
+
/** `Right-Spin` / `Left-Spin` / `Dual-Spin`. */
|
|
186
|
+
spinDirection?: string;
|
|
187
|
+
/**
|
|
188
|
+
* The hardware generation — `4D System`, `Hybrid Wheel System`, `Basic
|
|
189
|
+
* Line`. 1,893 of 1,988.
|
|
190
|
+
*
|
|
191
|
+
* 🛑 This is the within-generation assembly key. No bey can be built across
|
|
192
|
+
* generations, and Plastic and HMS do not interchange either, so a build
|
|
193
|
+
* validator needs it. It is also what files the bey under a forest line
|
|
194
|
+
* (`scripts/lib/bey-system-to-line.mjs`).
|
|
195
|
+
*/
|
|
196
|
+
system?: string;
|
|
197
|
+
/** A second system, for the handful of releases that span two. */
|
|
198
|
+
system2?: string;
|
|
199
|
+
/** The retail series — `Beyblade Burst QuadStrike`. */
|
|
200
|
+
series?: string;
|
|
201
|
+
series2?: string;
|
|
202
|
+
/**
|
|
203
|
+
* The paired tier-4 category leaf — where this model's LISTINGS live.
|
|
204
|
+
*
|
|
205
|
+
* 🛑 NOT derivable from `slug`. The leaf id comes from the bey's NAME with
|
|
206
|
+
* the trailing parenthetical stripped, so `…-press-hasbro` pairs with
|
|
207
|
+
* `category-…-press`; guessing would 404 on every vendor-qualified bey.
|
|
208
|
+
* Resolved by the generator with the leaf generator's own helper and then
|
|
209
|
+
* CHECKED against the leaves it emitted, so this is present only when the
|
|
210
|
+
* page really exists — 116 beys have no leaf because their `system` is
|
|
211
|
+
* deliberately unmapped.
|
|
212
|
+
*/
|
|
213
|
+
categoryId?: string;
|
|
214
|
+
/** The build, top to bottom. 1,911 of 1,988 carry one. */
|
|
215
|
+
composition?: BeyCompositionEntry[];
|
|
216
|
+
/**
|
|
217
|
+
* 🛑 DERIVED AND AVERAGED, never summed, and never the raw source numbers.
|
|
218
|
+
*
|
|
219
|
+
* Two rating scales live in the part data — Beyblade X rates 0-100 and
|
|
220
|
+
* everything before it rates 0-6 — so these are averaged over each part's
|
|
221
|
+
* NORMALISED vector. A sum would make a five-part bey look five times
|
|
222
|
+
* stronger than a three-part one, which is backwards: more parts is not
|
|
223
|
+
* more attack.
|
|
224
|
+
*/
|
|
225
|
+
ratingsFromParts?: BeyRatings;
|
|
226
|
+
ratingsFromPartsCoverage?: BeyDerivationCoverage;
|
|
227
|
+
/**
|
|
228
|
+
* Configurations this bey can be set to. **4 of 1,988 carry this.**
|
|
229
|
+
*
|
|
230
|
+
* 🛑 ABSENT, NEVER `[]`. An empty array says "this bey has no modes" to
|
|
231
|
+
* every consumer; absent says "unknown", which is the truth for 1,984 of
|
|
232
|
+
* them. Do not default it, and do not treat its absence as a gap to fill
|
|
233
|
+
* mechanically — see `scripts/lib/bey-modes-authored.mjs` for what the
|
|
234
|
+
* corpus can and cannot support, including the one bey excluded because its
|
|
235
|
+
* two sources contradict each other outright.
|
|
236
|
+
*
|
|
237
|
+
* Most of the corpus's mode content is about PARTS, not beys (135 of 147
|
|
238
|
+
* candidates were part-level analyses), so the bulk of it belongs on
|
|
239
|
+
* `partGlossary` if it is ever modelled at all.
|
|
240
|
+
*/
|
|
241
|
+
modes?: BeyMode[];
|
|
242
|
+
/** Mass as the source states it. Only 248 of 1,988 do. */
|
|
243
|
+
weightG?: number;
|
|
244
|
+
/**
|
|
245
|
+
* Mass SUMMED from the parts — 1,593 beys, 775 from a complete set.
|
|
246
|
+
*
|
|
247
|
+
* 🛑 A DIFFERENT KIND OF CLAIM from the ratings above, and the difference is
|
|
248
|
+
* why it is safe: mass is physically additive, so a sum is a measurement.
|
|
249
|
+
* Held-out validated against the 164 beys that state their own mass —
|
|
250
|
+
* median absolute error 0.00 g, 99% within 2 g.
|
|
251
|
+
*
|
|
252
|
+
* 🛑 Never scaled up from partial coverage. 3 of 5 parts weighed gives the
|
|
253
|
+
* mass of those three; inflating it by 5/3 would invent a figure that reads
|
|
254
|
+
* exactly like a measured one. And a stated `weightG` always wins.
|
|
255
|
+
*/
|
|
256
|
+
weightGFromParts?: number;
|
|
257
|
+
weightGFromPartsCoverage?: BeyDerivationCoverage;
|
|
258
|
+
/** Box codes. `productCodes[0]` is the one printed on the box. */
|
|
259
|
+
productCodes?: string[];
|
|
260
|
+
/**
|
|
261
|
+
* Every other name this bey ships under — 1,327 of 1,988.
|
|
262
|
+
*
|
|
263
|
+
* 🛑 This is the Hasbro ↔ Takara-Tomy map, stated by the source rather than
|
|
264
|
+
* inferred: Valkyrie/Valtryek, Spriggan/Spryzen. It must NOT be used to
|
|
265
|
+
* merge two rows — `Dead` and `Dread` are two real Gatinko bases one
|
|
266
|
+
* character apart, so any name-similarity join on this corpus is
|
|
267
|
+
* disqualified.
|
|
268
|
+
*/
|
|
269
|
+
aka?: string[];
|
|
270
|
+
jpName?: string;
|
|
271
|
+
romaji?: string;
|
|
272
|
+
kanjiName?: string;
|
|
273
|
+
krName?: string;
|
|
274
|
+
releaseJP?: string;
|
|
275
|
+
releaseUS?: string;
|
|
276
|
+
releaseCA?: string;
|
|
277
|
+
releaseKR?: string;
|
|
278
|
+
/** As stated, in the source's own currency and formatting. */
|
|
279
|
+
price?: string;
|
|
280
|
+
/** The owner string verbatim, qualifiers and all. */
|
|
281
|
+
owner?: string;
|
|
282
|
+
/** Each person named, cleaned and split. */
|
|
283
|
+
ownerNames?: string[];
|
|
284
|
+
/**
|
|
285
|
+
* Character slugs that resolved exactly — 661 of the 733 with an owner.
|
|
286
|
+
*
|
|
287
|
+
* 🛑 Resolved by stripping a stated qualifier and matching EXACTLY, never
|
|
288
|
+
* approximately. `owner` keeps the raw string because "(manga)" is a real
|
|
289
|
+
* distinction: that bey belongs to that character in the manga continuity
|
|
290
|
+
* only.
|
|
291
|
+
*/
|
|
292
|
+
ownerCharacters?: string[];
|
|
293
|
+
creator?: string;
|
|
294
|
+
specialMoves?: string[];
|
|
295
|
+
/** First anime appearance, as the source names the episode. */
|
|
296
|
+
appearAnime?: string;
|
|
297
|
+
appearManga?: string;
|
|
298
|
+
animeDebut?: string;
|
|
299
|
+
/** Beyblade-X-only attribute. 45 of 1,988 — not worth a facet. */
|
|
300
|
+
element?: string;
|
|
301
|
+
avatar?: string;
|
|
302
|
+
toyline?: string;
|
|
303
|
+
/** Image filenames, resolved to `/media/<shortId>` at upload time. */
|
|
304
|
+
/**
|
|
305
|
+
* `lead` marks the one image to show first. Optional because a hand-written
|
|
306
|
+
* row may omit it and absent reads as false; the generator sets it on all
|
|
307
|
+
* 2,459 entries, so no reader is relying on the default today.
|
|
308
|
+
*/
|
|
309
|
+
images?: {
|
|
310
|
+
file: string;
|
|
311
|
+
label?: string | null;
|
|
312
|
+
lead?: boolean;
|
|
313
|
+
}[];
|
|
314
|
+
/** Our own prose, composed from the facts above. */
|
|
315
|
+
seoDescription?: string;
|
|
316
|
+
/**
|
|
317
|
+
* The source page.
|
|
318
|
+
*
|
|
319
|
+
* 🛑 REQUIRED, and it must reach the UI. The corpus is CC-BY-SA, and the
|
|
320
|
+
* two things that keep us clear of share-alike are taking facts only and
|
|
321
|
+
* linking back. It was stored on ~5,500 rows and rendered on NONE of them
|
|
322
|
+
* until the credits work; storing it is not discharging the obligation.
|
|
323
|
+
*/
|
|
324
|
+
sourceUrl: string;
|
|
325
|
+
/** How the row was obtained — `fandom-infobox`. */
|
|
326
|
+
source?: string;
|
|
327
|
+
/** Community discussion thread, where the source links one. */
|
|
328
|
+
wboThreadUrl?: string;
|
|
329
|
+
}
|
|
330
|
+
/** One assortment slot of a product — a bey a buyer might get. */
|
|
331
|
+
export interface BeyProductAssortmentEntry {
|
|
332
|
+
/** The bey as the source names it. */
|
|
333
|
+
bey: string;
|
|
334
|
+
/** Resolved `beys` id, when the name matched. */
|
|
335
|
+
beyId?: string;
|
|
336
|
+
/** A prize bey is not purchasable — it is drawn. */
|
|
337
|
+
isPrize?: boolean;
|
|
338
|
+
/** Stated pull odds, where the source gives them. */
|
|
339
|
+
odds?: string;
|
|
340
|
+
/**
|
|
341
|
+
* The manufacturer's per-item code within the assortment ("B-101 01"), where
|
|
342
|
+
* the source lists one. Distinct from the PRODUCT's own code: a random
|
|
343
|
+
* booster has one code and a different one per bey you can pull from it.
|
|
344
|
+
*/
|
|
345
|
+
itemCode?: string;
|
|
346
|
+
/** This bey debuts in the set, rather than being a repackaged earlier release. */
|
|
347
|
+
newToSeries?: boolean;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* One image in a product's gallery.
|
|
351
|
+
*
|
|
352
|
+
* `caption` is optional-AND-nullable, mirroring `BeyDocument.images[].label`
|
|
353
|
+
* beside it: the generator emits the key on all 1,457 entries and leaves 292
|
|
354
|
+
* of them `null`, while a hand-written row may simply omit it. Both are "no
|
|
355
|
+
* caption", and the type accepts both rather than forcing one spelling on a
|
|
356
|
+
* generator that already chose the other.
|
|
357
|
+
*/
|
|
358
|
+
/** One line of a product's per-carton distribution table. */
|
|
359
|
+
export interface BeyProductCartonEntry {
|
|
360
|
+
/** The bey as the source names it — matches `BeyProductAssortmentEntry.bey`. */
|
|
361
|
+
bey: string;
|
|
362
|
+
/** How many of this bey ship in one sealed carton. */
|
|
363
|
+
perCarton: number;
|
|
364
|
+
}
|
|
365
|
+
export interface BeyProductGalleryImage {
|
|
366
|
+
/** Source filename, as the corpus names it — not a `/media/` slug yet. */
|
|
367
|
+
file: string;
|
|
368
|
+
caption?: string | null;
|
|
369
|
+
}
|
|
370
|
+
export interface BeyProductDocument extends BaseDocument {
|
|
371
|
+
id: string;
|
|
372
|
+
slug: string;
|
|
373
|
+
name: string;
|
|
374
|
+
/** `Random Booster`, `Battle Set`, `Starter`, `Stadium`. */
|
|
375
|
+
productType?: string;
|
|
376
|
+
productCodes?: string[];
|
|
377
|
+
price?: string;
|
|
378
|
+
releaseJP?: string;
|
|
379
|
+
jpName?: string;
|
|
380
|
+
romaji?: string;
|
|
381
|
+
/** What is in the box, or what can be pulled from it. */
|
|
382
|
+
assortment?: BeyProductAssortmentEntry[];
|
|
383
|
+
/**
|
|
384
|
+
* Every resolved `beys` id in `assortment`, flat.
|
|
385
|
+
*
|
|
386
|
+
* 🛑 A denormalised MIRROR of `assortment[].beyId`, and it exists for one
|
|
387
|
+
* reason: Firestore cannot `array-contains` a field nested inside objects
|
|
388
|
+
* in an array, so "which products contain this bey" is otherwise
|
|
389
|
+
* unanswerable. Derived by the generator in the same pass that resolves the
|
|
390
|
+
* ids — never hand-maintained, because a mirror somebody can edit
|
|
391
|
+
* independently is the drift in Root Cause #42.
|
|
392
|
+
*/
|
|
393
|
+
assortmentBeyIds?: string[];
|
|
394
|
+
/**
|
|
395
|
+
* Per-carton distribution, on the 24 products whose source states one.
|
|
396
|
+
*
|
|
397
|
+
* 🛑 An ARRAY, not the `Record<string, string>` this was declared as. A map
|
|
398
|
+
* keyed on the bey name cannot hold two entries for the same name, and it
|
|
399
|
+
* loses the source's ordering — both of which the carton tables rely on.
|
|
400
|
+
* Measured across all 24: 137 entries, every one carrying both keys.
|
|
401
|
+
*/
|
|
402
|
+
breakdown?: BeyProductCartonEntry[];
|
|
403
|
+
boxImage?: string;
|
|
404
|
+
gallery?: BeyProductGalleryImage[];
|
|
405
|
+
seoDescription?: string;
|
|
406
|
+
/** CC-BY-SA link-back — see `BeyDocument.sourceUrl`. */
|
|
407
|
+
sourceUrl: string;
|
|
408
|
+
source?: string;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The document id for a bey.
|
|
412
|
+
*
|
|
413
|
+
* 🛑 The ONE place this is composed, mirroring `partGlossaryId`. The id is
|
|
414
|
+
* also the slug, the public URL and a `searchTxt` source on the paired
|
|
415
|
+
* category leaf, so a second spelling of this rule is a second set of URLs.
|
|
416
|
+
*/
|
|
417
|
+
export declare function beyId(slug: string): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const BEYS_COLLECTION = "beys";
|
|
2
|
+
export const BEY_PRODUCTS_COLLECTION = "beyProducts";
|
|
3
|
+
/**
|
|
4
|
+
* One configuration a bey can be set to, or settles into.
|
|
5
|
+
*
|
|
6
|
+
* ── 🛑 THERE IS NO THRESHOLD FIELD, AND THAT IS DELIBERATE ────────────────
|
|
7
|
+
*
|
|
8
|
+
* Mode data exists only in the local case-study corpus (the wiki carries ZERO:
|
|
9
|
+
* 0 `Modes=` fields across 2,248 part pages and 2,011 bey pages), and every
|
|
10
|
+
* numeric threshold cross-checked there DISAGREED across sources. Cosmic
|
|
11
|
+
* Pegasus F:D is documented twice with the transition running in opposite
|
|
12
|
+
* directions and three different thresholds between the two cases and the plan.
|
|
13
|
+
*
|
|
14
|
+
* So a `thresholdRadPerSec` does not exist here. A field that is absent cannot
|
|
15
|
+
* be filled with a contested number by someone who assumes the gap was an
|
|
16
|
+
* oversight — which is why this is a schema decision and not a convention.
|
|
17
|
+
*
|
|
18
|
+
* 🛑 `trigger` is FREE TEXT, not an enum. The three authored shapes are
|
|
19
|
+
* already a pre-launch setting, passive spin decay, and what surface the tip
|
|
20
|
+
* is running on — and a three-value enum written before the fourth case was
|
|
21
|
+
* read would have forced the next one into the wrong bucket.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How a mode is selected.
|
|
25
|
+
*
|
|
26
|
+
* ── 🛑 THIS ENUM IS THE OWNER'S TAXONOMY, NOT MY GUESS ────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Owner, 2026-10-21: *"most mode switches are either flips, config 1, 2 or 3
|
|
29
|
+
* and more."* Everything beyond that set is a mode that changes WITHOUT the
|
|
30
|
+
* owner doing anything, and those divide cleanly by what drives them.
|
|
31
|
+
*
|
|
32
|
+
* I deliberately did NOT define this when the first four modes were authored,
|
|
33
|
+
* because at that point I had seen two kinds and an enum built from two would
|
|
34
|
+
* have forced the next two into the wrong value. It is defined now that the
|
|
35
|
+
* real set is known — which is the difference between a taxonomy and a guess.
|
|
36
|
+
*/
|
|
37
|
+
export const BEY_MODE_SWITCH_KINDS = [
|
|
38
|
+
/** Turn the part over. Two states by definition. */
|
|
39
|
+
"flip",
|
|
40
|
+
/** An indexed assembly position — config 1, 2, 3… See `configIndex`. */
|
|
41
|
+
"config",
|
|
42
|
+
/** Swap in a different piece (Delta Drive's three tips). */
|
|
43
|
+
"swap",
|
|
44
|
+
/** Changes on its own as spin speed falls. Nothing to set. */
|
|
45
|
+
"speed",
|
|
46
|
+
/** Fires when the bey is struck (Kreis' halves slipping). */
|
|
47
|
+
"impact",
|
|
48
|
+
/*
|
|
49
|
+
* 🛑 There is deliberately NO "surface" kind. Its only candidate was the
|
|
50
|
+
* claim that Bandit Genbu's TB tip runs on a ball or a cylinder rim
|
|
51
|
+
* depending on the floor — and TB is "Tapered Ball", a FIXED SHAPE with no
|
|
52
|
+
* modes at all (owner, 2026-10-21). An enum value whose only instance turned
|
|
53
|
+
* out not to exist is the speculative infra this project forbids.
|
|
54
|
+
*/
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* The document id for a bey.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 The ONE place this is composed, mirroring `partGlossaryId`. The id is
|
|
60
|
+
* also the slug, the public URL and a `searchTxt` source on the paired
|
|
61
|
+
* category leaf, so a second spelling of this rule is a second set of URLs.
|
|
62
|
+
*/
|
|
63
|
+
export function beyId(slug) {
|
|
64
|
+
return slug;
|
|
65
|
+
}
|
|
@@ -6,6 +6,7 @@ import { getMediaUrl } from "../../media/types/index.js";
|
|
|
6
6
|
import { getDefaultLocale } from "../../../core/baseline-resolver.js";
|
|
7
7
|
import { safeDisplayName } from "../../../security/index.js";
|
|
8
8
|
import { useLongPress } from "../../../react/hooks/useLongPress.js";
|
|
9
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
9
10
|
const __P = {
|
|
10
11
|
p4: "p-[var(--appkit-space-4)]",
|
|
11
12
|
};
|
|
@@ -32,11 +33,23 @@ export function BlogFeaturedCard({ post, href, labels = {}, className = "", sele
|
|
|
32
33
|
})
|
|
33
34
|
: "";
|
|
34
35
|
const card = (_jsxs(Article, { rounded: "xl", shadow: "hover-md", className: `group relative h-full overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-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(post.id, !isSelected); }, label: isSelected ? "Deselect post" : "Select post", 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" })), _jsxs(TextLink, { href: href, layout: "flex-col", className: "h-full", children: [showThumbnail && (_jsx(Div, { className: `relative aspect-video ${__O.hidden} flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Div, { role: "img", "aria-label": safeTitle, surface: "muted", className: "h-full w-full" })) })), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { gap: "xs", className: "mb-2", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? "bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]"}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels.featuredBadge ?? "Featured" }))] }), _jsx(Heading, { level: 3, className: `leading-snug mb-1`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: safeTitle }), post.excerpt && (_jsx(Text, { className: `mb-3`, color: "muted", truncate: 2, size: "sm", children: post.excerpt })), _jsxs(Row, { color: "muted", textSize: "xs", className: "mt-auto", gap: "3", wrap: true, children: [showAuthor && post.authorName && (_jsx(Span, { children: safeDisplayName(post.authorName, "Author") })), showReadTime && post.readTimeMinutes != null && (_jsxs(Span, { children: [post.readTimeMinutes, " ", labels.readTime ?? "min read"] })), date && _jsx(Span, { children: date })] })] })] })] }));
|
|
35
|
-
|
|
36
|
+
/*
|
|
37
|
+
* 🛑 Default, not opt-in. Nothing in either repo ever passed
|
|
38
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
39
|
+
* mounts this card (Root Cause #8 — a slot shell rendered with zero render
|
|
40
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
41
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
42
|
+
*/
|
|
43
|
+
const ownerActions = renderOwnerActions
|
|
44
|
+
? renderOwnerActions()
|
|
45
|
+
: post.id
|
|
46
|
+
? (_jsx(RecordOwnerQuickActions, { resource: "BLOG", recordId: String(post.id), recordLabel: post.title, placement: "card" }))
|
|
47
|
+
: null;
|
|
48
|
+
if (!ownerActions)
|
|
36
49
|
return card;
|
|
37
50
|
/*
|
|
38
51
|
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
39
52
|
* beats this card's own overlays, which all sit at `z-10`.
|
|
40
53
|
*/
|
|
41
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children:
|
|
54
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
|
|
42
55
|
}
|
|
@@ -11,7 +11,7 @@ import { getMediaUrl } from "../../media/types/index.js";
|
|
|
11
11
|
import { getDefaultLocale } from "../../../core/baseline-resolver.js";
|
|
12
12
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
13
13
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
14
|
-
import {
|
|
14
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
15
15
|
const __P = {
|
|
16
16
|
p8: "p-[var(--appkit-space-8)]",
|
|
17
17
|
};
|
|
@@ -27,7 +27,7 @@ const CATEGORY_BADGE = {
|
|
|
27
27
|
};
|
|
28
28
|
const CLS_FEATURED_BADGE = "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] rounded-full";
|
|
29
29
|
function renderBlogPostHeader(post, date, labels) {
|
|
30
|
-
return (_jsxs(Div, { className: "mb-8", children: [_jsxs(Row, { className: "mb-4", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? ""}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && _jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels?.featured ?? "Featured" })] }), _jsxs(Row, { className: "mb-4", gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: post.title }), _jsx(
|
|
30
|
+
return (_jsxs(Div, { className: "mb-8", children: [_jsxs(Row, { className: "mb-4", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? ""}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && _jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels?.featured ?? "Featured" })] }), _jsxs(Row, { className: "mb-4", gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: post.title }), _jsx(RecordOwnerQuickActions, { resource: "BLOG", recordId: post.id, placement: "detail", recordLabel: post.title })] }), post.excerpt && _jsx(Text, { className: "mb-6", color: "muted", size: "lg", children: post.excerpt }), _jsxs(Row, { color: "muted", textSize: "sm", wrap: true, gap: "md", children: [post.authorName && _jsxs(Span, { children: [labels?.author ?? "By", " ", _jsx(Span, { weight: "medium", color: "muted", children: post.authorName })] }), post.readTimeMinutes != null && _jsxs(Span, { children: [post.readTimeMinutes, " ", labels?.readTime ?? "min read"] }), date && _jsxs(Span, { children: [labels?.publishedOn ?? "Published", " ", date] }), post.views != null && _jsxs(Span, { children: [post.views, " ", labels?.viewsLabel ?? "views"] })] })] }));
|
|
31
31
|
}
|
|
32
32
|
function renderBlogPostRelated(related, title, renderRelatedCard) {
|
|
33
33
|
if (related.length === 0)
|
|
@@ -5,7 +5,7 @@ import Link from "next/link.js";
|
|
|
5
5
|
import { categoriesRepository, productRepository, } from "../../../repositories/index.js";
|
|
6
6
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
7
|
import { Anchor, Container, Div, Dl, Dt, Dd, Heading, Main, Nav, Section, Span, Stack, Text } from "../../../ui/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
9
9
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
|
|
10
10
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
11
11
|
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
@@ -177,5 +177,7 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
177
177
|
const ldFaq = brand?.faqs && brand.faqs.length > 0
|
|
178
178
|
? faqJsonLd(brand.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
179
179
|
: null;
|
|
180
|
-
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 && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [brand?.id && (_jsx(
|
|
180
|
+
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 && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [brand?.id && (_jsx(RecordOwnerQuickActions, { resource: "BRAND", recordId: brand.id, placement: "detail",
|
|
181
|
+
/* See CategoryDetailPageView — ghost ink is invisible on the cover hero. */
|
|
182
|
+
onMedia: hasCover, className: "mb-2", recordLabel: brand.name ?? slug })), _jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 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: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 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: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 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: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
|
|
181
183
|
}
|
|
@@ -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.
|