@mohasinac/appkit 4.53.2 → 4.54.0-next.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.
Files changed (179) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  4. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  5. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  6. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  7. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  8. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  9. package/dist/_internal/shared/actions/action-registry.js +128 -0
  10. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  11. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  12. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  13. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  14. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  15. package/dist/cli/index.js +0 -2
  16. package/dist/client.d.ts +21 -2
  17. package/dist/client.js +23 -4
  18. package/dist/constants/api-endpoints.d.ts +21 -0
  19. package/dist/constants/api-endpoints.js +7 -0
  20. package/dist/features/account/components/UserOrdersView.js +26 -7
  21. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  22. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  23. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  24. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  25. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  26. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  27. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  28. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  29. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  30. package/dist/features/auth/permissions/constants.d.ts +29 -2
  31. package/dist/features/auth/permissions/constants.js +68 -1
  32. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  33. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  34. package/dist/features/cart/components/CartDrawer.js +3 -2
  35. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  36. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  37. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  38. package/dist/features/cart/utils/pending-ops.js +14 -2
  39. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  40. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  41. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  42. package/dist/features/categories/components/CategoryGrid.js +9 -2
  43. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  44. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  45. package/dist/features/events/components/EventCard.d.ts +11 -1
  46. package/dist/features/events/components/EventCard.js +9 -2
  47. package/dist/features/layout/AppLayoutShell.js +2 -1
  48. package/dist/features/layout/BottomActions.d.ts +1 -0
  49. package/dist/features/layout/BottomActions.js +330 -0
  50. package/dist/features/layout/BottomChrome.d.ts +45 -0
  51. package/dist/features/layout/BottomChrome.js +5 -17
  52. package/dist/features/layout/SideActionDock.js +36 -13
  53. package/dist/features/layout/index.d.ts +2 -7
  54. package/dist/features/layout/index.js +7 -7
  55. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  56. package/dist/features/layout/useIsDesktop.js +48 -0
  57. package/dist/features/media/messages/en.json +67 -67
  58. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  59. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  60. package/dist/features/orders/components/index.d.ts +2 -1
  61. package/dist/features/orders/components/index.js +1 -1
  62. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  63. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  64. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  65. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  66. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  67. package/dist/features/parts/schemas/firestore.js +6 -6
  68. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  69. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  70. package/dist/features/products/api/[id]/route.js +8 -1
  71. package/dist/features/products/api/route.js +16 -4
  72. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  73. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  74. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  75. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  76. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  77. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  78. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  79. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  80. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  81. package/dist/features/products/components/ProductGrid.js +22 -5
  82. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  83. package/dist/features/reviews/api/[id]/route.js +83 -23
  84. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  85. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  86. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  87. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  88. package/dist/features/reviews/components/index.d.ts +4 -0
  89. package/dist/features/reviews/components/index.js +2 -0
  90. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  91. package/dist/features/seller/actions/seller-actions.js +49 -3
  92. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  93. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  94. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  95. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  96. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  97. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  98. package/dist/features/stores/components/index.d.ts +0 -2
  99. package/dist/features/stores/components/index.js +0 -1
  100. package/dist/features/stores/schemas/index.d.ts +1 -1
  101. package/dist/features/stores/schemas/index.js +10 -1
  102. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +403 -0
  103. package/dist/index.d.ts +8 -27
  104. package/dist/index.js +14 -35
  105. package/dist/next/routing/route-map.d.ts +2 -0
  106. package/dist/next/routing/route-map.js +9 -0
  107. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  108. package/dist/providers/shipping-shiprocket/index.js +353 -0
  109. package/dist/schemas/registry.d.ts +2 -2
  110. package/dist/security/csp.d.ts +16 -1
  111. package/dist/security/csp.js +5 -3
  112. package/dist/seed/_helpers/category-forest.js +27 -0
  113. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  114. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  115. package/dist/seed/_helpers/category-parts.js +13412 -3020
  116. package/dist/seed/categories-seed-data.js +48 -6
  117. package/dist/seed/homepage-sections-seed-data.js +41 -28
  118. package/dist/seed/model-composition-seed-data.js +6 -8
  119. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  120. package/dist/seed/part-glossary/chunk0.js +35044 -0
  121. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  122. package/dist/seed/part-glossary/chunk1.js +39955 -0
  123. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  124. package/dist/seed/part-glossary/chunk2.js +50335 -0
  125. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  126. package/dist/seed/part-glossary/chunk3.js +52386 -0
  127. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  128. package/dist/seed/part-glossary/chunk4.js +50549 -0
  129. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  130. package/dist/seed/part-glossary-seed-data.js +23 -47483
  131. package/dist/server.d.ts +4 -8
  132. package/dist/server.js +8 -20
  133. package/dist/styles.css +516 -358
  134. package/dist/tailwind-utilities.css +1 -1
  135. package/dist/tokens/themes/index.d.ts +3 -1
  136. package/dist/tokens/themes/index.js +11 -1
  137. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  138. package/dist/tokens/themes/technical-dark.js +103 -0
  139. package/dist/tokens/themes/technical-light.d.ts +22 -0
  140. package/dist/tokens/themes/technical-light.js +89 -0
  141. package/dist/tokens/tokens.css +155 -0
  142. package/dist/ui/components/GatedPrice.js +30 -3
  143. package/dist/ui/components/GatedPrice.style.css +22 -19
  144. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  145. package/dist/ui/forms/FormShell.style.css +260 -260
  146. package/package.json +1 -1
  147. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  148. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  149. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  150. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  151. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  152. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  153. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  154. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  155. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  156. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  157. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  158. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  159. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  160. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  161. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  162. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  163. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  164. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  165. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  166. package/dist/features/whatsapp-bot/components/index.js +0 -2
  167. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  168. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  169. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  170. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  171. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  172. package/dist/features/whatsapp-bot/index.js +0 -3
  173. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  174. package/dist/features/whatsapp-bot/manifest.js +0 -13
  175. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  176. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  177. package/dist/features/whatsapp-bot/server.js +0 -7
  178. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  179. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ /**
4
+ * The line at which the layout stops being a phone.
5
+ *
6
+ * 64rem is Tailwind's `lg` — deliberately the same breakpoint `BottomNavbar`
7
+ * disappears at, so every decision that depends on "is there a tab bar at the
8
+ * bottom of this screen" flips at one place.
9
+ */
10
+ export const DESKTOP_QUERY = "(min-width: 64rem)";
11
+ /**
12
+ * `true` at `lg` and up.
13
+ *
14
+ * ── 🛑 WHY A HOOK AND NOT A `lg:` CLASS ────────────────────────────────────
15
+ *
16
+ * Three callers need the breakpoint as a VALUE, not as styling:
17
+ *
18
+ * - `BottomChrome` must know which CSS variable to publish — a media query
19
+ * can move the box but cannot tell a `ResizeObserver` which end of the
20
+ * screen it landed at.
21
+ * - `SideActionDock` and `BottomActions` are the two halves of one control
22
+ * and must never both be mounted: two toolbars reading the same context,
23
+ * both `fixed`, both dispatching the same callbacks. Hiding one with
24
+ * `display:none` would still mount it, still run its observers, and still
25
+ * let a stray `:focus` land inside an invisible bar.
26
+ *
27
+ * And the class form is actively unsafe here. Both Tailwind configs set
28
+ * `important: true`, so a `lg:` override and its unprefixed base tie on
29
+ * specificity AND on `!important` — a media query adds no specificity — and
30
+ * the winner is decided by EMISSION ORDER in the built CSS, which is invisible
31
+ * from the call site. That is exactly how `BottomChrome` ended up anchored to
32
+ * both edges at once and published an 801px phantom bar (see its header).
33
+ *
34
+ * SSR renders the mobile branch, because that is the branch that is correct
35
+ * when nothing is known: a phone that briefly gets the dock has lost its only
36
+ * CTA, while a desktop that briefly gets the bar has a bar.
37
+ */
38
+ export function useIsDesktop() {
39
+ const [isDesktop, setIsDesktop] = useState(false);
40
+ useEffect(() => {
41
+ const mq = window.matchMedia(DESKTOP_QUERY);
42
+ const sync = () => setIsDesktop(mq.matches);
43
+ sync();
44
+ mq.addEventListener("change", sync);
45
+ return () => mq.removeEventListener("change", sync);
46
+ }, []);
47
+ return isDesktop;
48
+ }
@@ -1,67 +1,67 @@
1
- {
2
- "mediaEditor": {
3
- "cropTitle": "Crop Image",
4
- "cropInstruction": "Drag to reposition. Use the zoom slider or +/- keys to zoom.",
5
- "cropPreviewAlt": "Preview",
6
- "cropZoom": "Zoom",
7
- "cropZoomIn": "Zoom in",
8
- "cropZoomOut": "Zoom out",
9
- "cropRotateLeft": "Rotate left 90°",
10
- "cropRotateRight": "Rotate right 90°",
11
- "cropFlipH": "Flip horizontally",
12
- "cropFlipV": "Flip vertically",
13
- "cropWarningTitle": "Low zoom",
14
- "cropWarningMessage": "Zoom in for better quality.",
15
- "cropPosition": "Position",
16
- "cropReset": "Reset",
17
- "cropSave": "Save Crop",
18
- "cropCancel": "Cancel",
19
- "trimTitle": "Trim Video",
20
- "trimInstruction": "Set the start and end times, then click Trim.",
21
- "trimStart": "Start",
22
- "trimEnd": "End",
23
- "trimDuration": "Duration: {duration}s",
24
- "trimQuality": "Quality",
25
- "trimQualityLow": "Low",
26
- "trimQualityMedium": "Medium",
27
- "trimQualityHigh": "High",
28
- "trimSave": "Trim Video",
29
- "trimSkip": "Skip",
30
- "trimming": "Trimming…",
31
- "thumbnailTitle": "Select Thumbnail",
32
- "thumbnailInstruction": "Seek to the frame you want to use as the thumbnail.",
33
- "thumbnailCapture": "Use This Frame",
34
- "thumbnailSkip": "Skip",
35
- "thumbnailCapturing": "Capturing…",
36
- "thumbnailUploading": "Uploading…",
37
- "cropSuccess": "Image cropped",
38
- "trimSuccess": "Video trimmed",
39
- "thumbnailSuccess": "Thumbnail saved",
40
- "cropError": "Failed to crop image",
41
- "trimError": "Failed to trim video",
42
- "thumbnailError": "Failed to capture thumbnail",
43
- "editImage": "Edit / Crop",
44
- "editVideo": "Trim / Thumbnail"
45
- },
46
- "camera": {
47
- "takePhoto": "Take Photo",
48
- "startRecording": "Start Recording",
49
- "stopRecording": "Stop Recording",
50
- "flipCamera": "Flip Camera",
51
- "switchToCamera": "Use Camera",
52
- "switchToUpload": "Upload File",
53
- "cameraError": "Camera unavailable",
54
- "permissionDenied": "Camera permission denied. Please allow access in your browser settings.",
55
- "notSupported": "Camera is not supported on this device",
56
- "starting": "Starting camera..."
57
- },
58
- "upload": {
59
- "change": "Change",
60
- "remove": "Remove",
61
- "clickToUpload": "Click to upload",
62
- "uploading": "Uploading…",
63
- "uploadingProgress": "Uploading {progress}%…",
64
- "replaceFile": "Replace file",
65
- "chooseFile": "Choose file to upload"
66
- }
67
- }
1
+ {
2
+ "mediaEditor": {
3
+ "cropTitle": "Crop Image",
4
+ "cropInstruction": "Drag to reposition. Use the zoom slider or +/- keys to zoom.",
5
+ "cropPreviewAlt": "Preview",
6
+ "cropZoom": "Zoom",
7
+ "cropZoomIn": "Zoom in",
8
+ "cropZoomOut": "Zoom out",
9
+ "cropRotateLeft": "Rotate left 90°",
10
+ "cropRotateRight": "Rotate right 90°",
11
+ "cropFlipH": "Flip horizontally",
12
+ "cropFlipV": "Flip vertically",
13
+ "cropWarningTitle": "Low zoom",
14
+ "cropWarningMessage": "Zoom in for better quality.",
15
+ "cropPosition": "Position",
16
+ "cropReset": "Reset",
17
+ "cropSave": "Save Crop",
18
+ "cropCancel": "Cancel",
19
+ "trimTitle": "Trim Video",
20
+ "trimInstruction": "Set the start and end times, then click Trim.",
21
+ "trimStart": "Start",
22
+ "trimEnd": "End",
23
+ "trimDuration": "Duration: {duration}s",
24
+ "trimQuality": "Quality",
25
+ "trimQualityLow": "Low",
26
+ "trimQualityMedium": "Medium",
27
+ "trimQualityHigh": "High",
28
+ "trimSave": "Trim Video",
29
+ "trimSkip": "Skip",
30
+ "trimming": "Trimming…",
31
+ "thumbnailTitle": "Select Thumbnail",
32
+ "thumbnailInstruction": "Seek to the frame you want to use as the thumbnail.",
33
+ "thumbnailCapture": "Use This Frame",
34
+ "thumbnailSkip": "Skip",
35
+ "thumbnailCapturing": "Capturing…",
36
+ "thumbnailUploading": "Uploading…",
37
+ "cropSuccess": "Image cropped",
38
+ "trimSuccess": "Video trimmed",
39
+ "thumbnailSuccess": "Thumbnail saved",
40
+ "cropError": "Failed to crop image",
41
+ "trimError": "Failed to trim video",
42
+ "thumbnailError": "Failed to capture thumbnail",
43
+ "editImage": "Edit / Crop",
44
+ "editVideo": "Trim / Thumbnail"
45
+ },
46
+ "camera": {
47
+ "takePhoto": "Take Photo",
48
+ "startRecording": "Start Recording",
49
+ "stopRecording": "Stop Recording",
50
+ "flipCamera": "Flip Camera",
51
+ "switchToCamera": "Use Camera",
52
+ "switchToUpload": "Upload File",
53
+ "cameraError": "Camera unavailable",
54
+ "permissionDenied": "Camera permission denied. Please allow access in your browser settings.",
55
+ "notSupported": "Camera is not supported on this device",
56
+ "starting": "Starting camera..."
57
+ },
58
+ "upload": {
59
+ "change": "Change",
60
+ "remove": "Remove",
61
+ "clickToUpload": "Click to upload",
62
+ "uploading": "Uploading…",
63
+ "uploadingProgress": "Uploading {progress}%…",
64
+ "replaceFile": "Replace file",
65
+ "chooseFile": "Choose file to upload"
66
+ }
67
+ }
@@ -0,0 +1,24 @@
1
+ export interface PurchaseContext {
2
+ order: {
3
+ id: string;
4
+ shortRef: string;
5
+ status: string;
6
+ orderDate: string | null;
7
+ trackHref: string;
8
+ detailHref: string;
9
+ } | null;
10
+ review: {
11
+ id: string;
12
+ rating: number;
13
+ status: string;
14
+ } | null;
15
+ windowDays: number;
16
+ }
17
+ export interface YourPurchaseStripProps {
18
+ productId: string;
19
+ /** Opens the review editor. Omit to render the review half as a link only. */
20
+ onEditReview?: (reviewId: string) => void;
21
+ onWriteReview?: () => void;
22
+ className?: string;
23
+ }
24
+ export declare function YourPurchaseStrip({ productId, onEditReview, onWriteReview, className, }: YourPurchaseStripProps): import("react").JSX.Element | null;
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * "You ordered this — track it" and "your review — edit it", on a product page.
5
+ *
6
+ * ── Why this exists ───────────────────────────────────────────────────────
7
+ *
8
+ * A buyer looking at an item they already own had no link to their own order
9
+ * and no link to their own review. Both records exist; nothing connected them
10
+ * to the product page, so the route was: remember you bought it, go to
11
+ * /user/orders, page through, find it.
12
+ *
13
+ * ── Three rules this component is built around ────────────────────────────
14
+ *
15
+ * 🛑 NO MONEY. An order total here would need the guest-price gate, and the
16
+ * strip is signed-in-only by construction — omitting the amount removes the
17
+ * question entirely rather than answering it carefully.
18
+ *
19
+ * 🛑 SAY THE WINDOW. The lookup is bounded to 90 days (see the route: order
20
+ * `items[]` holds objects, so there is no index for "order containing this
21
+ * product"). When nothing is found the copy is "No order in the last 90 days",
22
+ * never "you haven't bought this". A filtered empty result reported as an
23
+ * absolute zero is a lie the buyer cannot check — the same defect
24
+ * `UserOrdersView` carries a long comment about.
25
+ *
26
+ * 🛑 RENDER NOTHING UNTIL RESOLVED. Public product pages are ISR and read no
27
+ * session, so `loading` starts true on every hard load even for signed-in
28
+ * users. Showing the signed-out state first would flash "no order" at someone
29
+ * who has one.
30
+ *
31
+ * @tag domain:orders
32
+ * @tag layer:components
33
+ * @tag access:client
34
+ */
35
+ import { useApiQuery } from "../../../client/api/useApiQuery.js";
36
+ import { apiClient } from "../../../http/index.js";
37
+ import { useOptionalSession } from "../../../react/contexts/SessionContext.js";
38
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
39
+ import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints.js";
40
+ import { ROUTES } from "../../../next/routing/route-map.js";
41
+ import { Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
42
+ import { Icon } from "../../../ui/components/Icon.js";
43
+ import { formatDate } from "../../../utils/index.js";
44
+ export function YourPurchaseStrip({ productId, onEditReview, onWriteReview, className = "", }) {
45
+ const session = useOptionalSession();
46
+ const signedIn = Boolean(session?.user?.uid);
47
+ const query = useApiQuery({
48
+ queryKey: ["user", "purchases", productId],
49
+ queryFn: async () => {
50
+ const res = await apiClient.get(ACCOUNT_ENDPOINTS.PURCHASE_CONTEXT(productId));
51
+ return (res?.data ?? { order: null, review: null, windowDays: 90 });
52
+ },
53
+ // Only ask once we know there is somebody to ask about.
54
+ enabled: signedIn,
55
+ staleTime: 60000,
56
+ silentBackgroundFailures: true,
57
+ });
58
+ /*
59
+ * A guest, or auth not yet resolved, renders nothing at all. There is no
60
+ * useful signed-out state here: "sign in to see your order" is noise on a
61
+ * page whose primary job is selling the item.
62
+ */
63
+ if (!session || session.loading || !signedIn)
64
+ return null;
65
+ if (query.isLoading || !query.data)
66
+ return null;
67
+ const { order, review, windowDays } = query.data;
68
+ // Nothing bought and nothing written — stay out of the way entirely.
69
+ if (!order && !review)
70
+ return null;
71
+ return (_jsx(Div, { surface: "muted", rounded: "xl", padding: "md", border: "subtle", className: className, children: _jsxs(Stack, { gap: "sm", children: [order ? (_jsxs(Row, { align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Icon, { name: "order", size: "sm", tone: "muted" }), _jsxs(Text, { size: "sm", children: ["You ordered this", order.orderDate ? ` on ${formatDate(order.orderDate)}` : "", " \u00B7", " ", _jsxs(Span, { className: "font-mono", children: ["#", order.shortRef] })] })] }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(TextLink, { href: order.trackHref, children: ACTIONS.USER["track-order"].label }) }), _jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(TextLink, { href: order.detailHref, children: ACTIONS.USER["view-order"].label }) })] })] })) : (
72
+ /*
73
+ * 🛑 Bounded, and it says so. Not "you haven't bought this".
74
+ */
75
+ _jsxs(Text, { size: "sm", color: "muted", children: ["No order for this item in the last ", windowDays, " days.", " ", _jsx(TextLink, { href: String(ROUTES.USER.ORDERS), children: "See all your orders" })] })), review ? (_jsxs(Row, { align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Icon, { name: "review", size: "sm", tone: "muted", filled: true }), _jsxs(Text, { size: "sm", children: ["Your review: ", review.rating, "/5", review.status !== "approved" ? ` · ${review.status}` : ""] })] }), onEditReview && (_jsx(Button, { variant: "outline", size: "sm", onClick: () => onEditReview(review.id), children: ACTIONS.REVIEW.edit.label }))] })) : (order &&
76
+ onWriteReview && (_jsxs(Row, { align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(Text, { size: "sm", color: "muted", children: "You have not reviewed this yet." }), _jsx(Button, { variant: "outline", size: "sm", onClick: onWriteReview, children: ACTIONS.REVIEW.write.label })] })))] }) }));
77
+ }
@@ -1,6 +1,5 @@
1
1
  export { OrderCard, OrdersList } from "./OrdersList";
2
2
  export { OrderAddonBadges, type OrderAddonBadgesOrder, type OrderAddonBadgesProps, } from "./OrderAddonBadges";
3
- export { MarketplaceOrderCard, type MarketplaceOrderCardLabels, type MarketplaceOrderCardLinks, type MarketplaceOrderCardOrder, type MarketplaceOrderCardProps, } from "./MarketplaceOrderCard";
4
3
  export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
5
4
  export type { OrderFiltersProps, OrderFilterVariant } from "./OrderFilters";
6
5
  export { OrderSiblingPayments } from "./OrderSiblingPayments";
@@ -13,3 +12,5 @@ export { RefundHistoryTable } from "./RefundHistoryTable";
13
12
  export type { RefundHistoryTableProps } from "./RefundHistoryTable";
14
13
  export { RefundRequestView } from "./RefundRequestView";
15
14
  export type { RefundRequestViewProps } from "./RefundRequestView";
15
+ export { YourPurchaseStrip } from "./YourPurchaseStrip";
16
+ export type { YourPurchaseStripProps, PurchaseContext } from "./YourPurchaseStrip";
@@ -1,9 +1,9 @@
1
1
  export { OrderCard, OrdersList } from "./OrdersList.js";
2
2
  export { OrderAddonBadges, } from "./OrderAddonBadges.js";
3
- export { MarketplaceOrderCard, } from "./MarketplaceOrderCard.js";
4
3
  export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters.js";
5
4
  export { OrderSiblingPayments } from "./OrderSiblingPayments.js";
6
5
  export { OrderPaymentSummary } from "./OrderPaymentSummary.js";
7
6
  export { OrderStatusTimeline } from "./OrderStatusTimeline.js";
8
7
  export { RefundHistoryTable } from "./RefundHistoryTable.js";
9
8
  export { RefundRequestView } from "./RefundRequestView.js";
9
+ export { YourPurchaseStrip } from "./YourPurchaseStrip.js";
@@ -0,0 +1,18 @@
1
+ export interface PurchaseAnnouncementInput {
2
+ buyerName: string;
3
+ firstItemName: string;
4
+ /** 0 if only one item in the order */
5
+ additionalItemCount: number;
6
+ /** Total order amount in decimal rupees */
7
+ totalAmount: number;
8
+ orderId: string;
9
+ }
10
+ /**
11
+ * Plain-text body for the "New order placed" staff notification row.
12
+ *
13
+ * Originally built for the WhatsApp purchase announcement. That integration
14
+ * was removed 2026-10-20, but the builder survives because its output is now
15
+ * the `adminNotifications` body the admin inbox renders and the daily digest
16
+ * counts — a durable record rather than a per-admin blast.
17
+ */
18
+ export declare function buildPurchaseAnnouncementMessage(input: PurchaseAnnouncementInput): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Plain-text body for the "New order placed" staff notification row.
3
+ *
4
+ * Originally built for the WhatsApp purchase announcement. That integration
5
+ * was removed 2026-10-20, but the builder survives because its output is now
6
+ * the `adminNotifications` body the admin inbox renders and the daily digest
7
+ * counts — a durable record rather than a per-admin blast.
8
+ */
9
+ export function buildPurchaseAnnouncementMessage(input) {
10
+ const { buyerName, firstItemName, additionalItemCount, totalAmount, orderId } = input;
11
+ const extra = additionalItemCount > 0
12
+ ? ` + ${additionalItemCount} more item${additionalItemCount > 1 ? "s" : ""}`
13
+ : "";
14
+ const amount = totalAmount.toLocaleString("en-IN");
15
+ return `🛍️ New order! ${buyerName} purchased ${firstItemName}${extra} for ₹${amount}. Order #${orderId}`;
16
+ }
@@ -0,0 +1,20 @@
1
+ export interface PaymentProofReviewMessageInput {
2
+ orderId: string;
3
+ buyerName: string;
4
+ productTitle: string;
5
+ totalAmount: number;
6
+ reviewUrl: string;
7
+ }
8
+ /**
9
+ * Tier PP — message body for the buyer/seller "share for review" `wa.me`
10
+ * link on the order payment page.
11
+ *
12
+ * Pure string builder with no crypto and no network, so it is safe to export
13
+ * from the client-safe barrel. It used to live beside the WhatsApp Cloud API
14
+ * senders in `features/whatsapp-bot/`, isolated into its own file precisely
15
+ * because those siblings carried a top-level `import { createHmac } from
16
+ * "crypto"`. The Cloud API integration was removed 2026-10-20; this builder
17
+ * survived because it feeds an ordinary `wa.me` deep link, which needs no
18
+ * token, no registered number and no Meta billing relationship.
19
+ */
20
+ export declare function buildPaymentProofReviewMessage(input: PaymentProofReviewMessageInput): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tier PP — message body for the buyer/seller "share for review" `wa.me`
3
+ * link on the order payment page.
4
+ *
5
+ * Pure string builder with no crypto and no network, so it is safe to export
6
+ * from the client-safe barrel. It used to live beside the WhatsApp Cloud API
7
+ * senders in `features/whatsapp-bot/`, isolated into its own file precisely
8
+ * because those siblings carried a top-level `import { createHmac } from
9
+ * "crypto"`. The Cloud API integration was removed 2026-10-20; this builder
10
+ * survived because it feeds an ordinary `wa.me` deep link, which needs no
11
+ * token, no registered number and no Meta billing relationship.
12
+ */
13
+ export function buildPaymentProofReviewMessage(input) {
14
+ const { orderId, buyerName, productTitle, totalAmount, reviewUrl } = input;
15
+ const amount = totalAmount.toLocaleString("en-IN");
16
+ return `💳 Payment proof submitted! ${buyerName} uploaded proof for "${productTitle}" (₹${amount}). Order #${orderId}. Please review within 2 hours or it auto-confirms: ${reviewUrl}`;
17
+ }
@@ -210,6 +210,67 @@ export interface PartColourway {
210
210
  /** The bey this colourway was released in. */
211
211
  inBey: string;
212
212
  }
213
+ /**
214
+ * The infobox stat grid, as NUMBERS.
215
+ *
216
+ * 🛑 This is a deliberate second representation of the `Stats` row in
217
+ * `specs[]`, not a mirror to keep in sync. The row is a RENDERING — one line,
218
+ * because six rows of single digits crowd out every real specification on a
219
+ * part page — and this is the VALUE, which is what a sort, a filter, a
220
+ * side-by-side comparison and the simulation all need. Neither can do the
221
+ * other's job, and both are produced by one parse in one pass, so there is no
222
+ * second write path to forget (unlike Root Cause #42's shape).
223
+ *
224
+ * Every key is OPTIONAL because the source states them per part: a page that
225
+ * gives Attack and Burst and nothing else yields exactly those two. A missing
226
+ * key is "not stated", never zero — `weight: 0` is a real rating on a
227
+ * featherweight disc and must stay distinguishable from silence.
228
+ *
229
+ * The scale is 0–6 and is the wiki's own editorial judgement, not a
230
+ * measurement. It is useful for comparison between parts of the same slot and
231
+ * should not be presented as a physical quantity.
232
+ */
233
+ export interface PartRatings {
234
+ attack?: number;
235
+ defense?: number;
236
+ stamina?: number;
237
+ weight?: number;
238
+ speed?: number;
239
+ burst?: number;
240
+ dash?: number;
241
+ burstResistance?: number;
242
+ power?: number;
243
+ height?: number;
244
+ runTime?: number;
245
+ }
246
+ /**
247
+ * WBO tournament-format legality.
248
+ *
249
+ * 🛑 A format the source does not rule on is ABSENT, never `false`. Three
250
+ * states exist — legal, banned, and not stated — and collapsing the third into
251
+ * the second would publish "banned in Burst Classic" as a fact about every
252
+ * part whose page simply never mentions Burst Classic.
253
+ */
254
+ export interface PartWboLegality {
255
+ burst?: boolean;
256
+ burstGt?: boolean;
257
+ burstLimited?: boolean;
258
+ burstClassic?: boolean;
259
+ xStandard?: boolean;
260
+ xMasters?: boolean;
261
+ xOpen?: boolean;
262
+ mfbStandard?: boolean;
263
+ mfbLimited?: boolean;
264
+ mfbZeroG?: boolean;
265
+ mfb4d?: boolean;
266
+ }
267
+ /**
268
+ * OUR role vocabulary — 10 values covering all 59 slots.
269
+ *
270
+ * Mirrors `PART_ROLE` in `scripts/lib/part-slot-vocab.mjs`, which is the
271
+ * source. A slot missing from that map yields no `role` rather than a guess.
272
+ */
273
+ export type PartRole = "chip" | "ring" | "wheel" | "weight" | "base" | "blade" | "track" | "tip" | "gear" | "support";
213
274
  export interface PartGlossaryDocument extends BaseDocument {
214
275
  /** `${slot}--${codeOrSlug}` — both halves, because a code is not unique. */
215
276
  id: string;
@@ -294,6 +355,172 @@ export interface PartGlossaryDocument extends BaseDocument {
294
355
  * it is a DIFFERENT axis from colour and must not be folded into it.
295
356
  */
296
357
  hasMoldVariations?: boolean;
358
+ /**
359
+ * The 0–6 stat grid as numbers. Present on 1,149 of 2,490 rows — the source
360
+ * states it per part, so absence is "not stated", not zero.
361
+ */
362
+ ratings?: PartRatings;
363
+ /**
364
+ * The ceiling `ratings` are out of — 6, 8 or 100.
365
+ *
366
+ * 🛑 LOAD-BEARING, because each generation rates on its own scale and they
367
+ * share these fields. Measured: Beyblade X rates 0-100 (237 rows) while
368
+ * Burst rates 0-6 or 0-8 (945 rows), so mean `attack` by slot reads
369
+ * `blade 31.8` beside `driver 1.78`. Anything that sorts, compares or
370
+ * simulates on a rating MUST divide by this first; without it a Beyblade X
371
+ * blade outranks every Burst layer five times over.
372
+ *
373
+ * Derived per system from the highest rating observed for it, rounded up to
374
+ * a standard step — not from a hand-written table, which was wrong within
375
+ * one run (`warrior-wheel--dragoon` carries attack 7 against a declared 6).
376
+ */
377
+ ratingScale?: number;
378
+ /**
379
+ * The same axes rescaled to 0-100, so a part can be compared ACROSS
380
+ * generations — which the game needs, since it has cross-gen battles.
381
+ *
382
+ * 🛑 This equalises the EDITORIAL JUDGEMENT, not the capability. A Burst
383
+ * layer at 6/6 and a Beyblade X blade at 100/100 both land on 100 and are
384
+ * not equally strong — that blade is 30 g against the layer's 13 g. So it
385
+ * answers "how good is this part for its generation", which is the right
386
+ * basis for a balanced roster and the wrong basis for claiming physical
387
+ * equivalence. `weightG` carries the latter, in grams, unnormalised.
388
+ *
389
+ * `ratings` stays raw beside it: a normalised value can be recomputed from
390
+ * the raw pair, a discarded raw value cannot.
391
+ *
392
+ * 🛑 FOR COMPARING OPPONENTS, NEVER FOR SUGGESTING A BUILD. A bey can only
393
+ * be ASSEMBLED within one generation — a Burst layer does not fit a Beyblade
394
+ * X ratchet — while battles are cross-generation. So this field belongs in a
395
+ * matchup, a leaderboard or a side-by-side, and a "build your own" picker
396
+ * must constrain to a single system's stack regardless of what it says.
397
+ */
398
+ ratingsNormalised?: PartRatings;
399
+ /** WBO format legality across 11 formats; an unstated format is absent. */
400
+ wboLegality?: PartWboLegality;
401
+ /**
402
+ * Other names for this exact part — the community abbreviation (`Armor Tip
403
+ * - Adventure` is `Ad`) and the other vendor's name. On 1,072 of 2,490 rows.
404
+ * Feeds search, so a buyer can find a part by the code printed on it.
405
+ */
406
+ aka?: string[];
407
+ /**
408
+ * Hasbro's own product code, a SEPARATE field from `productCodes`. On 309
409
+ * rows, and it is the Hasbro <-> Takara-Tomy equivalence stated outright.
410
+ */
411
+ hasbroCode?: string;
412
+ /** Attack / Defense / Stamina / Balance, per part. 793 rows. */
413
+ partType?: string;
414
+ /** Right-Spin / Left-Spin / Dual. 756 rows. */
415
+ spinDirection?: string;
416
+ /** Beyblade X's attribute label — "Sky" and siblings. 29 rows. */
417
+ element?: string;
418
+ /**
419
+ * The character this part belongs to in the story — `Bit Chip - Black
420
+ * Dranzer` is Kai Hiwatari's. Only 27 rows, but it is the sole place a PART
421
+ * is tied to a character.
422
+ */
423
+ owner?: string;
424
+ /** The episode this part appeared in. 22 rows. */
425
+ appearAnime?: string;
426
+ /**
427
+ * OUR OWN classification — what the part DOES, across generations.
428
+ *
429
+ * 🛑 `slot` cannot carry this. It mirrors the source's `Classification=`
430
+ * spelling and it is half of every document id (`${slot}--${code}`), and
431
+ * those ids are LIVE URLS (`/parts/{classification}/{code}`). Renaming a
432
+ * slot re-keys 2,490 rows and breaks every inbound link — a slug is the one
433
+ * field that cannot be quietly corrected later. So this is additive.
434
+ *
435
+ * It collapses the source's line-specific names onto one role:
436
+ * `bu-blade` / `qs-blade` / `blade` are all `blade`; the six Burst chip
437
+ * variants are all `chip`. Derived in `scripts/lib/part-slot-vocab.mjs`,
438
+ * which covers all 59 slots with 10 roles.
439
+ */
440
+ role?: PartRole;
441
+ /**
442
+ * Every stack role one moulding fills, listed TOP-TO-BOTTOM. A part that
443
+ * covers a single position does not carry this field at all.
444
+ *
445
+ * Integration is real and spans generations: HMS merges spin gear and blade
446
+ * base into a Running Core, a 4D Bottom (`D:D`, `X:D`) is a spin track and
447
+ * a performance tip in one piece, a Burst `disc-integrated-driver` is a disc
448
+ * and a driver, and `fusion-wheel--variares` is an energy ring and a fusion
449
+ * wheel. A Plastic-era base can integrate its Sub Attack Ring.
450
+ *
451
+ * 🛑 Load-bearing for any stack validation. `role` files an integrated part
452
+ * under its PRIMARY function, so a validator reading only that believes the
453
+ * other position is still empty and offers a part that cannot physically go
454
+ * there — it will refuse a legal build, and an exploded 3D view will show a
455
+ * gap where a solid piece is.
456
+ *
457
+ * 🛑 PER PART, NOT PER SLOT — that is the whole reason this is a field and
458
+ * not a lookup. `fusion-wheel--variares` integrates the ring and
459
+ * `fusion-wheel--jade` does not; same slot, different moulding. Jade is
460
+ * multi-PIECE, which is a different fact (see `pieceCount` when it lands).
461
+ *
462
+ * 🛑 Do NOT re-spell this as "fused". "Fusion Wheel" is a literal MFB slot
463
+ * name, so `fusedRoles` reads as *the roles of the fusion wheel* — the
464
+ * opposite of the meaning. The source's own slot names already use the
465
+ * right word: `disc-integrated-driver`, `ratchet-integrated-bit`.
466
+ *
467
+ * Populated by `scripts/derive-part-integration.mjs`, which reads it off the
468
+ * compositions each part actually appears in — the source states a 4D
469
+ * Bottom's `Classification` as plain "Performance Tip", so this cannot be
470
+ * copied from an infobox and is not guessed either.
471
+ */
472
+ integratedRoles?: PartRole[];
473
+ /**
474
+ * An owner-uploaded 3D model. Empty by default and deliberately so — the
475
+ * models are supplied by the owner, not ingested from anywhere.
476
+ *
477
+ * 🛑 NEVER served through `/api/media/[...slug]`. That route applies NO
478
+ * authentication, raw-pipes non-images with a 30-day immutable cache, and
479
+ * sits over a bucket whose storage rule is `allow read: if true` — so a GLB
480
+ * there is a public download. A mesh is the asset someone could build a
481
+ * competing game from, so it needs a signed, short-lived URL and an
482
+ * authenticated route of its own.
483
+ *
484
+ * 🛑 And a browser viewer cannot make a mesh unextractable — WebGL buffers
485
+ * can be dumped from any page that renders them. The honest mitigation is to
486
+ * publish a DECIMATED preview and keep the full-resolution mesh server-side;
487
+ * "protected" here means inconvenient, never impossible.
488
+ */
489
+ model3d?: {
490
+ url: string;
491
+ format: "glb";
492
+ sizeBytes: number;
493
+ /** A low-poly public preview, when the full mesh is held back. */
494
+ previewUrl?: string;
495
+ credit?: string;
496
+ creditUrl?: string;
497
+ };
498
+ /**
499
+ * An owner-uploaded sound, with its waveform for display.
500
+ *
501
+ * Renders through the existing `<MediaAudio>` primitive, so this needs no
502
+ * new UI primitive. `waveform` is a coarse amplitude array for drawing the
503
+ * trace without decoding the audio client-side.
504
+ */
505
+ sound?: {
506
+ url: string;
507
+ durationMs?: number;
508
+ waveform?: number[];
509
+ label?: string;
510
+ };
511
+ /**
512
+ * The Japanese name, the romanisation, and the kanji where the source gives
513
+ * one.
514
+ *
515
+ * 🛑 Top-level fields AS WELL AS `specs[]` rows, which is not duplication for
516
+ * its own sake: a `<title>`, an `og:title`, an hreflang alternate and a
517
+ * JSON-LD `alternateName` all need a FIELD. None of them can scan an array
518
+ * of label/value pairs for one whose label happens to read "Japanese name",
519
+ * and that is where these 1,068 values previously lived and died.
520
+ */
521
+ jpName?: string;
522
+ romaji?: string;
523
+ kanjiName?: string;
297
524
  /** `/media/<slug>`. 🛑 Never a raw Storage host URL. */
298
525
  image?: string;
299
526
  /** The wiki page the photo came from — the CC-BY-SA attribution target. */