@mohasinac/appkit 4.56.0 โ 4.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/beys/data.d.ts +44 -0
- package/dist/_internal/server/features/beys/data.js +107 -0
- package/dist/_internal/server/features/beys/og.d.ts +42 -0
- package/dist/_internal/server/features/beys/og.js +89 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
- package/dist/_internal/server/features/checkout/actions.js +117 -5
- package/dist/_internal/server/features/orders/actions.d.ts +30 -0
- package/dist/_internal/server/features/orders/actions.js +103 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +139 -0
- package/dist/_internal/shared/shipping/quote.js +5 -6
- package/dist/client.d.ts +23 -0
- package/dist/client.js +3 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +10 -0
- package/dist/constants/field-names.d.ts +46 -0
- package/dist/constants/field-names.js +46 -0
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/account/components/UserOrdersView.js +25 -3
- package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
- package/dist/features/admin/components/AdminBeysView.js +178 -0
- package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
- package/dist/features/admin/components/AdminPartsView.js +160 -0
- package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
- package/dist/features/admin/components/OwnerQuickActions.js +83 -8
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
- package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
- package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +12 -1
- package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
- package/dist/features/beys/components/BeyDetailPageView.js +98 -0
- package/dist/features/beys/schemas/firestore.d.ts +417 -0
- package/dist/features/beys/schemas/firestore.js +65 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +4 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
- package/dist/features/categories/components/CategoryGrid.js +24 -2
- package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/faq/components/FAQPageContent.js +17 -4
- package/dist/features/faq/constants/help-guides.d.ts +36 -0
- package/dist/features/faq/constants/help-guides.js +64 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
- package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +15 -0
- package/dist/features/parts/components/PartDetailPageView.js +5 -3
- package/dist/features/parts/components/PartsSearchBox.js +79 -4
- package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
- package/dist/features/parts/schemas/admin-edit.js +89 -0
- package/dist/features/parts/schemas/firestore.d.ts +2 -1
- package/dist/features/points/repository/points.repository.js +5 -5
- package/dist/features/points/schemas/firestore.d.ts +16 -0
- package/dist/features/points/schemas/firestore.js +17 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
- package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
- package/dist/features/products/components/ProductGrid.js +18 -2
- package/dist/features/products/schemas/firestore.d.ts +18 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
- package/dist/features/seller/components/SellerProductShell.js +31 -3
- package/dist/features/stores/components/StoreHeader.js +3 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +17 -1
- package/dist/next/routing/route-map.js +39 -0
- package/dist/schemas/registry.d.ts +8 -8
- package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
- package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
- package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
- package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
- package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
- package/dist/seed/_helpers/category-models.d.ts +9 -2
- package/dist/seed/_helpers/category-models.js +19 -1368
- package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
- package/dist/seed/_helpers/category-parts.js +39 -39
- package/dist/seed/bey-products-seed-data.d.ts +18 -0
- package/dist/seed/bey-products-seed-data.js +18011 -0
- package/dist/seed/beys/chunk0.d.ts +17 -0
- package/dist/seed/beys/chunk0.js +36733 -0
- package/dist/seed/beys/chunk1.d.ts +17 -0
- package/dist/seed/beys/chunk1.js +35728 -0
- package/dist/seed/beys/chunk2.d.ts +17 -0
- package/dist/seed/beys/chunk2.js +35857 -0
- package/dist/seed/beys/chunk3.d.ts +17 -0
- package/dist/seed/beys/chunk3.js +34476 -0
- package/dist/seed/beys-seed-data.d.ts +18 -0
- package/dist/seed/beys-seed-data.js +11 -0
- package/dist/seed/homepage-sections-seed-data.js +16 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/media-assets-seed-data.js +4 -4
- package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk0.js +7262 -0
- package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk1.js +7290 -0
- package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk10.js +7290 -0
- package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk11.js +7005 -0
- package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk12.js +7002 -0
- package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk13.js +7002 -0
- package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk14.js +7002 -0
- package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk15.js +7002 -0
- package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk16.js +7002 -0
- package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk17.js +7002 -0
- package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk18.js +4398 -0
- package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk2.js +7268 -0
- package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk3.js +7264 -0
- package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk4.js +7268 -0
- package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk5.js +7258 -0
- package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk6.js +7268 -0
- package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk7.js +7311 -0
- package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk8.js +7304 -0
- package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk9.js +7309 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
- package/dist/seed/products-catalogue-seed-data.js +58 -86382
- package/dist/seed/seed-purpose.js +9 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +19 -0
- package/dist/styles.css +468 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PosterCard.d.ts +99 -0
- package/dist/ui/components/PosterCard.js +109 -0
- package/dist/ui/components/PosterCard.style.css +466 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +108 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The client half of `OwnerQuickActions` for every admin-owned collection.
|
|
5
|
+
*
|
|
6
|
+
* ## ๐ Why a wrapper is REQUIRED, not a convenience
|
|
7
|
+
*
|
|
8
|
+
* `OwnerQuickActions` takes `onDelete` / `onClone` as FUNCTIONS, and every
|
|
9
|
+
* detail view that renders it (`CategoryDetailPageView`, `BrandDetailPageView`,
|
|
10
|
+
* `BundleDetailView`, โฆ) is a Server Component. A closure cannot cross that
|
|
11
|
+
* boundary โ React throws
|
|
12
|
+
*
|
|
13
|
+
* Event handlers cannot be passed to Client Component props.
|
|
14
|
+
*
|
|
15
|
+
* at render. `ProductOwnerQuickActions` records that this exact mistake took
|
|
16
|
+
* down every `/products/[slug]` page in production, with an opaque digest per
|
|
17
|
+
* handler.
|
|
18
|
+
*
|
|
19
|
+
* That is why ten detail views were passing `editHref` and nothing else: the
|
|
20
|
+
* bin and the clone are rendered behind `{showDelete && โฆ}` / `{showClone && โฆ}`,
|
|
21
|
+
* so with no handler they silently do not exist. It read as "admins get no
|
|
22
|
+
* delete button", and the cause was a serialization boundary, not a permission.
|
|
23
|
+
*
|
|
24
|
+
* ## ๐ Why ONE wrapper and not one per resource
|
|
25
|
+
*
|
|
26
|
+
* `ProductOwnerQuickActions` is resource-specific because products are the one
|
|
27
|
+
* collection a SELLER can also own, so its endpoints live under
|
|
28
|
+
* `SELLER_ENDPOINTS` and its clone has a bespoke redirect to the copy's editor.
|
|
29
|
+
* Everything here is admin-owned and differs only by three strings, so eight
|
|
30
|
+
* near-identical files would trip the Duplication Framework's Rule of Three
|
|
31
|
+
* immediately. One table, one component.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the redirect lands on the ADMIN list
|
|
34
|
+
*
|
|
35
|
+
* You deleted the record from its own public page, so that page is now a 404 โ
|
|
36
|
+
* staying is not an option and the public index tells you nothing about whether
|
|
37
|
+
* the delete worked. The admin list is where the absence is verifiable.
|
|
38
|
+
* `ProductOwnerQuickActions` differs deliberately: a seller may delete a
|
|
39
|
+
* product and has no admin list to land on.
|
|
40
|
+
*
|
|
41
|
+
* @tag domain:admin
|
|
42
|
+
* @tag layer:components
|
|
43
|
+
* @tag access:client
|
|
44
|
+
*/
|
|
45
|
+
import { useCallback } from "react";
|
|
46
|
+
import { OwnerQuickActions } from "./OwnerQuickActions.js";
|
|
47
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
48
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
49
|
+
const WIRING = {
|
|
50
|
+
CATEGORY: {
|
|
51
|
+
endpoint: ADMIN_ENDPOINTS.CATEGORY_BY_ID,
|
|
52
|
+
editHref: (id) => String(ROUTES.ADMIN.CATEGORIES_EDIT(id)),
|
|
53
|
+
afterDelete: String(ROUTES.ADMIN.CATEGORIES),
|
|
54
|
+
},
|
|
55
|
+
PART: {
|
|
56
|
+
endpoint: ADMIN_ENDPOINTS.PART_BY_ID,
|
|
57
|
+
editHref: (id) => String(ROUTES.ADMIN.PARTS_EDIT(id)),
|
|
58
|
+
afterDelete: String(ROUTES.ADMIN.PARTS),
|
|
59
|
+
},
|
|
60
|
+
BEY: {
|
|
61
|
+
endpoint: ADMIN_ENDPOINTS.BEY_BY_ID,
|
|
62
|
+
editHref: (id) => String(ROUTES.ADMIN.BEYS_EDIT(id)),
|
|
63
|
+
afterDelete: String(ROUTES.ADMIN.BEYS),
|
|
64
|
+
},
|
|
65
|
+
BRAND: {
|
|
66
|
+
endpoint: ADMIN_ENDPOINTS.BRAND_BY_ID,
|
|
67
|
+
editHref: (id) => String(ROUTES.ADMIN.BRANDS_EDIT(id)),
|
|
68
|
+
afterDelete: String(ROUTES.ADMIN.BRANDS),
|
|
69
|
+
},
|
|
70
|
+
BUNDLE: {
|
|
71
|
+
endpoint: ADMIN_ENDPOINTS.BUNDLE_BY_ID,
|
|
72
|
+
editHref: (id) => String(ROUTES.ADMIN.BUNDLES_EDIT(id)),
|
|
73
|
+
afterDelete: String(ROUTES.ADMIN.BUNDLES),
|
|
74
|
+
},
|
|
75
|
+
GROUP: {
|
|
76
|
+
endpoint: ADMIN_ENDPOINTS.GROUPED_LISTING_BY_ID,
|
|
77
|
+
editHref: (id) => String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(id)),
|
|
78
|
+
afterDelete: String(ROUTES.ADMIN.GROUPED_LISTINGS),
|
|
79
|
+
},
|
|
80
|
+
EVENT: {
|
|
81
|
+
endpoint: ADMIN_ENDPOINTS.EVENT_BY_ID,
|
|
82
|
+
editHref: (id) => String(ROUTES.ADMIN.EVENTS_EDIT(id)),
|
|
83
|
+
afterDelete: String(ROUTES.ADMIN.EVENTS),
|
|
84
|
+
},
|
|
85
|
+
BLOG: {
|
|
86
|
+
endpoint: ADMIN_ENDPOINTS.BLOG_BY_ID,
|
|
87
|
+
editHref: (id) => String(ROUTES.ADMIN.BLOG_EDIT(id)),
|
|
88
|
+
afterDelete: String(ROUTES.ADMIN.BLOG),
|
|
89
|
+
},
|
|
90
|
+
/*
|
|
91
|
+
* A franchise row IS a category (`categoryType: "franchise"`), so it shares
|
|
92
|
+
* the category endpoint and editor โ the same reasoning `RESOURCE_LEAVES`
|
|
93
|
+
* uses to give it the category copy. Only the landing page differs.
|
|
94
|
+
*/
|
|
95
|
+
FRANCHISE: {
|
|
96
|
+
endpoint: ADMIN_ENDPOINTS.CATEGORY_BY_ID,
|
|
97
|
+
editHref: (id) => String(ROUTES.ADMIN.CATEGORIES_EDIT(id)),
|
|
98
|
+
afterDelete: String(ROUTES.ADMIN.CATEGORIES),
|
|
99
|
+
},
|
|
100
|
+
SUBLISTING: {
|
|
101
|
+
endpoint: ADMIN_ENDPOINTS.SUBLISTING_CATEGORY_BY_ID,
|
|
102
|
+
editHref: (id) => String(ROUTES.ADMIN.SUBLISTING_CATEGORIES_EDIT(id)),
|
|
103
|
+
afterDelete: String(ROUTES.ADMIN.SUBLISTING_CATEGORIES),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
export function RecordOwnerQuickActions({ resource, recordId, recordLabel, ownerId, storeId, placement = "detail", onMedia, className, }) {
|
|
107
|
+
const wiring = WIRING[resource];
|
|
108
|
+
/*
|
|
109
|
+
* ๐ THROWS on a failed DELETE rather than returning quietly.
|
|
110
|
+
*
|
|
111
|
+
* `OwnerQuickActions.handleConfirmedDelete` catches and toasts, and success
|
|
112
|
+
* navigates away โ so a swallowed failure is indistinguishable from "it
|
|
113
|
+
* worked but the page did not move", and the obvious user response is to
|
|
114
|
+
* press the button again. Returning early on `!res.ok` is the bug
|
|
115
|
+
* `ProductOwnerQuickActions` documents having had.
|
|
116
|
+
*/
|
|
117
|
+
const handleDelete = useCallback(async () => {
|
|
118
|
+
const res = await fetch(wiring.endpoint(recordId), { method: "DELETE" });
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const body = (await res.json().catch(() => null));
|
|
121
|
+
throw Object.assign(new Error("Delete failed"), { code: body?.code });
|
|
122
|
+
}
|
|
123
|
+
if (typeof window !== "undefined") {
|
|
124
|
+
window.location.assign(wiring.afterDelete);
|
|
125
|
+
}
|
|
126
|
+
}, [wiring, recordId]);
|
|
127
|
+
return (_jsx(OwnerQuickActions, { resource: resource, placement: placement, onMedia: onMedia, className: className, recordLabel: recordLabel, ownerId: ownerId, storeId: storeId, editHref: wiring.editHref(recordId), onDelete: handleDelete }));
|
|
128
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReferenceRecordEditor โ correct one row of a GENERATED reference database.
|
|
3
|
+
*
|
|
4
|
+
* โโ ๐ ONE COMPONENT, TWO FIELD LISTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
*
|
|
6
|
+
* Parts and beys differ only in WHICH fields a human may own. Loading the row,
|
|
7
|
+
* PATCHing it, surfacing a server error on the right field, and telling the
|
|
8
|
+
* operator what a correction means for the next reseed are identical โ so they
|
|
9
|
+
* are written once. The Duplication Framework's "same prop surface, same
|
|
10
|
+
* return shape" test points here, and the alternative was two files whose
|
|
11
|
+
* error handling would drift.
|
|
12
|
+
*
|
|
13
|
+
* โโ ๐ WHAT THIS DELIBERATELY IS NOT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
14
|
+
*
|
|
15
|
+
* It is not a create form. Both collections are generated, so a row authored
|
|
16
|
+
* by hand has no source and vanishes on the next regeneration. There is no
|
|
17
|
+
* code path here that POSTs.
|
|
18
|
+
*
|
|
19
|
+
* It also shows no DERIVED field as editable. A bey's summed mass, its ratings
|
|
20
|
+
* and its coverage records are computed from the composition and each carries a
|
|
21
|
+
* coverage claim; a hand-typed derived value is a number whose stated
|
|
22
|
+
* provenance is no longer true.
|
|
23
|
+
*
|
|
24
|
+
* @tag domain:admin
|
|
25
|
+
* @tag layer:feature
|
|
26
|
+
* @tag access:client
|
|
27
|
+
*/
|
|
28
|
+
import React from "react";
|
|
29
|
+
/** One editable field. `kind` picks the input, nothing more. */
|
|
30
|
+
export interface ReferenceEditorField {
|
|
31
|
+
name: string;
|
|
32
|
+
label: string;
|
|
33
|
+
kind?: "text" | "number";
|
|
34
|
+
hint?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ReferenceRecordEditorProps {
|
|
37
|
+
/** `ADMIN_ENDPOINTS.PART_BY_ID(id)` / `BEY_BY_ID(id)` โ already applied. */
|
|
38
|
+
endpoint: string;
|
|
39
|
+
/** What the record is called, for copy. "part" / "Beyblade". */
|
|
40
|
+
noun: string;
|
|
41
|
+
fields: readonly ReferenceEditorField[];
|
|
42
|
+
/** Where the reference page for this record lives, so the operator can check their work. */
|
|
43
|
+
publicHref?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function ReferenceRecordEditor({ endpoint, noun, fields, publicHref, }: ReferenceRecordEditorProps): React.JSX.Element;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* ReferenceRecordEditor โ correct one row of a GENERATED reference database.
|
|
5
|
+
*
|
|
6
|
+
* โโ ๐ ONE COMPONENT, TWO FIELD LISTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
7
|
+
*
|
|
8
|
+
* Parts and beys differ only in WHICH fields a human may own. Loading the row,
|
|
9
|
+
* PATCHing it, surfacing a server error on the right field, and telling the
|
|
10
|
+
* operator what a correction means for the next reseed are identical โ so they
|
|
11
|
+
* are written once. The Duplication Framework's "same prop surface, same
|
|
12
|
+
* return shape" test points here, and the alternative was two files whose
|
|
13
|
+
* error handling would drift.
|
|
14
|
+
*
|
|
15
|
+
* โโ ๐ WHAT THIS DELIBERATELY IS NOT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
16
|
+
*
|
|
17
|
+
* It is not a create form. Both collections are generated, so a row authored
|
|
18
|
+
* by hand has no source and vanishes on the next regeneration. There is no
|
|
19
|
+
* code path here that POSTs.
|
|
20
|
+
*
|
|
21
|
+
* It also shows no DERIVED field as editable. A bey's summed mass, its ratings
|
|
22
|
+
* and its coverage records are computed from the composition and each carries a
|
|
23
|
+
* coverage claim; a hand-typed derived value is a number whose stated
|
|
24
|
+
* provenance is no longer true.
|
|
25
|
+
*
|
|
26
|
+
* @tag domain:admin
|
|
27
|
+
* @tag layer:feature
|
|
28
|
+
* @tag access:client
|
|
29
|
+
*/
|
|
30
|
+
import React from "react";
|
|
31
|
+
import { Alert, Button, Form, FieldInput, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
32
|
+
import { apiClient } from "../../../http/index.js";
|
|
33
|
+
import { useToast } from "../../../ui/components/toast-context.js";
|
|
34
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
35
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
36
|
+
export function ReferenceRecordEditor({ endpoint, noun, fields, publicHref, }) {
|
|
37
|
+
const { showToast } = useToast();
|
|
38
|
+
const [values, setValues] = React.useState({});
|
|
39
|
+
const [owned, setOwned] = React.useState([]);
|
|
40
|
+
const [loading, setLoading] = React.useState(true);
|
|
41
|
+
const [saving, setSaving] = React.useState(false);
|
|
42
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const row = await apiClient.get(endpoint);
|
|
48
|
+
if (cancelled)
|
|
49
|
+
return;
|
|
50
|
+
const next = {};
|
|
51
|
+
for (const f of fields) {
|
|
52
|
+
const v = row?.[f.name];
|
|
53
|
+
/*
|
|
54
|
+
* ๐ An ABSENT field becomes an empty string, not "undefined". Both
|
|
55
|
+
* render, and only one of them is honest โ and on save an empty
|
|
56
|
+
* string is dropped rather than written, so an untouched empty field
|
|
57
|
+
* cannot blank a value the generator owns.
|
|
58
|
+
*/
|
|
59
|
+
next[f.name] = v === null || v === undefined ? "" : String(v);
|
|
60
|
+
}
|
|
61
|
+
setValues(next);
|
|
62
|
+
setOwned(Array.isArray(row?.adminEditedFields) ? row.adminEditedFields : []);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const e = normalizeError(err);
|
|
66
|
+
if (!cancelled)
|
|
67
|
+
setLoadError(e.message);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (!cancelled)
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
return () => {
|
|
75
|
+
cancelled = true;
|
|
76
|
+
};
|
|
77
|
+
}, [endpoint, fields]);
|
|
78
|
+
/**
|
|
79
|
+
* Build the PATCH body.
|
|
80
|
+
*
|
|
81
|
+
* ๐ Only NON-EMPTY values are sent, and numbers are coerced.
|
|
82
|
+
*
|
|
83
|
+
* An empty input means "I did not set this", not "write an empty string".
|
|
84
|
+
* Sending it would both blank a real value AND mark the field as
|
|
85
|
+
* admin-owned, freezing the blank in place against every future reseed โ
|
|
86
|
+
* a one-click way to permanently delete data from a generated row.
|
|
87
|
+
*/
|
|
88
|
+
const buildPatch = React.useCallback(() => {
|
|
89
|
+
const patch = {};
|
|
90
|
+
for (const f of fields) {
|
|
91
|
+
const raw = (values[f.name] ?? "").trim();
|
|
92
|
+
if (!raw)
|
|
93
|
+
continue;
|
|
94
|
+
patch[f.name] = f.kind === "number" ? Number(raw) : raw;
|
|
95
|
+
}
|
|
96
|
+
return patch;
|
|
97
|
+
}, [fields, values]);
|
|
98
|
+
/**
|
|
99
|
+
* Put a failure where the operator can act on it.
|
|
100
|
+
*
|
|
101
|
+
* ๐ Zod issues go to their OWN FIELD; everything else becomes one toast
|
|
102
|
+
* built by `toUserMessage`. Never the server's own message: a raw fallback
|
|
103
|
+
* is what rendered a Node MODULE_NOT_FOUND with a full require stack inside
|
|
104
|
+
* a bid modal (Root Cause #86), and `mapToHttpError` scrubs 5xx text in
|
|
105
|
+
* production anyway.
|
|
106
|
+
*/
|
|
107
|
+
const reportFailure = React.useCallback((err,
|
|
108
|
+
/** Already normalized by the caller โ see the note at the catch site. */
|
|
109
|
+
fallbackMessage, setFieldError) => {
|
|
110
|
+
const issues = err?.issues;
|
|
111
|
+
if (issues?.length) {
|
|
112
|
+
for (const issue of issues)
|
|
113
|
+
setFieldError(String(issue.path?.[0] ?? ""), issue.message);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const code = err?.code;
|
|
117
|
+
const message = code ? toUserMessage(code, undefined) : fallbackMessage;
|
|
118
|
+
showToast(message, "error");
|
|
119
|
+
}, [showToast]);
|
|
120
|
+
const save = React.useCallback(async (setFieldError, clearErrors) => {
|
|
121
|
+
clearErrors();
|
|
122
|
+
setSaving(true);
|
|
123
|
+
try {
|
|
124
|
+
await apiClient.patch(endpoint, buildPatch());
|
|
125
|
+
showToast(`${noun[0].toUpperCase()}${noun.slice(1)} updated.`, "success");
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const e = normalizeError(err);
|
|
129
|
+
reportFailure(err, e.message, setFieldError);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
setSaving(false);
|
|
133
|
+
}
|
|
134
|
+
}, [endpoint, buildPatch, noun, showToast, reportFailure]);
|
|
135
|
+
if (loading)
|
|
136
|
+
return _jsx(Text, { color: "muted", children: "Loading\u2026" });
|
|
137
|
+
if (loadError) {
|
|
138
|
+
return (_jsx(Alert, { variant: "error", title: `Could not load this ${noun}`, children: _jsx(Text, { size: "sm", children: loadError }) }));
|
|
139
|
+
}
|
|
140
|
+
/*
|
|
141
|
+
* ๐ No `className` on `<Form>` โ the `<Stack gap="md">` inside already owns
|
|
142
|
+
* the spacing, so `space-y-4` was a second, competing source of it
|
|
143
|
+
* (audit-variant-prop-coverage: a layout token belongs in a variant prop).
|
|
144
|
+
*
|
|
145
|
+
* ๐ And this is a PLAIN comment, above the `return`, not a `{/* โฆ *โฃ/}` JSX
|
|
146
|
+
* one below it. I wrote it as JSX first and it broke the parse: a JSX comment
|
|
147
|
+
* between `return (` and the root element is a second expression, so the
|
|
148
|
+
* return has two children and no wrapper. `tsc` caught it โ the first
|
|
149
|
+
* typecheck this session was allowed to run โ which is exactly the class of
|
|
150
|
+
* error the 4 GB no-build cap was hiding.
|
|
151
|
+
*/
|
|
152
|
+
return (_jsx(Form, { children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsxs(Alert, { variant: "info", title: "This record is generated", children: [_jsxs(Text, { size: "sm", children: [`This ${noun} is built from a crawled source by a committed generator. A field you `, "change here is marked as yours and will not be overwritten the next time the database is regenerated \u2014 every field you leave alone keeps getting the generator's improvements."] }), owned.length > 0 && (_jsx(Text, { size: "sm", color: "muted", children: `Already corrected by hand: ${owned.join(", ")}` }))] }), fields.map((f) => (_jsx(FieldInput, { name: f.name, label: f.label, type: f.kind === "number" ? "number" : "text", hint: f.hint, value: values[f.name] ?? "", onChange: (v) => setValues((p) => ({ ...p, [f.name]: v })) }, f.name))), _jsx(Button, { type: "button", isLoading: saving, onClick: () => save(setFieldError, clearErrors), children: "Save correction" }), publicHref && (_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: publicHref, children: "View the public reference page" }) }))] })) }));
|
|
153
|
+
}
|
|
@@ -73,7 +73,7 @@ export type NotificationUpdateInput = Partial<Pick<NotificationDocument, "isRead
|
|
|
73
73
|
export declare const notificationQueryHelpers: {
|
|
74
74
|
readonly byUser: (userId: string) => readonly ["userId", "==", string];
|
|
75
75
|
readonly unread: (userId: string) => readonly [readonly ["userId", "==", string], readonly ["isRead", "==", false]];
|
|
76
|
-
readonly byType: (type: NotificationType) => readonly ["type", "==", "welcome" | "promotion" | "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "auction_ended" | "review_approved" | "review_replied" | "product_available" | "
|
|
76
|
+
readonly byType: (type: NotificationType) => readonly ["type", "==", "system" | "welcome" | "promotion" | "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "auction_ended" | "review_approved" | "review_replied" | "product_available" | "account_action" | "support_ticket_update" | "scam_report_update" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated" | "prize_won" | "prize_reveal_expired" | "emi_installment_due_soon" | "emi_installment_overdue" | "payment_review" | "catalogue_images_stale" | "message_received"];
|
|
77
77
|
};
|
|
78
78
|
export interface TrustBarItem {
|
|
79
79
|
icon: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ProductOwnerQuickActions } from "../../products/components/ProductOwnerQuickActions.js";
|
|
3
4
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
4
5
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
6
|
import { useRouter } from "next/navigation.js";
|
|
@@ -181,11 +182,23 @@ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", v
|
|
|
181
182
|
countdownClass, mergedLabels, handleWishlist, handleNavigate,
|
|
182
183
|
};
|
|
183
184
|
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, isDisabled: isEnded, variant: baseVariant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [renderAuctionCardHero({ product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect }), _jsx(BaseListingCard.Info, { variant: baseVariant, children: baseVariant === "list" ? renderAuctionCardInfoList(cardInfoProps) : renderAuctionCardInfoGrid(cardInfoProps) })] }));
|
|
184
|
-
|
|
185
|
+
/*
|
|
186
|
+
* ๐ Default, not opt-in. Nothing in either repo ever passed
|
|
187
|
+
* `renderOwnerActions`, so this toolbar was unreachable on every grid that
|
|
188
|
+
* mounts this card (Root Cause #8 โ a slot shell rendered with zero render
|
|
189
|
+
* props). A card is resource-specific by construction, so it names its own
|
|
190
|
+
* resource rather than being threaded one; the prop stays as an override.
|
|
191
|
+
*/
|
|
192
|
+
const ownerActions = renderOwnerActions
|
|
193
|
+
? renderOwnerActions()
|
|
194
|
+
: product.id
|
|
195
|
+
? (_jsx(ProductOwnerQuickActions, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, recordLabel: product.title, placement: "card" }))
|
|
196
|
+
: null;
|
|
197
|
+
if (!ownerActions)
|
|
185
198
|
return card;
|
|
186
199
|
/*
|
|
187
200
|
* Sibling of the card, never a child โ see the prop's docstring. `z-20`
|
|
188
201
|
* clears this card's own overlays, which all sit at `z-10`.
|
|
189
202
|
*/
|
|
190
|
-
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children:
|
|
203
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: ownerActions })] }));
|
|
191
204
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Permission naming: admin:resource:action
|
|
12
12
|
* Ban action naming: plain verb phrase โ write_reviews, place_bids, etc.
|
|
13
13
|
*/
|
|
14
|
-
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read"
|
|
14
|
+
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:parts:read" | "admin:parts:write" | "admin:parts:delete" | "admin:beys:read" | "admin:beys:write" | "admin:beys:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read"
|
|
15
15
|
/** Destructive/mutating contact-submission actions. Added 2026-08-29 โ
|
|
16
16
|
* `DELETE /api/admin/contact-submissions/[id]` declared `admin:contact:delete`,
|
|
17
17
|
* which is not a member of this union and therefore could never be granted. */
|
|
@@ -131,6 +131,12 @@ export const PERMISSION_GROUPS = {
|
|
|
131
131
|
"admin:categories:read",
|
|
132
132
|
"admin:categories:write",
|
|
133
133
|
"admin:categories:delete",
|
|
134
|
+
"admin:parts:read",
|
|
135
|
+
"admin:parts:write",
|
|
136
|
+
"admin:parts:delete",
|
|
137
|
+
"admin:beys:read",
|
|
138
|
+
"admin:beys:write",
|
|
139
|
+
"admin:beys:delete",
|
|
134
140
|
"admin:brands:read",
|
|
135
141
|
"admin:brands:write",
|
|
136
142
|
"admin:brands:delete",
|
|
@@ -281,6 +287,8 @@ export const ROUTE_PERMISSION_MAP = {
|
|
|
281
287
|
"/admin/catalogue-approvals": "admin:catalogue:read",
|
|
282
288
|
"/admin/grouped-listings": "admin:grouped-listings:read",
|
|
283
289
|
"/admin/categories": "admin:categories:read",
|
|
290
|
+
"/admin/parts": "admin:parts:read",
|
|
291
|
+
"/admin/beys": "admin:beys:read",
|
|
284
292
|
"/admin/brands": "admin:brands:read",
|
|
285
293
|
"/admin/coupons": "admin:coupons:read",
|
|
286
294
|
"/admin/deals": "admin:deals:read",
|
|
@@ -437,7 +445,10 @@ export const PERMISSION_DOMAINS = [
|
|
|
437
445
|
{ label: "Orders & Returns", prefix: "admin:orders:|admin:returns:|admin:offers:" },
|
|
438
446
|
{ label: "Stores", prefix: "admin:stores:|admin:store-addresses:" },
|
|
439
447
|
{ label: "Finance", prefix: "admin:analytics:|admin:payouts:" },
|
|
440
|
-
{
|
|
448
|
+
{
|
|
449
|
+
label: "Catalog",
|
|
450
|
+
prefix: "admin:categories:|admin:parts:|admin:beys:|admin:brands:|admin:coupons:|admin:deals:|admin:featured:",
|
|
451
|
+
},
|
|
441
452
|
{
|
|
442
453
|
label: "Content",
|
|
443
454
|
prefix: "admin:reviews:|admin:blog:|admin:bids:|admin:media:",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One bey's public page โ what it is, what it is built from, who used it.
|
|
3
|
+
*
|
|
4
|
+
* โโ ๐ NO MONEY ON THIS PAGE, DELIBERATELY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
*
|
|
6
|
+
* Prices are hidden from signed-out visitors, so any amount here would need
|
|
7
|
+
* `<GatedPrice>` and make this a client component. Listing rows carry a title,
|
|
8
|
+
* a condition and a link; the price lives one click away on the listing, where
|
|
9
|
+
* the gate already is. Same rule as `PartDetailPageView` and `/search`: a row
|
|
10
|
+
* with nothing to gate has nothing to leak, and no `offers.price` is emitted
|
|
11
|
+
* so no paywall declaration is needed either.
|
|
12
|
+
*
|
|
13
|
+
* ๐ `price` IS on the document and is deliberately not rendered. It is the
|
|
14
|
+
* source's original retail price (`1404ยฅ`), not our listing price โ showing it
|
|
15
|
+
* beside real listings would read as an offer.
|
|
16
|
+
*
|
|
17
|
+
* โโ ๐ THE SOURCE LINK IS NOT OPTIONAL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
|
+
*
|
|
19
|
+
* The facts here are extracted from a CC-BY-SA corpus. Taking facts only and
|
|
20
|
+
* linking back is what keeps us clear of share-alike, and `sourceUrl` was
|
|
21
|
+
* stored on ~5,500 rows while being rendered on NONE of them. Storing it is
|
|
22
|
+
* not discharging the obligation.
|
|
23
|
+
*/
|
|
24
|
+
import React from "react";
|
|
25
|
+
import type { BeyDocument, BeyProductDocument } from "../schemas/firestore";
|
|
26
|
+
/** One live listing row. Deliberately money-free โ see the header. */
|
|
27
|
+
export interface BeyListingRow {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
slug?: string;
|
|
31
|
+
condition?: string;
|
|
32
|
+
href: string;
|
|
33
|
+
}
|
|
34
|
+
export interface BeyDetailPageViewProps {
|
|
35
|
+
bey: BeyDocument;
|
|
36
|
+
/** Products whose assortment contains this bey. */
|
|
37
|
+
products: readonly BeyProductDocument[];
|
|
38
|
+
/** Live listings, from the shared public product query. */
|
|
39
|
+
listings: readonly BeyListingRow[];
|
|
40
|
+
/** `total` is a FLOOR when the bounded fetch saturated. */
|
|
41
|
+
listingsTruncated?: boolean;
|
|
42
|
+
allListingsHref: string;
|
|
43
|
+
/** A part's own page, when the composition entry resolved to a glossary id. */
|
|
44
|
+
hrefForPart: (partId: string) => string | null;
|
|
45
|
+
/** This bey's category leaf, when one exists (116 beys have no leaf). */
|
|
46
|
+
categoryHref?: string | null;
|
|
47
|
+
/** A character's page, for a resolved owner. */
|
|
48
|
+
hrefForCharacter?: (slug: string) => string;
|
|
49
|
+
}
|
|
50
|
+
export declare function BeyDetailPageView({ bey, products, listings, listingsTruncated, allListingsHref, hrefForPart, categoryHref, hrefForCharacter, }: BeyDetailPageViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Anchor, Badge, } from "../../../ui/index.js";
|
|
3
|
+
import { SpecTable } from "../../../ui/components/SpecTable.js";
|
|
4
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
5
|
+
import { PART_SLOT_LABEL } from "../../parts/schemas/firestore.js";
|
|
6
|
+
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
7
|
+
/**
|
|
8
|
+
* The rating axes, in a fixed order.
|
|
9
|
+
*
|
|
10
|
+
* ๐ ORDERED EXPLICITLY, never by `Object.keys`. Firestore returns map fields
|
|
11
|
+
* alphabetically, which would put "burst" and "dash" above "attack" and
|
|
12
|
+
* reorder the table per bey depending on which axes it happens to carry โ so
|
|
13
|
+
* two beys could not be compared row against row.
|
|
14
|
+
*/
|
|
15
|
+
const RATING_AXES = [
|
|
16
|
+
{ key: "attack", label: "Attack" },
|
|
17
|
+
{ key: "defense", label: "Defense" },
|
|
18
|
+
{ key: "stamina", label: "Stamina" },
|
|
19
|
+
{ key: "weight", label: "Weight" },
|
|
20
|
+
{ key: "speed", label: "Speed" },
|
|
21
|
+
{ key: "burst", label: "Burst" },
|
|
22
|
+
{ key: "burstResistance", label: "Burst resistance" },
|
|
23
|
+
{ key: "dash", label: "Dash" },
|
|
24
|
+
{ key: "power", label: "Power" },
|
|
25
|
+
{ key: "height", label: "Height" },
|
|
26
|
+
{ key: "runTime", label: "Run time" },
|
|
27
|
+
];
|
|
28
|
+
export function BeyDetailPageView({ bey, products, listings, listingsTruncated, allListingsHref, hrefForPart, categoryHref, hrefForCharacter, }) {
|
|
29
|
+
/*
|
|
30
|
+
* โโ THE SPEC TABLE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
31
|
+
*
|
|
32
|
+
* Only what the source states. ๐ A row is omitted when the fact is absent
|
|
33
|
+
* rather than rendered with an em-dash: "Element โ" would claim we looked it
|
|
34
|
+
* up and found none, and `element` is on 45 of 1,988 beys precisely because
|
|
35
|
+
* it is a Beyblade-X-only attribute, not missing data.
|
|
36
|
+
*/
|
|
37
|
+
const specRows = [
|
|
38
|
+
...(bey.type ? [{ name: "Type", value: bey.type }] : []),
|
|
39
|
+
...(bey.spinDirection ? [{ name: "Spin", value: bey.spinDirection }] : []),
|
|
40
|
+
...(bey.system ? [{ name: "System", value: bey.system }] : []),
|
|
41
|
+
...(bey.series ? [{ name: "Series", value: bey.series }] : []),
|
|
42
|
+
...(bey.element ? [{ name: "Element", value: bey.element }] : []),
|
|
43
|
+
/*
|
|
44
|
+
* ๐ MASS: the stated figure wins, and a SUMMED one is labelled as summed
|
|
45
|
+
* and marked approximate when the parts are only partly weighed. Printing
|
|
46
|
+
* a partial sum as a plain total is the one thing this must not do โ the
|
|
47
|
+
* generator deliberately refuses to scale it up for the same reason.
|
|
48
|
+
*/
|
|
49
|
+
...(typeof bey.weightG === "number"
|
|
50
|
+
? [{ name: "Weight", value: String(bey.weightG), unit: "g" }]
|
|
51
|
+
: typeof bey.weightGFromParts === "number"
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
name: "Weight",
|
|
55
|
+
value: String(bey.weightGFromParts),
|
|
56
|
+
unit: bey.weightGFromPartsCoverage?.complete
|
|
57
|
+
? "g (summed from parts)"
|
|
58
|
+
: `g (from ${bey.weightGFromPartsCoverage?.weighed ?? 0} of ` +
|
|
59
|
+
`${bey.weightGFromPartsCoverage?.total ?? 0} parts)`,
|
|
60
|
+
approximate: !bey.weightGFromPartsCoverage?.complete,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
...(bey.productCodes?.length
|
|
65
|
+
? [{ name: bey.productCodes.length > 1 ? "Product codes" : "Product code", value: bey.productCodes.join(" ยท ") }]
|
|
66
|
+
: []),
|
|
67
|
+
...(bey.releaseJP ? [{ name: "Released (JP)", value: bey.releaseJP }] : []),
|
|
68
|
+
...(bey.releaseUS ? [{ name: "Released (US)", value: bey.releaseUS }] : []),
|
|
69
|
+
...(bey.jpName ? [{ name: "Japanese name", value: bey.jpName }] : []),
|
|
70
|
+
...(bey.romaji ? [{ name: "Romaji", value: bey.romaji }] : []),
|
|
71
|
+
...(bey.creator ? [{ name: "Creator", value: bey.creator }] : []),
|
|
72
|
+
];
|
|
73
|
+
const ratings = bey.ratingsFromParts ?? null;
|
|
74
|
+
const ratingRows = ratings
|
|
75
|
+
? RATING_AXES.map((a) => ({ ...a, value: ratings[a.key] })).filter((r) => typeof r.value === "number")
|
|
76
|
+
: [];
|
|
77
|
+
const composition = bey.composition ?? [];
|
|
78
|
+
const aliases = (bey.aka ?? []).filter((a) => a && a !== bey.name);
|
|
79
|
+
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: [categoryHref && (_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: categoryHref, children: "Listings for this model" }) })), _jsx(Heading, { level: 1, children: bey.name }), _jsx(RecordOwnerQuickActions, { resource: "BEY", recordId: bey.id, placement: "detail", recordLabel: bey.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [bey.type && _jsx(Badge, { variant: "info", children: bey.type }), bey.spinDirection && _jsx(Badge, { variant: "default", children: bey.spinDirection }), bey.system && _jsx(Badge, { variant: "secondary", children: bey.system })] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also known as: ", aliases.join(" ยท ")] }))] }), composition.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Parts" }), _jsx(Stack, { gap: "xs", className: "min-w-0", children: composition.map((c, i) => {
|
|
80
|
+
const href = c.partId ? hrefForPart(c.partId) : null;
|
|
81
|
+
const label = PART_SLOT_LABEL[c.slot] ?? c.slot;
|
|
82
|
+
return (_jsxs(Row, { gap: "md", align: "center", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", className: "w-36 flex-shrink-0", children: label }), href ? (_jsx(TextLink, { href: href, children: c.partName })) : (_jsx(Span, { children: c.partName }))] }, `${c.slot}-${c.partName}-${i}`));
|
|
83
|
+
}) })] })), 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) => ({ name: r.label, value: String(r.value) })) }), _jsxs(Text, { size: "xs", color: "faint", children: ["Averaged from this bey\u2019s parts and rescaled to 0\u2013100 for comparison across generations", bey.ratingsFromPartsCoverage
|
|
84
|
+
? `; from ${bey.ratingsFromPartsCoverage.rated ?? 0} of ` +
|
|
85
|
+
`${bey.ratingsFromPartsCoverage.total} parts`
|
|
86
|
+
: "", "."] })] })), bey.modes?.length ? (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Modes" }), _jsx(Stack, { gap: "md", className: "min-w-0", children: bey.modes.map((mode) => (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", wrap: true, className: "min-w-0", children: [_jsx(Text, { weight: "semibold", children: mode.name }), _jsx(Text, { size: "xs", color: "muted", children: mode.switchKind === "speed" || mode.switchKind === "impact"
|
|
87
|
+
? "changes on its own"
|
|
88
|
+
: "set before launch" })] }), _jsx(Text, { size: "sm", children: mode.behaviour }), _jsx(Text, { size: "sm", color: "muted", children: mode.trigger })] }, mode.name))) })] })) : null, (bey.ownerNames?.length || bey.specialMoves?.length || bey.appearAnime) && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "In the anime" }), _jsx(SpecTable, { rows: [
|
|
89
|
+
...(bey.ownerNames?.length
|
|
90
|
+
? [{ name: "Used by", value: bey.ownerNames.join(" ยท ") }]
|
|
91
|
+
: []),
|
|
92
|
+
...(bey.appearAnime ? [{ name: "First appearance", value: bey.appearAnime }] : []),
|
|
93
|
+
...(bey.appearManga ? [{ name: "In the manga", value: bey.appearManga }] : []),
|
|
94
|
+
...(bey.specialMoves?.length
|
|
95
|
+
? [{ name: "Special moves", value: bey.specialMoves.join(" ยท ") }]
|
|
96
|
+
: []),
|
|
97
|
+
] }), hrefForCharacter && (bey.ownerCharacters?.length ?? 0) > 0 && (_jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: bey.ownerCharacters.map((slug) => (_jsx(TextLink, { href: hrefForCharacter(slug), children: slug.replace(/-/g, " ") }, slug))) }))] })), specRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), products.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Found in" }), _jsx(Stack, { gap: "xs", className: "min-w-0", children: products.map((p) => (_jsxs(Row, { gap: "md", align: "center", className: "min-w-0", children: [_jsx(Span, { children: p.name }), p.productType && (_jsx(Text, { size: "xs", color: "muted", children: p.productType }))] }, p.id))) })] })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: listings.length > 0 ? "Currently listed" : "Listings" }), listings.length === 0 ? (_jsx(Text, { color: "muted", size: "sm", children: "Nothing matching this model is listed right now." })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: listings.map((l) => (_jsxs(Row, { gap: "md", align: "center", className: "min-w-0", children: [_jsx(TextLink, { href: l.href, children: l.title }), l.condition && (_jsx(Text, { size: "xs", color: "muted", children: l.condition }))] }, l.id))) })), _jsx(Text, { size: "sm", children: _jsx(TextLink, { href: allListingsHref, children: listingsTruncated ? "See all listings" : "Search listings for this model" }) })] }), _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: bey.sourceUrl, tone: "muted", underline: "always", children: "Source" }), bey.wboThreadUrl ? (_jsxs(_Fragment, { children: [" ยท ", _jsx(Anchor, { href: bey.wboThreadUrl, tone: "muted", underline: "always", children: "Community discussion" })] })) : null, " ยท ", _jsx(TextLink, { href: String(ROUTES.PUBLIC.CREDITS), children: "Credits" })] }) })] }) }) }));
|
|
98
|
+
}
|