@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.
Files changed (172) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/beys/data.d.ts +44 -0
  3. package/dist/_internal/server/features/beys/data.js +107 -0
  4. package/dist/_internal/server/features/beys/og.d.ts +42 -0
  5. package/dist/_internal/server/features/beys/og.js +89 -0
  6. package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
  7. package/dist/_internal/server/features/checkout/actions.js +117 -5
  8. package/dist/_internal/server/features/orders/actions.d.ts +30 -0
  9. package/dist/_internal/server/features/orders/actions.js +103 -0
  10. package/dist/_internal/server/features/orders/adapters.js +7 -0
  11. package/dist/_internal/server/features/parts/data.js +3 -2
  12. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  13. package/dist/_internal/shared/actions/action-registry.js +139 -0
  14. package/dist/_internal/shared/shipping/quote.js +5 -6
  15. package/dist/client.d.ts +23 -0
  16. package/dist/client.js +3 -0
  17. package/dist/constants/api-endpoints.d.ts +12 -0
  18. package/dist/constants/api-endpoints.js +10 -0
  19. package/dist/constants/field-names.d.ts +46 -0
  20. package/dist/constants/field-names.js +46 -0
  21. package/dist/features/about/components/PublicProfileView.js +2 -1
  22. package/dist/features/account/components/UserOrdersView.js +25 -3
  23. package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
  24. package/dist/features/admin/components/AdminBeysView.js +178 -0
  25. package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
  26. package/dist/features/admin/components/AdminPartsView.js +160 -0
  27. package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
  28. package/dist/features/admin/components/OwnerQuickActions.js +83 -8
  29. package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
  30. package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
  31. package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
  32. package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
  33. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  34. package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
  35. package/dist/features/auth/permissions/constants.d.ts +1 -1
  36. package/dist/features/auth/permissions/constants.js +12 -1
  37. package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
  38. package/dist/features/beys/components/BeyDetailPageView.js +98 -0
  39. package/dist/features/beys/schemas/firestore.d.ts +417 -0
  40. package/dist/features/beys/schemas/firestore.js +65 -0
  41. package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
  42. package/dist/features/blog/components/BlogPostView.js +2 -1
  43. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  44. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  45. package/dist/features/categories/components/BrandDetailPageView.js +4 -2
  46. package/dist/features/categories/components/BundleDetailView.js +2 -2
  47. package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
  48. package/dist/features/categories/components/CategoryGrid.js +24 -2
  49. package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
  50. package/dist/features/events/components/EventCard.js +15 -2
  51. package/dist/features/faq/components/FAQPageContent.js +17 -4
  52. package/dist/features/faq/constants/help-guides.d.ts +36 -0
  53. package/dist/features/faq/constants/help-guides.js +64 -0
  54. package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
  55. package/dist/features/homepage/components/CustomCardsSection.js +67 -50
  56. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  57. package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
  58. package/dist/features/homepage/lib/section-renderer.js +2 -1
  59. package/dist/features/homepage/schemas/index.d.ts +10 -10
  60. package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
  61. package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
  62. package/dist/features/orders/components/index.d.ts +1 -0
  63. package/dist/features/orders/components/index.js +1 -0
  64. package/dist/features/orders/schemas/index.d.ts +2 -2
  65. package/dist/features/orders/types/index.d.ts +15 -0
  66. package/dist/features/parts/components/PartDetailPageView.js +5 -3
  67. package/dist/features/parts/components/PartsSearchBox.js +79 -4
  68. package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
  69. package/dist/features/parts/schemas/admin-edit.js +89 -0
  70. package/dist/features/parts/schemas/firestore.d.ts +2 -1
  71. package/dist/features/points/repository/points.repository.js +5 -5
  72. package/dist/features/points/schemas/firestore.d.ts +16 -0
  73. package/dist/features/points/schemas/firestore.js +17 -0
  74. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
  75. package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
  76. package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
  77. package/dist/features/products/components/ProductGrid.js +18 -2
  78. package/dist/features/products/schemas/firestore.d.ts +18 -0
  79. package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
  80. package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
  81. package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
  82. package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
  83. package/dist/features/seller/components/SellerProductShell.js +31 -3
  84. package/dist/features/stores/components/StoreHeader.js +3 -1
  85. package/dist/index.d.ts +8 -0
  86. package/dist/index.js +13 -0
  87. package/dist/next/routing/route-map.d.ts +17 -1
  88. package/dist/next/routing/route-map.js +39 -0
  89. package/dist/schemas/registry.d.ts +8 -8
  90. package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
  91. package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
  92. package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
  93. package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
  94. package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
  95. package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
  96. package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
  97. package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
  98. package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
  99. package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
  100. package/dist/seed/_helpers/category-models.d.ts +9 -2
  101. package/dist/seed/_helpers/category-models.js +19 -1368
  102. package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
  103. package/dist/seed/_helpers/category-parts.js +39 -39
  104. package/dist/seed/bey-products-seed-data.d.ts +18 -0
  105. package/dist/seed/bey-products-seed-data.js +18011 -0
  106. package/dist/seed/beys/chunk0.d.ts +17 -0
  107. package/dist/seed/beys/chunk0.js +36733 -0
  108. package/dist/seed/beys/chunk1.d.ts +17 -0
  109. package/dist/seed/beys/chunk1.js +35728 -0
  110. package/dist/seed/beys/chunk2.d.ts +17 -0
  111. package/dist/seed/beys/chunk2.js +35857 -0
  112. package/dist/seed/beys/chunk3.d.ts +17 -0
  113. package/dist/seed/beys/chunk3.js +34476 -0
  114. package/dist/seed/beys-seed-data.d.ts +18 -0
  115. package/dist/seed/beys-seed-data.js +11 -0
  116. package/dist/seed/homepage-sections-seed-data.js +16 -0
  117. package/dist/seed/index.d.ts +2 -0
  118. package/dist/seed/index.js +3 -0
  119. package/dist/seed/media-assets-seed-data.js +4 -4
  120. package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
  121. package/dist/seed/products-catalogue/chunk0.js +7262 -0
  122. package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
  123. package/dist/seed/products-catalogue/chunk1.js +7290 -0
  124. package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
  125. package/dist/seed/products-catalogue/chunk10.js +7290 -0
  126. package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
  127. package/dist/seed/products-catalogue/chunk11.js +7005 -0
  128. package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
  129. package/dist/seed/products-catalogue/chunk12.js +7002 -0
  130. package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
  131. package/dist/seed/products-catalogue/chunk13.js +7002 -0
  132. package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
  133. package/dist/seed/products-catalogue/chunk14.js +7002 -0
  134. package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
  135. package/dist/seed/products-catalogue/chunk15.js +7002 -0
  136. package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
  137. package/dist/seed/products-catalogue/chunk16.js +7002 -0
  138. package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
  139. package/dist/seed/products-catalogue/chunk17.js +7002 -0
  140. package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
  141. package/dist/seed/products-catalogue/chunk18.js +4398 -0
  142. package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
  143. package/dist/seed/products-catalogue/chunk2.js +7268 -0
  144. package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
  145. package/dist/seed/products-catalogue/chunk3.js +7264 -0
  146. package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
  147. package/dist/seed/products-catalogue/chunk4.js +7268 -0
  148. package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
  149. package/dist/seed/products-catalogue/chunk5.js +7258 -0
  150. package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
  151. package/dist/seed/products-catalogue/chunk6.js +7268 -0
  152. package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
  153. package/dist/seed/products-catalogue/chunk7.js +7311 -0
  154. package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
  155. package/dist/seed/products-catalogue/chunk8.js +7304 -0
  156. package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
  157. package/dist/seed/products-catalogue/chunk9.js +7309 -0
  158. package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
  159. package/dist/seed/products-catalogue-seed-data.js +58 -86382
  160. package/dist/seed/seed-purpose.js +9 -0
  161. package/dist/server.d.ts +5 -1
  162. package/dist/server.js +19 -0
  163. package/dist/styles.css +468 -1
  164. package/dist/tailwind-utilities.css +1 -1
  165. package/dist/ui/components/PosterCard.d.ts +99 -0
  166. package/dist/ui/components/PosterCard.js +109 -0
  167. package/dist/ui/components/PosterCard.style.css +466 -0
  168. package/dist/ui/components/index.style.css +1 -0
  169. package/dist/ui/index.d.ts +2 -0
  170. package/dist/ui/index.js +2 -0
  171. package/package.json +1 -1
  172. package/scripts/seed-cli.mjs +108 -1
@@ -412,3 +412,106 @@ export async function raiseOrderDisputeAction(orderId, reason) {
412
412
  });
413
413
  });
414
414
  }
415
+ /**
416
+ * A buyer submits payment for ONE of their own EMI instalments.
417
+ *
418
+ * 🛑 Why this exists at all: there was no buyer-side route. The only writer
419
+ * was the seller's `markEmiInstallmentPaid`, so a buyer on an EMI plan could
420
+ * not pay an instalment — they waited for the seller to record it. The
421
+ * schema had anticipated them all along (`EmiInstallment.transactionId` and
422
+ * `.proofUrl` are documented as "buyer-uploaded"), which is the tell that the
423
+ * surface, not the model, was missing.
424
+ *
425
+ * 🛑 **ANY unpaid instalment, at ANY time — deliberately unbounded.** There is
426
+ * no due-date check and none should be added. A buyer who wants to clear the
427
+ * plan early is reducing our exposure, not breaking a rule, and the dueDate is
428
+ * the date we will CHASE them on, never the first date they may pay. Paying
429
+ * ahead also keeps `emiComplete` honest: that flag is what
430
+ * `assertEmiShippable` reads, so early settlement should be able to release a
431
+ * shipment early rather than waiting out a calendar.
432
+ *
433
+ * It mirrors the manual-payment model rather than inventing a second one: the
434
+ * buyer supplies a UTR and a proof, the seller or an admin confirms. The
435
+ * status therefore moves to `pending` review here, NOT to `paid` — a buyer
436
+ * must never be able to mark their own money received. `markEmiInstallmentPaid`
437
+ * stays the only writer of `paid`, and so stays the only way `emiComplete`
438
+ * can flip.
439
+ */
440
+ export async function submitEmiInstallmentPaymentAction(orderId, input) {
441
+ return wrapAction(async () => {
442
+ const user = await requireRoleUser(["user", "buyer", "seller", "admin"]);
443
+ const order = await orderRepository.findById(orderId);
444
+ if (!order)
445
+ throw new OrderNotFoundError(orderId);
446
+ if (!isAdminUser(user) && order.userId !== user.uid)
447
+ throw new OrderOwnershipError(orderId);
448
+ if (!order.emiEnabled)
449
+ throw new ValidationError("This order is not on an EMI plan");
450
+ const installments = order.emiInstallments ?? [];
451
+ const target = installments.find((i) => i.index === input.installmentIndex);
452
+ if (!target) {
453
+ throw new ValidationError(`Installment ${input.installmentIndex} not found on this order`);
454
+ }
455
+ if (target.status === "paid") {
456
+ throw new ValidationError("This installment is already paid");
457
+ }
458
+ // A second submission would overwrite the reference the reviewer is
459
+ // currently looking at, which reads to them as the buyer changing their
460
+ // story. Re-submitting needs the reviewer to reject the first one.
461
+ if (target.transactionId) {
462
+ throw new ValidationError("A payment reference is already under review for this installment");
463
+ }
464
+ /*
465
+ * 🛑 `status` is NOT written here, and writing it would be a lie dressed
466
+ * as a state change.
467
+ *
468
+ * A new instalment is created `status: "pending"` (see
469
+ * `createOrderForGroup`), so setting `"pending"` again changes nothing
470
+ * while reading like a transition — and it would leave "nobody has paid"
471
+ * and "the buyer has reported paying" indistinguishable, which is exactly
472
+ * the state a reviewer needs to tell apart.
473
+ *
474
+ * "Awaiting review" is DERIVED: `transactionId` set and `status !== "paid"`.
475
+ * Same rule as the manual-payment queue, which derives its four states
476
+ * from `paymentProofUrl` + `paymentReviewOutcome` rather than storing a
477
+ * mirror field that any write path could forget to update (Root Cause
478
+ * #42). Only `markEmiInstallmentPaid` writes `paid`, so a buyer can never
479
+ * mark their own money received, and `emiComplete` keeps its single writer.
480
+ */
481
+ const updatedInstallments = installments.map((i) => i.index === input.installmentIndex
482
+ ? { ...i, transactionId: input.transactionId, proofUrl: input.proofUrl }
483
+ : i);
484
+ await orderRepository.update(orderId, { emiInstallments: updatedInstallments });
485
+ /*
486
+ * Tell the seller there is something to confirm. Best-effort: a
487
+ * notification that fails must not lose the buyer's submitted reference,
488
+ * which is the only record that they paid.
489
+ */
490
+ const store = order.storeId ? await storeRepository.findById(order.storeId) : null;
491
+ if (store?.ownerId) {
492
+ await sendNotification({
493
+ userId: store.ownerId,
494
+ type: "payment_review",
495
+ /*
496
+ * `high`, matching the other `payment_review` send in this file. The
497
+ * two differ in urgency — that one has a 15-minute re-upload window
498
+ * and this one has no deadline — but a single notification TYPE whose
499
+ * priority disagrees between its call sites is the drift this codebase
500
+ * keeps paying for, and unconfirmed money is a seller action item
501
+ * either way.
502
+ */
503
+ priority: "high",
504
+ title: "EMI instalment payment reported",
505
+ message: `A buyer has submitted payment for instalment ${input.installmentIndex} on order ${orderId}.`,
506
+ relatedId: orderId,
507
+ relatedType: "order",
508
+ }).catch((err) => {
509
+ void normalizeError(err);
510
+ serverLogger.error("Failed to notify seller of EMI instalment payment (non-fatal)", {
511
+ orderId,
512
+ installmentIndex: input.installmentIndex,
513
+ });
514
+ });
515
+ }
516
+ });
517
+ }
@@ -170,6 +170,13 @@ export function orderDocumentToOrder(doc) {
170
170
  // the packer needs giftWrapMessage in hand. An
171
171
  // adapter that drops them makes every downstream surface render its
172
172
  // "not applicable" fallback instead (Root Cause #57).
173
+ // EMI, so the buyer can see and pay their own schedule. Mapped with the
174
+ // add-ons above for the same reason recorded there: an adapter that drops
175
+ // a field makes every downstream surface render its not-applicable
176
+ // fallback (Root Cause #57).
177
+ emiEnabled: doc.emiEnabled,
178
+ emiInstallments: doc.emiInstallments,
179
+ emiRemainingBalance: doc.emiRemainingBalance,
173
180
  giftWrapAddon: doc.giftWrapAddon,
174
181
  giftWrapFee: doc.giftWrapFee,
175
182
  giftWrapMessage: doc.giftWrapMessage,
@@ -11,6 +11,7 @@ import { PART_GLOSSARY_COLLECTION, MODEL_COMPOSITION_COLLECTION, partGlossaryId,
11
11
  import { getAdminDb } from "../../../../providers/db-firebase/index.js";
12
12
  import { serverLogger } from "../../../../monitoring/server-logger.js";
13
13
  import { normalizeError } from "../../../../errors/normalize.js";
14
+ import { MODEL_COMPOSITION_FIELDS, PART_GLOSSARY_FIELDS, } from "../../../../constants/field-names.js";
14
15
  /** The bucket cap. 87 is the largest real classification, so this clears it. */
15
16
  export const PART_SLOT_PAGE_SIZE = 120;
16
17
  /** One part, by `(slot, code|name)` — the only key this collection has. */
@@ -125,7 +126,7 @@ export const getCompositionsForCategory = cache(async (categoryId) => {
125
126
  try {
126
127
  const snap = await getAdminDb()
127
128
  .collection(MODEL_COMPOSITION_COLLECTION)
128
- .where("categoryId", "==", categoryId)
129
+ .where(MODEL_COMPOSITION_FIELDS.CATEGORY_ID, "==", categoryId)
129
130
  .limit(8)
130
131
  .get();
131
132
  return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
@@ -154,7 +155,7 @@ export const listPartsBySlot = cache(async (slot) => {
154
155
  try {
155
156
  const snap = await getAdminDb()
156
157
  .collection(PART_GLOSSARY_COLLECTION)
157
- .where("slot", "==", slot)
158
+ .where(PART_GLOSSARY_FIELDS.SLOT, "==", slot)
158
159
  .limit(PART_SLOT_PAGE_SIZE)
159
160
  .get();
160
161
  const parts = snap.docs
@@ -29,7 +29,7 @@ import type { IconKey } from "../../../ui/icons/icon-registry";
29
29
  /** What KIND of action — drives default UI affordance (link vs button vs danger). */
30
30
  export type ActionKind = "primary" | "secondary" | "danger" | "ghost" | "link" | "chip";
31
31
  /** Resource bucket — keyed top-level in the ACTIONS tree. */
32
- export type ActionResource = "PRODUCT" | "AUCTION" | "PRE_ORDER" | "PRIZE_DRAW" | "DIGITAL_CODE" | "BUNDLE" | "GROUP" | "CATEGORY" | "BRAND" | "SUBLISTING" | "STORE" | "BLOG" | "EVENT" | "REVIEW" | "SHIPMENT" | "CATALOGUE" | "USER" | "SELLER" | "ADMIN" | "CART" | "CHECKOUT" | "NAV" | "MEDIA" | "SUPPORT" | "LOTTERY" | "TESTER";
32
+ export type ActionResource = "PRODUCT" | "AUCTION" | "PRE_ORDER" | "PRIZE_DRAW" | "DIGITAL_CODE" | "BUNDLE" | "GROUP" | "CATEGORY" | "BRAND" | "PART" | "BEY" | "SUBLISTING" | "STORE" | "BLOG" | "EVENT" | "REVIEW" | "SHIPMENT" | "CATALOGUE" | "USER" | "SELLER" | "ADMIN" | "CART" | "CHECKOUT" | "NAV" | "MEDIA" | "SUPPORT" | "LOTTERY" | "TESTER";
33
33
  /** Confirmation modal config — when set, <Button action={...}> opens
34
34
  * a confirm dialog before firing the actual handler. */
35
35
  export interface ActionConfirmation {
@@ -432,6 +432,92 @@ export const ACTIONS = {
432
432
  },
433
433
  },
434
434
  },
435
+ /*
436
+ * ── 🛑 THE TWO GENERATED REFERENCE DATABASES ─────────────────────────────
437
+ *
438
+ * `partGlossary` (2,488) and `beys` (1,988) are built by committed
439
+ * generators from a crawled corpus. That shapes every leaf below:
440
+ *
441
+ * · NO `create`. A row authored by hand has no source, vanishes on the
442
+ * next regeneration, and makes "this dataset rebuilds from the repo"
443
+ * false. Adding a part is a change to the generator, not a button.
444
+ * · `edit` is CORRECT-A-WRONG-EXTRACTION, and says so, because that is
445
+ * what it is for — the corpus was measurably wrong five times in one
446
+ * session on mode data alone.
447
+ * · `delete`'s confirmation does NOT say "cannot be undone", because that
448
+ * would be a lie: the row is in a committed seed file and the next
449
+ * reseed restores it. It says what actually happens.
450
+ */
451
+ PART: {
452
+ view: {
453
+ iconKey: "view",
454
+ id: "part.view",
455
+ label: "View",
456
+ ariaLabel: "View part",
457
+ description: "Open the part reference page.",
458
+ kind: "secondary",
459
+ },
460
+ edit: {
461
+ iconKey: "edit",
462
+ id: "part.edit",
463
+ label: "Edit",
464
+ ariaLabel: "Edit part",
465
+ description: "Correct this part's recorded details. The correction is kept when the database is regenerated.",
466
+ kind: "secondary",
467
+ permissions: ["admin"],
468
+ },
469
+ delete: {
470
+ iconKey: "delete",
471
+ id: "part.delete",
472
+ label: "Delete",
473
+ ariaLabel: "Delete part",
474
+ description: "Remove this part from the live database.",
475
+ kind: "danger",
476
+ permissions: ["admin"],
477
+ confirmation: {
478
+ title: "Remove this part from the live database?",
479
+ body: "Beyblades built from it keep their parts list and will show its name without a link. " +
480
+ "This part is also in the committed seed data, so regenerating the database will bring it " +
481
+ "back — to remove it for good, take it out of the generator.",
482
+ confirmLabel: "Remove",
483
+ },
484
+ },
485
+ },
486
+ BEY: {
487
+ view: {
488
+ iconKey: "view",
489
+ id: "bey.view",
490
+ label: "View",
491
+ ariaLabel: "View Beyblade",
492
+ description: "Open the Beyblade reference page.",
493
+ kind: "secondary",
494
+ },
495
+ edit: {
496
+ iconKey: "edit",
497
+ id: "bey.edit",
498
+ label: "Edit",
499
+ ariaLabel: "Edit Beyblade",
500
+ description: "Correct this Beyblade's recorded details. The correction is kept when the database is regenerated.",
501
+ kind: "secondary",
502
+ permissions: ["admin"],
503
+ },
504
+ delete: {
505
+ iconKey: "delete",
506
+ id: "bey.delete",
507
+ label: "Delete",
508
+ ariaLabel: "Delete Beyblade",
509
+ description: "Remove this Beyblade from the live database.",
510
+ kind: "danger",
511
+ permissions: ["admin"],
512
+ confirmation: {
513
+ title: "Remove this Beyblade from the live database?",
514
+ body: "Its category page and any listings under it are not touched, and boxed sets that include " +
515
+ "it keep their contents list. It is also in the committed seed data, so regenerating the " +
516
+ "database will bring it back — to remove it for good, take it out of the generator.",
517
+ confirmLabel: "Remove",
518
+ },
519
+ },
520
+ },
435
521
  CATEGORY: {
436
522
  view: {
437
523
  iconKey: "view",
@@ -1039,6 +1125,21 @@ export const ACTIONS = {
1039
1125
  },
1040
1126
  },
1041
1127
  BLOG: {
1128
+ /*
1129
+ * Clone, admin-only, backed by POST /api/admin/blog/[id]/duplicate. A
1130
+ * templated post is the real use case: same structure and category, new
1131
+ * subject. The copy starts as a DRAFT with a freshly derived slug — blog is
1132
+ * a pure-slug resource, so reusing the source slug would overwrite it.
1133
+ */
1134
+ "duplicate-post": {
1135
+ iconKey: "duplicate",
1136
+ id: "blog.duplicate-post",
1137
+ label: "Duplicate",
1138
+ ariaLabel: "Duplicate this post as a draft",
1139
+ description: "Copy this post into a new draft, without its views or featured flag.",
1140
+ kind: "secondary",
1141
+ permissions: ["admin"],
1142
+ },
1042
1143
  "create-post": {
1043
1144
  iconKey: "create",
1044
1145
  id: "blog.create-post",
@@ -1113,6 +1214,23 @@ export const ACTIONS = {
1113
1214
  },
1114
1215
  },
1115
1216
  EVENT: {
1217
+ /*
1218
+ * Clone, admin-only, backed by POST /api/admin/events/[id]/duplicate. A
1219
+ * recurring sale is the real use case: same copy, same prizes, new dates.
1220
+ *
1221
+ * 🛑 The endpoint rebuilds every lottery slot from its prize definition and
1222
+ * resets the booking half — a spread would attribute a real buyer’s
1223
+ * purchased slot to a brand-new event.
1224
+ */
1225
+ duplicate: {
1226
+ iconKey: "duplicate",
1227
+ id: "event.duplicate",
1228
+ label: "Duplicate",
1229
+ ariaLabel: "Duplicate this event as a draft",
1230
+ description: "Copy this event into a new draft, without its entries, winner or slot bookings.",
1231
+ kind: "secondary",
1232
+ permissions: ["admin"],
1233
+ },
1116
1234
  /*
1117
1235
  * This bucket had only register / cancel-registration / draw-raffle-winner
1118
1236
  * — no edit, no delete — so the owner affordance on an event card or detail
@@ -2208,6 +2326,27 @@ export const ACTIONS = {
2208
2326
  kind: "ghost",
2209
2327
  permissions: ["admin"],
2210
2328
  },
2329
+ /*
2330
+ * Homepage sections have no bucket of their own, so their edit leaf lives
2331
+ * here beside the carousel pair for the same reason — both are homepage
2332
+ * chrome an admin edits from a list rather than from a detail page.
2333
+ *
2334
+ * 🛑 There is deliberately NO `delete-section` counterpart. Section `order`
2335
+ * is POSITIONAL, so removing one silently renumbers its neighbours — not
2336
+ * something to offer from the public homepage, where the only confirmation
2337
+ * a person gets is a dialog. `OwnerQuickActions` enforces the absence by
2338
+ * omitting `remove` from the SECTION mapping, so no call site can pass a
2339
+ * delete handler and have it render.
2340
+ */
2341
+ "edit-section": {
2342
+ iconKey: "edit",
2343
+ id: "admin.edit-section",
2344
+ label: "Edit Section",
2345
+ ariaLabel: "Edit this homepage section",
2346
+ description: "Open the homepage sections list, which owns this section's config.",
2347
+ kind: "ghost",
2348
+ permissions: ["admin"],
2349
+ },
2211
2350
  "delete-carousel": {
2212
2351
  iconKey: "delete",
2213
2352
  id: "admin.delete-carousel",
@@ -78,7 +78,7 @@ export function roundShippingUp(rawRate) {
78
78
  export function reconcileShipping(quote, actual) {
79
79
  const charged = Number.isFinite(quote.chargedRate) ? Math.max(0, quote.chargedRate) : 0;
80
80
  const spent = Number.isFinite(actual.actualRate) ? Math.max(0, actual.actualRate) : 0;
81
- const delta = roundPaise(charged - spent);
81
+ const delta = roundMoney(charged - spent);
82
82
  return {
83
83
  sellerAbsorbs: delta < 0 ? Math.abs(delta) : 0,
84
84
  buyerPoints: delta > 0 ? delta : 0,
@@ -88,12 +88,11 @@ export function reconcileShipping(quote, actual) {
88
88
  /**
89
89
  * Two decimal places.
90
90
  *
91
- * Money here is decimal rupees (the paise→rupees migration, Root Cause #31), so
92
- * a subtraction can leave a float artefact — `100 - 47.1` is `52.900000000000006`,
93
- * and that would become a points credit with eleven decimal places on a user's
94
- * balance.
91
+ * Money is stored as decimal rupees (Root Cause #31), so a subtraction can
92
+ * leave a float artefact — `100 - 47.1` is `52.900000000000006`, and that would
93
+ * become a points credit with eleven decimal places on a user's balance.
95
94
  */
96
- function roundPaise(value) {
95
+ function roundMoney(value) {
97
96
  return Math.round(value * 100) / 100;
98
97
  }
99
98
  /**
package/dist/client.d.ts CHANGED
@@ -387,6 +387,8 @@ export type { UserAccountHubViewProps, UserAccountHubViewLabels, UserOrdersViewP
387
387
  export { useOrders, useOrder, OrdersList } from "./features/orders/index";
388
388
  export { OrderStatusTimeline } from "./features/orders/components/OrderStatusTimeline";
389
389
  export type { OrderStatusTimelineProps } from "./features/orders/components/OrderStatusTimeline";
390
+ export { EmiInstallmentsPanel } from "./features/orders/components/EmiInstallmentsPanel";
391
+ export type { EmiInstallmentsPanelProps } from "./features/orders/components/EmiInstallmentsPanel";
390
392
  export { CountdownDisplay } from "./ui/components/CountdownDisplay";
391
393
  export type { CountdownDisplayProps } from "./ui/components/CountdownDisplay";
392
394
  export { OrderAddonBadges } from "./features/orders/components/OrderAddonBadges";
@@ -435,6 +437,8 @@ export type { ListingTypeCapability } from "./_internal/shared/listing-types/cap
435
437
  export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
436
438
  export { useStoreListingEntitlements } from "./features/seller/hooks/useStoreListingEntitlements";
437
439
  export { OwnerQuickActions } from "./features/admin/components/OwnerQuickActions";
440
+ export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwnerQuickActions";
441
+ export type { RecordOwnerQuickActionsProps, RecordQuickActionResource } from "./features/admin/components/RecordOwnerQuickActions";
438
442
  export { YourPurchaseStrip } from "./features/orders/components/YourPurchaseStrip";
439
443
  export { ReviewFormDrawer, reviewFormSchema } from "./features/reviews/components/ReviewFormDrawer";
440
444
  export type { ReviewFormDrawerProps, ReviewFormValues } from "./features/reviews/components/ReviewFormDrawer";
@@ -584,3 +588,22 @@ export { PriceIndexToolView } from "./features/price-index/components/PriceIndex
584
588
  export type { PriceIndexToolViewProps, PriceIndexToolResponse, PriceIndexResultRow, PriceIndexBandRow, PriceIndexConditionRow, } from "./features/price-index/components/PriceIndexToolView";
585
589
  export { COURIER_RATING_MIN_CONFIDENT_SAMPLES, courierRatingDisplay, isValidCourierVote, courierVoteId, rollUpCourierVotes, } from "./_internal/shared/features/shipping/courier-rating";
586
590
  export type { CourierRatingPair, CourierRatingDisplay, CourierRatingVote, CourierStats, } from "./_internal/shared/features/shipping/courier-rating";
591
+ /**
592
+ * The `/api/checkout/pricing-preview` RESPONSE CONTRACT.
593
+ *
594
+ * 🛑 `export type`, from a `_internal/server` module, deliberately. A type-only
595
+ * re-export is fully erased at compile time — `audit-client-server-only-leak`
596
+ * states that explicitly — so this pulls no server chain into the client
597
+ * bundle. It is the one exception worth making for this module.
598
+ *
599
+ * The consumer had been keeping a HAND-WRITTEN copy of both shapes in
600
+ * `src/lib/api/payment-client.ts`, under a comment reading "mirrors
601
+ * `CheckoutPricingPreviewStore` on the server". It drifted exactly as Root
602
+ * Cause #42 predicts: the server gained `emiSurcharge`, the mirror did not, and
603
+ * the preview the cart rendered could not be assigned to the breakdown
604
+ * component that displays it. Four `tsc` errors across the cart and checkout
605
+ * clients, all from one missing field in a second definition of one shape.
606
+ *
607
+ * A response contract has exactly one owner — the handler that produces it.
608
+ */
609
+ export type { CheckoutPricingPreview, CheckoutPricingPreviewStore, } from "./_internal/server/features/checkout/actions";
package/dist/client.js CHANGED
@@ -405,6 +405,8 @@ export { couponDraftToPayload } from "./features/seller/schemas/coupon-form.js";
405
405
  export { UserAccountHubView, UserOrdersView, UserBidsView, OrderDetailView, UserReturnsView, useNotifications } from "./features/account/index.js";
406
406
  export { useOrders, useOrder, OrdersList } from "./features/orders/index.js";
407
407
  export { OrderStatusTimeline } from "./features/orders/components/OrderStatusTimeline.js";
408
+ // Buyer-facing EMI schedule with a pay-any-instalment action.
409
+ export { EmiInstallmentsPanel } from "./features/orders/components/EmiInstallmentsPanel.js";
408
410
  export { CountdownDisplay } from "./ui/components/CountdownDisplay.js";
409
411
  export { OrderAddonBadges } from "./features/orders/components/OrderAddonBadges.js";
410
412
  // Manual-payment (cash / UPI / EMI) shared constants — pure data + predicates,
@@ -458,6 +460,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
458
460
  export { useStoreListingEntitlements } from "./features/seller/hooks/useStoreListingEntitlements.js";
459
461
  // Pencil / clone / bin for whoever owns the record, on a PUBLIC surface.
460
462
  export { OwnerQuickActions } from "./features/admin/components/OwnerQuickActions.js";
463
+ export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwnerQuickActions.js";
461
464
  // "You ordered this - track it" / "your review - edit it", on a product page.
462
465
  export { YourPurchaseStrip } from "./features/orders/components/YourPurchaseStrip.js";
463
466
  // The first review form this app has had - see the component's header.
@@ -126,6 +126,10 @@ export declare const ADMIN_ENDPOINTS: {
126
126
  readonly BUNDLE_REBUILD: (id: string) => string;
127
127
  readonly CATEGORIES: "/api/admin/categories";
128
128
  readonly CATEGORY_BY_ID: (id: string) => string;
129
+ readonly PARTS: "/api/admin/parts";
130
+ readonly PART_BY_ID: (id: string) => string;
131
+ readonly BEYS: "/api/admin/beys";
132
+ readonly BEY_BY_ID: (id: string) => string;
129
133
  readonly BRANDS: "/api/admin/brands";
130
134
  readonly BRAND_BY_ID: (id: string) => string;
131
135
  readonly FAQS: "/api/admin/faqs";
@@ -690,6 +694,10 @@ export declare const API_ENDPOINTS: {
690
694
  readonly BUNDLE_REBUILD: (id: string) => string;
691
695
  readonly CATEGORIES: "/api/admin/categories";
692
696
  readonly CATEGORY_BY_ID: (id: string) => string;
697
+ readonly PARTS: "/api/admin/parts";
698
+ readonly PART_BY_ID: (id: string) => string;
699
+ readonly BEYS: "/api/admin/beys";
700
+ readonly BEY_BY_ID: (id: string) => string;
693
701
  readonly BRANDS: "/api/admin/brands";
694
702
  readonly BRAND_BY_ID: (id: string) => string;
695
703
  readonly FAQS: "/api/admin/faqs";
@@ -1219,6 +1227,10 @@ export declare const API_ROUTES: {
1219
1227
  readonly BUNDLE_REBUILD: (id: string) => string;
1220
1228
  readonly CATEGORIES: "/api/admin/categories";
1221
1229
  readonly CATEGORY_BY_ID: (id: string) => string;
1230
+ readonly PARTS: "/api/admin/parts";
1231
+ readonly PART_BY_ID: (id: string) => string;
1232
+ readonly BEYS: "/api/admin/beys";
1233
+ readonly BEY_BY_ID: (id: string) => string;
1222
1234
  readonly BRANDS: "/api/admin/brands";
1223
1235
  readonly BRAND_BY_ID: (id: string) => string;
1224
1236
  readonly FAQS: "/api/admin/faqs";
@@ -157,6 +157,16 @@ export const ADMIN_ENDPOINTS = {
157
157
  BUNDLE_REBUILD: (id) => `/api/admin/bundles/${id}/rebuild`,
158
158
  CATEGORIES: "/api/admin/categories",
159
159
  CATEGORY_BY_ID: (id) => `/api/admin/categories/${id}`,
160
+ /*
161
+ * The generated reference databases. PATCH corrects a wrong extraction;
162
+ * DELETE removes a row that should not exist. No POST — see the note on
163
+ * `ROUTES.ADMIN.PARTS` for why creating one by hand is not a supported
164
+ * operation on generated data.
165
+ */
166
+ PARTS: "/api/admin/parts",
167
+ PART_BY_ID: (id) => `/api/admin/parts/${id}`,
168
+ BEYS: "/api/admin/beys",
169
+ BEY_BY_ID: (id) => `/api/admin/beys/${id}`,
160
170
  BRANDS: "/api/admin/brands",
161
171
  BRAND_BY_ID: (id) => `/api/admin/brands/${id}`,
162
172
  FAQS: "/api/admin/faqs",
@@ -509,6 +509,52 @@ export declare const STORE_FIELDS: {
509
509
  readonly TOTAL_PRODUCTS: "stats.totalProducts";
510
510
  };
511
511
  };
512
+ /**
513
+ * The two GENERATED reference databases.
514
+ *
515
+ * 🛑 NOT reused from `CATEGORY_FIELDS`. `audit-route-nav-field-constants`
516
+ * suggested `CATEGORY_FIELDS.NAME` for a query against `partGlossary`, and the
517
+ * string would have been identical — which is exactly why that would be the
518
+ * wrong fix. A constant's job is to say WHICH collection's field is meant, so
519
+ * that renaming one collection's field cannot silently retarget another's
520
+ * query. Same bytes, different claim.
521
+ */
522
+ /**
523
+ * `modelComposition` — which parts a model is built from.
524
+ *
525
+ * 🛑 ITS OWN `CATEGORY_ID`, not `BEY_FIELDS.CATEGORY_ID`. The audit suggested
526
+ * the bey constant for a query against THIS collection, and the string is
527
+ * identical — which is precisely the substitution the comment on
528
+ * `PART_GLOSSARY_FIELDS` warns about. A composition row's `categoryId` points
529
+ * at the model leaf it describes; a bey's points at its own paired leaf. Same
530
+ * bytes, two different facts, and only one of them moves if either is renamed.
531
+ */
532
+ export declare const MODEL_COMPOSITION_FIELDS: {
533
+ readonly ID: "id";
534
+ readonly CATEGORY_ID: "categoryId";
535
+ readonly SLOT: "slot";
536
+ };
537
+ export declare const PART_GLOSSARY_FIELDS: {
538
+ readonly ID: "id";
539
+ readonly NAME: "name";
540
+ readonly SLOT: "slot";
541
+ readonly CLASSIFICATION: "classification";
542
+ readonly CODE: "code";
543
+ readonly WEIGHT_G: "weightG";
544
+ /** Set by `applyAdminEdit`; the seed loader queries on it. */
545
+ readonly ADMIN_EDITED: "adminEdited";
546
+ readonly ADMIN_EDITED_FIELDS: "adminEditedFields";
547
+ };
548
+ export declare const BEY_FIELDS: {
549
+ readonly ID: "id";
550
+ readonly SLUG: "slug";
551
+ readonly NAME: "name";
552
+ readonly TYPE: "type";
553
+ readonly SYSTEM: "system";
554
+ readonly CATEGORY_ID: "categoryId";
555
+ readonly ADMIN_EDITED: "adminEdited";
556
+ readonly ADMIN_EDITED_FIELDS: "adminEditedFields";
557
+ };
512
558
  export declare const CATEGORY_FIELDS: {
513
559
  readonly ID: "id";
514
560
  readonly NAME: "name";
@@ -613,6 +613,52 @@ export const STORE_FIELDS = {
613
613
  // ============================================================================
614
614
  // CATEGORY FIELDS
615
615
  // ============================================================================
616
+ /**
617
+ * The two GENERATED reference databases.
618
+ *
619
+ * 🛑 NOT reused from `CATEGORY_FIELDS`. `audit-route-nav-field-constants`
620
+ * suggested `CATEGORY_FIELDS.NAME` for a query against `partGlossary`, and the
621
+ * string would have been identical — which is exactly why that would be the
622
+ * wrong fix. A constant's job is to say WHICH collection's field is meant, so
623
+ * that renaming one collection's field cannot silently retarget another's
624
+ * query. Same bytes, different claim.
625
+ */
626
+ /**
627
+ * `modelComposition` — which parts a model is built from.
628
+ *
629
+ * 🛑 ITS OWN `CATEGORY_ID`, not `BEY_FIELDS.CATEGORY_ID`. The audit suggested
630
+ * the bey constant for a query against THIS collection, and the string is
631
+ * identical — which is precisely the substitution the comment on
632
+ * `PART_GLOSSARY_FIELDS` warns about. A composition row's `categoryId` points
633
+ * at the model leaf it describes; a bey's points at its own paired leaf. Same
634
+ * bytes, two different facts, and only one of them moves if either is renamed.
635
+ */
636
+ export const MODEL_COMPOSITION_FIELDS = {
637
+ ID: "id",
638
+ CATEGORY_ID: "categoryId",
639
+ SLOT: "slot",
640
+ };
641
+ export const PART_GLOSSARY_FIELDS = {
642
+ ID: "id",
643
+ NAME: "name",
644
+ SLOT: "slot",
645
+ CLASSIFICATION: "classification",
646
+ CODE: "code",
647
+ WEIGHT_G: "weightG",
648
+ /** Set by `applyAdminEdit`; the seed loader queries on it. */
649
+ ADMIN_EDITED: "adminEdited",
650
+ ADMIN_EDITED_FIELDS: "adminEditedFields",
651
+ };
652
+ export const BEY_FIELDS = {
653
+ ID: "id",
654
+ SLUG: "slug",
655
+ NAME: "name",
656
+ TYPE: "type",
657
+ SYSTEM: "system",
658
+ CATEGORY_ID: "categoryId",
659
+ ADMIN_EDITED: "adminEdited",
660
+ ADMIN_EDITED_FIELDS: "adminEditedFields",
661
+ };
616
662
  export const CATEGORY_FIELDS = {
617
663
  ID: "id",
618
664
  NAME: "name",
@@ -16,6 +16,7 @@ import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js"
16
16
  import { normalizeListingType } from "../../products/utils/listing-type.js";
17
17
  import { isAdminUser, isSellerUser } from "../../auth/role-predicates.js";
18
18
  import { User, Star, ShoppingBag, Package, Trophy, Globe, MapPin, ExternalLink } from "lucide-react";
19
+ import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
19
20
  const __P = {
20
21
  p4: "p-[var(--appkit-space-4)]",
21
22
  p6: "p-[var(--appkit-space-6)]",
@@ -120,7 +121,7 @@ export async function PublicProfileView({ userId, tab, }) {
120
121
  const profileHeroCtx = { displayName, photoURL, memberSince, isSeller, storeSlug, flex, page };
121
122
  const statItems = buildProfileStatItems(t, { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller });
122
123
  const activeTab = tab === "catalogue" ? "catalogue" : "overview";
123
- return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [_jsxs(Row, { gap: "md", padding: "t-xs", border: "bottom", children: [_jsx(TextLink, { href: `/profile/${userId}/overview`, variant: activeTab === "overview" ? "default" : "muted", weight: activeTab === "overview" ? "semibold" : "normal", children: "Overview" }), _jsx(TextLink, { href: `/profile/${userId}/catalogue`, variant: activeTab === "catalogue" ? "default" : "muted", weight: activeTab === "catalogue" ? "semibold" : "normal", children: "Catalogue" })] }), activeTab === "catalogue" ? (_jsx(PublicCatalogueView, { ownerSlug: userId })) : (_jsxs(_Fragment, { children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName), reviewsReceivedAsBuyer.length > 0 && renderBuyerReceivedReviewsSection(t, reviewsReceivedAsBuyer)] })), _jsx(Row, { justify: "center", padding: "t-xs", children: _jsxs(Link, { href: String(ROUTES.HOME), className: "text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]", children: ["\u2190 ", t("backHome")] }) })] })] }));
124
+ return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [_jsx(Row, { justify: "end", children: _jsx(OwnerQuickActions, { resource: "USER", placement: "detail", recordLabel: displayName, editHref: String(ROUTES.ADMIN.USER_EDIT(userId)) }) }), _jsxs(Row, { gap: "md", padding: "t-xs", border: "bottom", children: [_jsx(TextLink, { href: `/profile/${userId}/overview`, variant: activeTab === "overview" ? "default" : "muted", weight: activeTab === "overview" ? "semibold" : "normal", children: "Overview" }), _jsx(TextLink, { href: `/profile/${userId}/catalogue`, variant: activeTab === "catalogue" ? "default" : "muted", weight: activeTab === "catalogue" ? "semibold" : "normal", children: "Catalogue" })] }), activeTab === "catalogue" ? (_jsx(PublicCatalogueView, { ownerSlug: userId })) : (_jsxs(_Fragment, { children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName), reviewsReceivedAsBuyer.length > 0 && renderBuyerReceivedReviewsSection(t, reviewsReceivedAsBuyer)] })), _jsx(Row, { justify: "center", padding: "t-xs", children: _jsxs(Link, { href: String(ROUTES.HOME), className: "text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]", children: ["\u2190 ", t("backHome")] }) })] })] }));
124
125
  }
125
126
  function buildProfileStatItems(t, ctx) {
126
127
  const { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller } = ctx;