@mohasinac/appkit 4.62.1 → 4.63.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 +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server.d.ts +27 -1
- package/dist/server.js +144 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -25,6 +25,11 @@ export const blogAdminColumns = [
|
|
|
25
25
|
header: "Featured",
|
|
26
26
|
render: (p) => (p.isFeatured ? "Yes" : "No"),
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
key: "isPinned",
|
|
30
|
+
header: "Pinned",
|
|
31
|
+
render: (p) => (p.isPinned ? "Yes" : "No"),
|
|
32
|
+
},
|
|
28
33
|
{
|
|
29
34
|
key: "readTimeMinutes",
|
|
30
35
|
header: "Read Time",
|
|
@@ -3,6 +3,7 @@ import { blogRepository } from "../../../repositories/index.js";
|
|
|
3
3
|
import { Container, Heading, Main, Section } from "../../../ui/index.js";
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot.js";
|
|
5
5
|
import { BlogIndexListing } from "./BlogIndexListing.js";
|
|
6
|
+
import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
|
|
6
7
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
7
8
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
8
9
|
function sp(params, key) {
|
|
@@ -10,7 +11,7 @@ function sp(params, key) {
|
|
|
10
11
|
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
11
12
|
}
|
|
12
13
|
export async function BlogIndexPageView({ searchParams = {} }) {
|
|
13
|
-
const sort = sp(searchParams, "sort") ||
|
|
14
|
+
const sort = sp(searchParams, "sort") || BLOG_DEFAULT_PUBLIC_SORT;
|
|
14
15
|
const page = Number(sp(searchParams, "page")) || 1;
|
|
15
16
|
const pageSize = Number(sp(searchParams, "pageSize")) || 24;
|
|
16
17
|
const category = sp(searchParams, "category");
|
|
@@ -223,6 +223,11 @@ BlogRepository.SIEVE_FIELDS = {
|
|
|
223
223
|
authorName: { canFilter: true, canSort: true },
|
|
224
224
|
authorId: { canFilter: true, canSort: false },
|
|
225
225
|
isFeatured: { canFilter: true, canSort: false },
|
|
226
|
+
// canSort because it is the FIRST key of the default sort
|
|
227
|
+
// `-isPinned,-publishedAt`. A `canSort: false` field is silently dropped by
|
|
228
|
+
// sievejs (Root Cause #63), which would leave the index ordered by date
|
|
229
|
+
// alone and the pin doing nothing, with no error anywhere.
|
|
230
|
+
isPinned: { canFilter: true, canSort: true },
|
|
226
231
|
readTimeMinutes: { canFilter: true, canSort: true },
|
|
227
232
|
views: { canFilter: true, canSort: true },
|
|
228
233
|
publishedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
@@ -47,6 +47,7 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
47
47
|
publishedAt: z.ZodString;
|
|
48
48
|
authorName: z.ZodString;
|
|
49
49
|
isFeatured: z.ZodBoolean;
|
|
50
|
+
isPinned: z.ZodBoolean;
|
|
50
51
|
}, "strip", z.ZodTypeAny, {
|
|
51
52
|
status: "published" | "draft" | "archived";
|
|
52
53
|
title: string;
|
|
@@ -60,6 +61,7 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
60
61
|
excerpt: string;
|
|
61
62
|
coverImage: string;
|
|
62
63
|
authorName: string;
|
|
64
|
+
isPinned: boolean;
|
|
63
65
|
metaTitle: string;
|
|
64
66
|
metaDescription: string;
|
|
65
67
|
}, {
|
|
@@ -75,6 +77,7 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
75
77
|
excerpt: string;
|
|
76
78
|
coverImage: string;
|
|
77
79
|
authorName: string;
|
|
80
|
+
isPinned: boolean;
|
|
78
81
|
metaTitle: string;
|
|
79
82
|
metaDescription: string;
|
|
80
83
|
}>;
|
|
@@ -97,4 +97,5 @@ export const blogDraftSchema = z.object({
|
|
|
97
97
|
}),
|
|
98
98
|
authorName: annotate(z.string(), { section: "publish", order: 3, row: "pair" }),
|
|
99
99
|
isFeatured: annotate(z.boolean(), { section: "publish", order: 4, row: "quarter" }),
|
|
100
|
+
isPinned: annotate(z.boolean(), { section: "publish", order: 5, row: "quarter" }),
|
|
100
101
|
});
|
|
@@ -29,6 +29,23 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
29
29
|
category: BlogPostCategory;
|
|
30
30
|
tags: string[];
|
|
31
31
|
isFeatured: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Keeps the post at the top of the blog index and the homepage
|
|
34
|
+
* `blog-articles` section, above newer posts.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 REQUIRED, not optional, and that is load-bearing. The default sort is
|
|
37
|
+
* `-isPinned,-publishedAt`, and Firestore EXCLUDES a document that lacks an
|
|
38
|
+
* `orderBy` field (Root Cause #100) — so an optional `isPinned` would drop
|
|
39
|
+
* every pre-existing post out of its own index, silently, with a 200.
|
|
40
|
+
* A `mapDoc` default cannot rescue it either: that is a read-side fixup and
|
|
41
|
+
* a query never sees it (Root Cause #101). Every row needs a real `false`
|
|
42
|
+
* written, which is why the seed carries it explicitly.
|
|
43
|
+
*
|
|
44
|
+
* Distinct from `isFeatured` on purpose: featured drives the badge and the
|
|
45
|
+
* featured-card treatment, and there are several featured posts. Overloading
|
|
46
|
+
* it would make "featured" and "first" the same decision.
|
|
47
|
+
*/
|
|
48
|
+
isPinned: boolean;
|
|
32
49
|
status: BlogPostStatus;
|
|
33
50
|
publishedAt?: Date;
|
|
34
51
|
authorId: string;
|
|
@@ -40,7 +57,25 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
40
57
|
metaDescription?: string;
|
|
41
58
|
}
|
|
42
59
|
export declare const BLOG_POSTS_COLLECTION: "blogPosts";
|
|
43
|
-
export declare const BLOG_POSTS_INDEXED_FIELDS: readonly ["searchTxt", "status", "publishedAt", "isFeatured", "category", "slug"];
|
|
60
|
+
export declare const BLOG_POSTS_INDEXED_FIELDS: readonly ["searchTxt", "status", "publishedAt", "isFeatured", "isPinned", "category", "slug"];
|
|
61
|
+
/**
|
|
62
|
+
* The default ordering for every PUBLIC blog surface — the `/blog` index, the
|
|
63
|
+
* `/api/blog` route that refetches it, and the homepage `blog-articles` feed.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 ONE constant, three consumers, deliberately. The SSR view hands its result
|
|
66
|
+
* to the listing hook as `initialData`, and that hook sets `staleTime: Infinity`
|
|
67
|
+
* when given SSR data — so if the route's default sort differed from the view's
|
|
68
|
+
* by even a character, the first paint would be frozen in one order while the
|
|
69
|
+
* query key described another (Root Cause #30). Three literals is how that
|
|
70
|
+
* drift happens; one import is how it cannot.
|
|
71
|
+
*
|
|
72
|
+
* `isPinned` leads so pinned posts sit above newer ones. Needs the composite
|
|
73
|
+
* indexes on (status, isPinned DESC, publishedAt DESC) and its category /
|
|
74
|
+
* isFeatured / searchTxt variants — a multi-field sieve sort becomes two
|
|
75
|
+
* `orderBy` calls, and a missing index is a silent FAILED_PRECONDITION that
|
|
76
|
+
* every caller swallows into an empty page (Root Cause #2 / #59).
|
|
77
|
+
*/
|
|
78
|
+
export declare const BLOG_DEFAULT_PUBLIC_SORT: "-isPinned,-publishedAt";
|
|
44
79
|
export declare const BLOG_POST_FIELDS: {
|
|
45
80
|
/** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
|
|
46
81
|
readonly SEARCH_TXT: "searchTxt";
|
|
@@ -56,6 +91,7 @@ export declare const BLOG_POST_FIELDS: {
|
|
|
56
91
|
readonly CATEGORY: "category";
|
|
57
92
|
readonly TAGS: "tags";
|
|
58
93
|
readonly IS_FEATURED: "isFeatured";
|
|
94
|
+
readonly IS_PINNED: "isPinned";
|
|
59
95
|
readonly STATUS: "status";
|
|
60
96
|
readonly PUBLISHED_AT: "publishedAt";
|
|
61
97
|
readonly AUTHOR_ID: "authorId";
|
|
@@ -89,6 +125,7 @@ export type BlogPostUpdateInput = Partial<Omit<BlogPostDocument, "id" | "created
|
|
|
89
125
|
export declare const blogPostQueryHelpers: {
|
|
90
126
|
readonly published: () => readonly ["status", "==", BlogPostStatus];
|
|
91
127
|
readonly featured: () => readonly ["isFeatured", "==", true];
|
|
128
|
+
readonly pinned: () => readonly ["isPinned", "==", true];
|
|
92
129
|
readonly byCategory: (category: BlogPostCategory) => readonly ["category", "==", BlogPostCategory];
|
|
93
130
|
readonly bySlug: (slug: string) => readonly ["slug", "==", string];
|
|
94
131
|
readonly byAuthor: (authorId: string) => readonly ["authorId", "==", string];
|
|
@@ -14,9 +14,28 @@ export const BLOG_POSTS_INDEXED_FIELDS = [
|
|
|
14
14
|
"status",
|
|
15
15
|
"publishedAt",
|
|
16
16
|
"isFeatured",
|
|
17
|
+
"isPinned",
|
|
17
18
|
"category",
|
|
18
19
|
"slug",
|
|
19
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* The default ordering for every PUBLIC blog surface — the `/blog` index, the
|
|
23
|
+
* `/api/blog` route that refetches it, and the homepage `blog-articles` feed.
|
|
24
|
+
*
|
|
25
|
+
* 🛑 ONE constant, three consumers, deliberately. The SSR view hands its result
|
|
26
|
+
* to the listing hook as `initialData`, and that hook sets `staleTime: Infinity`
|
|
27
|
+
* when given SSR data — so if the route's default sort differed from the view's
|
|
28
|
+
* by even a character, the first paint would be frozen in one order while the
|
|
29
|
+
* query key described another (Root Cause #30). Three literals is how that
|
|
30
|
+
* drift happens; one import is how it cannot.
|
|
31
|
+
*
|
|
32
|
+
* `isPinned` leads so pinned posts sit above newer ones. Needs the composite
|
|
33
|
+
* indexes on (status, isPinned DESC, publishedAt DESC) and its category /
|
|
34
|
+
* isFeatured / searchTxt variants — a multi-field sieve sort becomes two
|
|
35
|
+
* `orderBy` calls, and a missing index is a silent FAILED_PRECONDITION that
|
|
36
|
+
* every caller swallows into an empty page (Root Cause #2 / #59).
|
|
37
|
+
*/
|
|
38
|
+
export const BLOG_DEFAULT_PUBLIC_SORT = "-isPinned,-publishedAt";
|
|
20
39
|
export const BLOG_POST_FIELDS = {
|
|
21
40
|
/** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
|
|
22
41
|
SEARCH_TXT: "searchTxt",
|
|
@@ -32,6 +51,7 @@ export const BLOG_POST_FIELDS = {
|
|
|
32
51
|
CATEGORY: "category",
|
|
33
52
|
TAGS: "tags",
|
|
34
53
|
IS_FEATURED: "isFeatured",
|
|
54
|
+
IS_PINNED: "isPinned",
|
|
35
55
|
STATUS: "status",
|
|
36
56
|
PUBLISHED_AT: "publishedAt",
|
|
37
57
|
AUTHOR_ID: "authorId",
|
|
@@ -64,6 +84,7 @@ export const DEFAULT_BLOG_POST_DATA = {
|
|
|
64
84
|
category: "news",
|
|
65
85
|
tags: [],
|
|
66
86
|
isFeatured: false,
|
|
87
|
+
isPinned: false,
|
|
67
88
|
status: "draft",
|
|
68
89
|
authorId: "",
|
|
69
90
|
authorName: "",
|
|
@@ -80,6 +101,7 @@ export const BLOG_POST_PUBLIC_FIELDS = [
|
|
|
80
101
|
"category",
|
|
81
102
|
"tags",
|
|
82
103
|
"isFeatured",
|
|
104
|
+
"isPinned",
|
|
83
105
|
"status",
|
|
84
106
|
"publishedAt",
|
|
85
107
|
"authorName",
|
|
@@ -94,6 +116,7 @@ export const BLOG_POST_PUBLIC_FIELDS = [
|
|
|
94
116
|
export const blogPostQueryHelpers = {
|
|
95
117
|
published: () => ["status", "==", "published"],
|
|
96
118
|
featured: () => ["isFeatured", "==", true],
|
|
119
|
+
pinned: () => ["isPinned", "==", true],
|
|
97
120
|
byCategory: (category) => ["category", "==", category],
|
|
98
121
|
bySlug: (slug) => ["slug", "==", slug],
|
|
99
122
|
byAuthor: (authorId) => ["authorId", "==", authorId],
|
|
@@ -99,6 +99,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
99
99
|
category: z.ZodString;
|
|
100
100
|
tags: z.ZodArray<z.ZodString, "many">;
|
|
101
101
|
isFeatured: z.ZodBoolean;
|
|
102
|
+
isPinned: z.ZodBoolean;
|
|
102
103
|
status: z.ZodEnum<["draft", "published", "archived"]>;
|
|
103
104
|
publishedAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
104
105
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
@@ -134,6 +135,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
134
135
|
authorId: string;
|
|
135
136
|
authorName: string;
|
|
136
137
|
readTimeMinutes: number;
|
|
138
|
+
isPinned: boolean;
|
|
137
139
|
publishedAt?: string | Date | z.objectOutputType<{
|
|
138
140
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
139
141
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
@@ -187,6 +189,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
187
189
|
authorId: string;
|
|
188
190
|
authorName: string;
|
|
189
191
|
readTimeMinutes: number;
|
|
192
|
+
isPinned: boolean;
|
|
190
193
|
publishedAt?: string | Date | z.objectInputType<{
|
|
191
194
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
192
195
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
@@ -343,6 +346,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
343
346
|
category: z.ZodEnum<["news", "tips", "guides", "updates", "community"]>;
|
|
344
347
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
345
348
|
isFeatured: z.ZodOptional<z.ZodBoolean>;
|
|
349
|
+
isPinned: z.ZodOptional<z.ZodBoolean>;
|
|
346
350
|
status: z.ZodEnum<["draft", "published", "archived"]>;
|
|
347
351
|
publishedAt: z.ZodOptional<z.ZodString>;
|
|
348
352
|
authorId: z.ZodOptional<z.ZodString>;
|
|
@@ -401,6 +405,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
401
405
|
authorId?: string | undefined;
|
|
402
406
|
authorName?: string | undefined;
|
|
403
407
|
readTimeMinutes?: number | undefined;
|
|
408
|
+
isPinned?: boolean | undefined;
|
|
404
409
|
authorAvatar?: string | undefined;
|
|
405
410
|
metaTitle?: string | undefined;
|
|
406
411
|
metaDescription?: string | undefined;
|
|
@@ -430,6 +435,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
430
435
|
authorId?: string | undefined;
|
|
431
436
|
authorName?: string | undefined;
|
|
432
437
|
readTimeMinutes?: number | undefined;
|
|
438
|
+
isPinned?: boolean | undefined;
|
|
433
439
|
additionalImages?: {
|
|
434
440
|
type: "video" | "image" | "file";
|
|
435
441
|
url: string;
|
|
@@ -18,6 +18,7 @@ export const blogPostFirestoreSchema = z.object({
|
|
|
18
18
|
category: z.string(),
|
|
19
19
|
tags: z.array(z.string()),
|
|
20
20
|
isFeatured: z.boolean(),
|
|
21
|
+
isPinned: z.boolean(),
|
|
21
22
|
status: blogPostStatusEnumSchema,
|
|
22
23
|
publishedAt: firestoreDateSchema.optional(),
|
|
23
24
|
authorId: z.string(),
|
|
@@ -72,6 +73,7 @@ export const blogPostSchema = z.object({
|
|
|
72
73
|
category: blogPostCategorySchema,
|
|
73
74
|
tags: z.array(z.string()).optional(),
|
|
74
75
|
isFeatured: z.boolean().optional(),
|
|
76
|
+
isPinned: z.boolean().optional(),
|
|
75
77
|
status: blogPostStatusSchema,
|
|
76
78
|
publishedAt: z.string().optional(),
|
|
77
79
|
authorId: z.string().optional(),
|
|
@@ -15,6 +15,8 @@ export interface BlogPost {
|
|
|
15
15
|
category: BlogPostCategory;
|
|
16
16
|
tags?: string[];
|
|
17
17
|
isFeatured?: boolean;
|
|
18
|
+
/** Keeps the post above newer ones on the blog index and homepage. */
|
|
19
|
+
isPinned?: boolean;
|
|
18
20
|
status: BlogPostStatus;
|
|
19
21
|
publishedAt?: string;
|
|
20
22
|
authorId?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { FeeDerivations } from "../../../_internal/shared/fees/explain";
|
|
2
3
|
/**
|
|
3
4
|
* The aggregate price breakdown shown in the cart's expandable preview and in
|
|
4
5
|
* the checkout Order Summary.
|
|
@@ -86,5 +87,15 @@ export interface CartPriceBreakdownProps {
|
|
|
86
87
|
errorNote?: React.ReactNode;
|
|
87
88
|
isLoading?: boolean;
|
|
88
89
|
className?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Per-line "why am I paying this" disclosures, built by the
|
|
92
|
+
* `explain*` helpers in `_internal/shared/fees/explain`.
|
|
93
|
+
*
|
|
94
|
+
* 🛑 Optional, and an absent entry renders no disclosure rather than a
|
|
95
|
+
* placeholder. The cart renders this breakdown for a signed-out guest with
|
|
96
|
+
* no server preview at all — a line that cannot be explained must say
|
|
97
|
+
* nothing, never "we could not work this out", which reads as a fault.
|
|
98
|
+
*/
|
|
99
|
+
derivations?: FeeDerivations;
|
|
89
100
|
}
|
|
90
|
-
export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
|
|
101
|
+
export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, derivations, }: CartPriceBreakdownProps): React.JSX.Element;
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { Div, Row, Stack, Text } from "../../../ui/index.js";
|
|
3
|
+
import { Details, Div, Row, Stack, Summary, Text } from "../../../ui/index.js";
|
|
4
4
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The "how is this calculated?" disclosure under a fee line.
|
|
7
|
+
*
|
|
8
|
+
* 🛑 Collapsed by default, and that is the whole design. An unexplained charge
|
|
9
|
+
* is untrustworthy, but five expanded derivations turn the Order Summary into
|
|
10
|
+
* a wall of arithmetic at the moment a buyer is deciding to pay. The
|
|
11
|
+
* obligation is that the answer is *available at the point of the charge* —
|
|
12
|
+
* not that it is shouted.
|
|
13
|
+
*
|
|
14
|
+
* A native `<details>` rather than a tooltip or a modal: it works before
|
|
15
|
+
* hydration, it is keyboard-reachable for free, and it does not cover the
|
|
16
|
+
* number it is explaining.
|
|
17
|
+
*/
|
|
18
|
+
function Derivation({ derivation }) {
|
|
19
|
+
return (_jsxs(Details, { tone: "muted", padding: "sm", rounded: "md", children: [_jsx(Summary, { size: "xs", color: "muted", children: "How is this calculated?" }), _jsxs(Stack, { gap: "xs", className: "pt-[var(--appkit-space-2)]", children: [_jsx(Text, { size: "xs", color: "muted", children: derivation.summary }), derivation.steps.map((step, i) => (_jsx(Text, { size: "xs", color: "faint", className: "tabular-nums", children: step }, i)))] })] }));
|
|
20
|
+
}
|
|
21
|
+
function Line({ label, amount, tone = "muted", strong = false, derivation, }) {
|
|
22
|
+
const row = (_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "min-w-0", truncate: 1, children: label }), _jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "flex-shrink-0 tabular-nums", children: amount })] }));
|
|
23
|
+
if (!derivation)
|
|
24
|
+
return row;
|
|
25
|
+
return (_jsxs(Stack, { gap: "xs", children: [row, _jsx(Derivation, { derivation: derivation })] }));
|
|
7
26
|
}
|
|
8
27
|
/** "Shipping (2 stores)" — only qualifies when more than one store contributes. */
|
|
9
28
|
function withStoreCount(label, count) {
|
|
10
29
|
return count > 1 ? `${label} (${count} stores)` : label;
|
|
11
30
|
}
|
|
12
|
-
export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", }) {
|
|
31
|
+
export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", derivations, }) {
|
|
13
32
|
const money = (value) => formatCurrency(value, currency);
|
|
14
33
|
if (!preview) {
|
|
15
34
|
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), (errorNote ?? unavailableNote) && (_jsx(Text, { size: "xs", color: errorNote ? "error" : "muted", children: errorNote ?? unavailableNote }))] }));
|
|
@@ -21,7 +40,7 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
|
|
|
21
40
|
const subtotalLabel = itemCount !== undefined
|
|
22
41
|
? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
|
|
23
42
|
: "Subtotal";
|
|
24
|
-
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge) })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
|
|
43
|
+
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee), derivation: derivations?.codHandlingFee })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge), derivation: derivations?.emiSurcharge })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && (_jsx(Line, { label: "Platform fee", amount: money(preview.platformFee), derivation: derivations?.platformFee })), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee), derivation: derivations?.gstOnFee })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
|
|
25
44
|
? "Your points could not be applied to this order."
|
|
26
45
|
: storeCount > 1
|
|
27
46
|
? "None of these sellers accepts points."
|
|
@@ -147,6 +147,20 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
|
147
147
|
* and keeping both shapes invites a reader that handles only one. Same
|
|
148
148
|
* reasoning as `setStoreAddons` directly above.
|
|
149
149
|
*/
|
|
150
|
+
/**
|
|
151
|
+
* Set or clear the instalment plan.
|
|
152
|
+
*
|
|
153
|
+
* 🛑 `null` DELETES the field rather than storing `{ optIn: false }`. The
|
|
154
|
+
* schema's rule is that absent means "no instalments", so a second
|
|
155
|
+
* representation of the same state is one more thing every reader has to
|
|
156
|
+
* get right (Root Cause #42's shape). `tenureMonths` is stored verbatim and
|
|
157
|
+
* validated against the live `emi.tenureOptions` at order time — the admin
|
|
158
|
+
* can change those options after a cart was saved, so validating here would
|
|
159
|
+
* freeze a tenure that is no longer offered.
|
|
160
|
+
*/
|
|
161
|
+
setEmiPlan(userId: string, plan: {
|
|
162
|
+
tenureMonths: number;
|
|
163
|
+
} | null): Promise<void>;
|
|
150
164
|
setUseAllPoints(userId: string, useAllPoints: boolean): Promise<void>;
|
|
151
165
|
/**
|
|
152
166
|
* Cloud Functions: return refs of stale carts not updated within TTL.
|
|
@@ -608,6 +608,26 @@ export class CartRepository extends BaseRepository {
|
|
|
608
608
|
* and keeping both shapes invites a reader that handles only one. Same
|
|
609
609
|
* reasoning as `setStoreAddons` directly above.
|
|
610
610
|
*/
|
|
611
|
+
/**
|
|
612
|
+
* Set or clear the instalment plan.
|
|
613
|
+
*
|
|
614
|
+
* 🛑 `null` DELETES the field rather than storing `{ optIn: false }`. The
|
|
615
|
+
* schema's rule is that absent means "no instalments", so a second
|
|
616
|
+
* representation of the same state is one more thing every reader has to
|
|
617
|
+
* get right (Root Cause #42's shape). `tenureMonths` is stored verbatim and
|
|
618
|
+
* validated against the live `emi.tenureOptions` at order time — the admin
|
|
619
|
+
* can change those options after a cart was saved, so validating here would
|
|
620
|
+
* freeze a tenure that is no longer offered.
|
|
621
|
+
*/
|
|
622
|
+
async setEmiPlan(userId, plan) {
|
|
623
|
+
await this.db
|
|
624
|
+
.collection(this.collection)
|
|
625
|
+
.doc(userId)
|
|
626
|
+
.set({
|
|
627
|
+
emiPlan: plan ? { tenureMonths: plan.tenureMonths } : FieldValue.delete(),
|
|
628
|
+
updatedAt: new Date(),
|
|
629
|
+
}, { merge: true });
|
|
630
|
+
}
|
|
611
631
|
async setUseAllPoints(userId, useAllPoints) {
|
|
612
632
|
await this.db
|
|
613
633
|
.collection(this.collection)
|
|
@@ -331,6 +331,30 @@ export interface CartDocument extends BaseDocument {
|
|
|
331
331
|
* (D46). A number was the wrong shape as well as the wrong place.
|
|
332
332
|
*/
|
|
333
333
|
useAllPoints?: boolean;
|
|
334
|
+
/**
|
|
335
|
+
* "Pay in instalments", with the chosen tenure — an opt-in ON THE ORDER,
|
|
336
|
+
* orthogonal to how the token is tendered.
|
|
337
|
+
*
|
|
338
|
+
* 🛑 It lives HERE, on the cart, for exactly the reason `storeAddons` does:
|
|
339
|
+
* the online path creates the gateway order in one request
|
|
340
|
+
* (`/api/payment/create-order`, which decides the CAPTURE amount) and the
|
|
341
|
+
* order document in a later one (`/api/payment/verify`). A choice carried in
|
|
342
|
+
* a request body cannot be read by both, so the two would be free to
|
|
343
|
+
* disagree about how much of the total is deferred — and the difference is
|
|
344
|
+
* money captured that the order does not record (the D37/D38/D40 class).
|
|
345
|
+
*
|
|
346
|
+
* `useAllPoints` is the precedent and the shape is deliberately the same: a
|
|
347
|
+
* durable selection the server re-reads, never a client-supplied amount.
|
|
348
|
+
*
|
|
349
|
+
* Absent means a plain full-price order, which is why it is written with
|
|
350
|
+
* `FieldValue.delete()` rather than `{ optIn: false }` — a stored false and
|
|
351
|
+
* an absent field must not be two ways of saying the same thing.
|
|
352
|
+
*/
|
|
353
|
+
emiPlan?: CartEmiPlan;
|
|
354
|
+
}
|
|
355
|
+
/** The buyer's instalment selection. `tenureMonths` is validated against the live `emi.tenureOptions` at order time, never here. */
|
|
356
|
+
export interface CartEmiPlan {
|
|
357
|
+
tenureMonths: number;
|
|
334
358
|
}
|
|
335
359
|
export declare const CART_COLLECTION: "carts";
|
|
336
360
|
export declare const CART_INDEXED_FIELDS: string[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AttentionCounts } from "../../../_internal/server/features/dashboard/attention";
|
|
3
|
+
import type { DashboardHubSection } from "../../../_internal/shared/features/dashboard/hub-sections";
|
|
4
|
+
/**
|
|
5
|
+
* The dashboard hub — one component, two modes, zero copy of its own.
|
|
6
|
+
*
|
|
7
|
+
* ## Why one component and not two
|
|
8
|
+
*
|
|
9
|
+
* A landing page lists SECTIONS; a section hub lists SCREENS. Those read as
|
|
10
|
+
* two features and are one: a grid of link cards, each with a title, a
|
|
11
|
+
* sentence about what it is for, and an optional count of things waiting. Same
|
|
12
|
+
* prop surface, same renderer — which the Duplication Framework calls a
|
|
13
|
+
* consolidate-on-sight ("copies accept the same parameters and return the same
|
|
14
|
+
* shape"), not a Rule-of-Three wait.
|
|
15
|
+
*
|
|
16
|
+
* The thing it must not become is `AdminGuideHubView`, which is the visual
|
|
17
|
+
* pattern here and hand-writes its own 13-card list — so a guide added to the
|
|
18
|
+
* nav never appears on it. Every card below comes from a nav item's existing
|
|
19
|
+
* `label` and `description`.
|
|
20
|
+
*
|
|
21
|
+
* ## Not a client component
|
|
22
|
+
*
|
|
23
|
+
* It takes plain data and renders. No hook, no fetch, no `"use client"` — the
|
|
24
|
+
* page that mounts it is already per-request (its layout reads the session), so
|
|
25
|
+
* the counts arrive with the render rather than costing a second invocation
|
|
26
|
+
* after it. See `_internal/server/features/dashboard/data.ts`.
|
|
27
|
+
*
|
|
28
|
+
* ## No icons, on purpose
|
|
29
|
+
*
|
|
30
|
+
* A nav item carries no `iconKey`, so icons would mean a hand-written map of
|
|
31
|
+
* 22 sections or ~130 screens — a second list to drift from the first, which is
|
|
32
|
+
* the whole defect this feature exists to avoid (Root Cause #61). The section
|
|
33
|
+
* heading plus the card's own sentence is the distinction; `<Badge>` carries
|
|
34
|
+
* the only signal that needs to catch the eye.
|
|
35
|
+
*/
|
|
36
|
+
export interface DashboardHubViewProps {
|
|
37
|
+
/**
|
|
38
|
+
* Page heading. Omit when the grid is embedded in a page that already has
|
|
39
|
+
* one — `/store` renders it inside a `CollapsibleSection`, and a second
|
|
40
|
+
* `<h1>` mid-page breaks the heading outline for a screen reader.
|
|
41
|
+
*/
|
|
42
|
+
title?: string;
|
|
43
|
+
/** One sentence under the heading. Only rendered alongside a `title`. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/**
|
|
46
|
+
* What to render.
|
|
47
|
+
*
|
|
48
|
+
* - `"sections"` — one card per section, linking to its hub. The landing page.
|
|
49
|
+
* - `"screens"` — one card per screen in `sections[0]`. A section hub.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 A hub is passed a ONE-section array rather than a section plus a mode
|
|
52
|
+
* flag, so the two modes cannot disagree about what they are rendering.
|
|
53
|
+
*/
|
|
54
|
+
mode: "sections" | "screens";
|
|
55
|
+
sections: DashboardHubSection[];
|
|
56
|
+
/** Built by the caller from `ROUTES.{PORTAL}.SECTION(slug)`. */
|
|
57
|
+
sectionHrefs: Record<string, string>;
|
|
58
|
+
counts: AttentionCounts;
|
|
59
|
+
/**
|
|
60
|
+
* Rendered above the cards — the live stat tiles on `/store` and `/user`.
|
|
61
|
+
*
|
|
62
|
+
* Those are real data and stay: the plan's "bland tile walls" are the
|
|
63
|
+
* undifferentiated grids of ~19 nav shortcuts, not the figures beside them.
|
|
64
|
+
* Deleting a working panel while replacing its neighbour is Root Cause #60's
|
|
65
|
+
* shape, so the slot exists and the caller keeps passing them.
|
|
66
|
+
*/
|
|
67
|
+
above?: React.ReactNode;
|
|
68
|
+
/** Shown when the viewer can see nothing at all. */
|
|
69
|
+
emptyMessage?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Render "7 screens" on a section card. Default `true`.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 Pass `false` whenever `sections` has NOT been through every filter that
|
|
74
|
+
* applies to the portal. `/store` is the live case: its entitlement filter
|
|
75
|
+
* (`filterStoreNavGroups`) runs in the layout and on the hub pages, but the
|
|
76
|
+
* client landing does not hold the store document, so its "Listings" card
|
|
77
|
+
* would claim nine screens where the seller can reach six. The section itself
|
|
78
|
+
* is still correct — it is the COUNT that would over-promise, so the count is
|
|
79
|
+
* withheld rather than guessed.
|
|
80
|
+
*/
|
|
81
|
+
showScreenCount?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function DashboardHubView({ title, description, mode, sections, sectionHrefs, counts, above, emptyMessage, showScreenCount, }: DashboardHubViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Badge, Div, Grid, Heading, Row, Section, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sum of the counts inside a section, or `null` when it has none.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 `null` and `0` are different answers and a card renders only the first as
|
|
7
|
+
* "no badge". A section whose every count FAILED must not read as "nothing
|
|
8
|
+
* waiting" (Root Cause #72), so a failed read inside an otherwise-countless
|
|
9
|
+
* section stays `null` rather than collapsing to a sum of zero.
|
|
10
|
+
*/
|
|
11
|
+
function sectionTotal(section, counts) {
|
|
12
|
+
let total = null;
|
|
13
|
+
for (const item of section.items) {
|
|
14
|
+
const value = item.id ? counts[item.id] : undefined;
|
|
15
|
+
if (typeof value !== "number")
|
|
16
|
+
continue;
|
|
17
|
+
total = (total ?? 0) + value;
|
|
18
|
+
}
|
|
19
|
+
return total;
|
|
20
|
+
}
|
|
21
|
+
/** A count worth showing. Zero is not — a `0` badge is noise on every card. */
|
|
22
|
+
function badgeFor(value) {
|
|
23
|
+
if (typeof value !== "number" || value <= 0)
|
|
24
|
+
return null;
|
|
25
|
+
return (_jsx(Badge, { variant: "warning", "aria-label": `${value} waiting`, children: value }));
|
|
26
|
+
}
|
|
27
|
+
function CardGrid({ cards }) {
|
|
28
|
+
return (_jsx(Grid, { cols: "cards", gap: "md", children: cards.map((card) => (_jsxs(TextLink, { variant: "bare", href: card.href, rounded: "xl", layout: "flex-col", gap: "sm", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0 w-full", children: [_jsx(Text, { weight: "semibold", className: "min-w-0 text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", children: card.label }), badgeFor(card.count)] }), card.description && (_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-muted)] leading-relaxed", children: card.description })), card.meta && (_jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-faint)] mt-auto", children: card.meta }))] }, card.key))) }));
|
|
29
|
+
}
|
|
30
|
+
export function DashboardHubView({ title, description, mode, sections, sectionHrefs, counts, above, emptyMessage, showScreenCount = true, }) {
|
|
31
|
+
const hasAny = sections.some((section) => section.items.length > 0);
|
|
32
|
+
return (_jsxs(Stack, { gap: "lg", children: [title && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, size: "xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] })), above, !hasAny && (_jsx(Alert, { variant: "info", title: "Nothing here yet", children: emptyMessage ??
|
|
33
|
+
"Your permission set doesn't include access to any of these screens yet. Ask an admin for the permissions you need." })), hasAny && mode === "sections" && (_jsx(CardGrid, { cards: sections.map((section) => ({
|
|
34
|
+
key: section.slug,
|
|
35
|
+
href: sectionHrefs[section.slug] ?? "",
|
|
36
|
+
label: section.title,
|
|
37
|
+
/*
|
|
38
|
+
* A section has no description of its own and must not invent one —
|
|
39
|
+
* so it names what is inside it instead. `items.length` is the
|
|
40
|
+
* post-filter count, which is what this viewer will actually find
|
|
41
|
+
* PROVIDED every filter has run; see `showScreenCount`.
|
|
42
|
+
*/
|
|
43
|
+
meta: showScreenCount
|
|
44
|
+
? `${section.items.length} ${section.items.length === 1 ? "screen" : "screens"}`
|
|
45
|
+
: undefined,
|
|
46
|
+
count: sectionTotal(section, counts),
|
|
47
|
+
})) })), hasAny &&
|
|
48
|
+
mode === "screens" &&
|
|
49
|
+
sections.map((section) => (_jsxs(Section, { children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", className: "mb-4 text-[var(--appkit-color-text)]", children: section.title }), _jsx(CardGrid, { cards: section.items.map((item) => ({
|
|
50
|
+
key: item.id ?? item.href,
|
|
51
|
+
href: item.href,
|
|
52
|
+
label: item.label,
|
|
53
|
+
description: item.description,
|
|
54
|
+
count: item.id ? (counts[item.id] ?? null) : null,
|
|
55
|
+
})) })] }, section.slug))), hasAny && mode === "sections" && (_jsx(Div, { children: _jsx(Text, { size: "xs", color: "faint", children: "Every card above is a section of the sidebar. The screens inside one are the same screens, with a sentence each on what they are for." }) }))] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { NavPortal, SidebarNavGroup } from "../../../_internal/shared/features/layout/types";
|
|
3
|
+
export interface DashboardSectionsClientProps {
|
|
4
|
+
portal: NavPortal;
|
|
5
|
+
/**
|
|
6
|
+
* The portal's nav groups.
|
|
7
|
+
*
|
|
8
|
+
* 🛑 Pass the groups the SIDEBAR was given, already put through any
|
|
9
|
+
* portal-specific filter (`filterStoreNavGroups` for `/store`). This applies
|
|
10
|
+
* only the navConfig/permission filter; a section the store is not entitled
|
|
11
|
+
* to would otherwise appear here and nowhere else.
|
|
12
|
+
*/
|
|
13
|
+
groups: readonly SidebarNavGroup[];
|
|
14
|
+
/** `null` means admin; `undefined` means "no permission gating applies". */
|
|
15
|
+
permissions?: string[] | null;
|
|
16
|
+
/** Omit when embedding in a page that already has a heading. */
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Live stat tiles and anything else that belongs above the cards. */
|
|
20
|
+
above?: React.ReactNode;
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function DashboardSectionsClient({ portal, groups, permissions, title, description, above, emptyMessage, }: DashboardSectionsClientProps): React.JSX.Element;
|