@mohasinac/appkit 4.42.18 → 4.44.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/_internal/client/features/lottery/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StackedViewShellProps } from "../../../ui";
|
|
3
|
+
export interface AdminTaxCodeEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
4
|
+
taxCodeId?: string;
|
|
5
|
+
/** When true, omit the StackedViewShell wrapper (SideDrawer body). */
|
|
6
|
+
embedded?: boolean;
|
|
7
|
+
onSaved?: (id: string) => void;
|
|
8
|
+
onDeleted?: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }: AdminTaxCodeEditorViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { ConfirmDeleteModal, Div, StackedViewShell, useToast } from "../../../ui";
|
|
7
|
+
import { apiClient } from "../../../http";
|
|
8
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
|
|
10
|
+
import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell";
|
|
11
|
+
import { buildSectionsFromSchema, visibleValues } from "../../shell/build-sections";
|
|
12
|
+
import { SectionForm, useSectionFormNav } from "../../shell/SectionForm";
|
|
13
|
+
import { taxCodeFormSchema, GST_RATE_OPTIONS, HSN_CHAPTER_OPTIONS, } from "../../tax-codes/schemas/tax-code-form";
|
|
14
|
+
const __P = { p4: "p-[var(--appkit-space-4)]" };
|
|
15
|
+
const __O = { yAuto: "overflow-y-auto" };
|
|
16
|
+
/**
|
|
17
|
+
* 🛑 The delete copy says what the server will actually do.
|
|
18
|
+
*
|
|
19
|
+
* `DELETE /api/admin/tax-codes/[id]` REFUSES with a 409 while any category
|
|
20
|
+
* still points here, and names the referrers. Promising an unconditional
|
|
21
|
+
* delete and then showing a 409 is how a confirmation dialog loses its
|
|
22
|
+
* meaning — and the honest alternative (deactivate) is the one an admin
|
|
23
|
+
* usually wants anyway, since an inactive code is already skipped by
|
|
24
|
+
* `findResolvable` and gone from every picker.
|
|
25
|
+
*/
|
|
26
|
+
const DELETE_CONFIRM_TEXT = "Delete this tax code? This is refused while any category still points at it — " +
|
|
27
|
+
"set it inactive instead to retire it without breaking those categories.";
|
|
28
|
+
const TOAST = {
|
|
29
|
+
CREATED: "Tax code created.",
|
|
30
|
+
UPDATED: "Tax code updated.",
|
|
31
|
+
DELETED: "Tax code deleted.",
|
|
32
|
+
};
|
|
33
|
+
export function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }) {
|
|
34
|
+
const isEdit = Boolean(taxCodeId);
|
|
35
|
+
const { showToast } = useToast();
|
|
36
|
+
const [form, setForm] = React.useState({
|
|
37
|
+
label: "",
|
|
38
|
+
hsnCode: "",
|
|
39
|
+
// The default rate is the one 70 of this catalogue's products need.
|
|
40
|
+
gstRate: "5",
|
|
41
|
+
chapter: "9503",
|
|
42
|
+
description: "",
|
|
43
|
+
notes: "",
|
|
44
|
+
isActive: true,
|
|
45
|
+
});
|
|
46
|
+
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
|
|
47
|
+
const patch = (partial) => setForm((prev) => Object.assign({}, prev, partial));
|
|
48
|
+
const codeQuery = useQuery({
|
|
49
|
+
queryKey: ["admin", "tax-code", taxCodeId],
|
|
50
|
+
queryFn: async () => {
|
|
51
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId));
|
|
52
|
+
return res?.data ?? res;
|
|
53
|
+
},
|
|
54
|
+
enabled: isEdit,
|
|
55
|
+
});
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const t = codeQuery.data;
|
|
58
|
+
if (!t)
|
|
59
|
+
return;
|
|
60
|
+
patch({
|
|
61
|
+
label: t.label ?? "",
|
|
62
|
+
hsnCode: t.hsnCode ?? "",
|
|
63
|
+
/*
|
|
64
|
+
* `?? 5` would be wrong here and the distinction is the whole point of
|
|
65
|
+
* this field: rate 0 is a DELIBERATE exemption, and `t.gstRate ?? 5`
|
|
66
|
+
* would silently re-rate an exempt row to 5% the moment an admin opened
|
|
67
|
+
* it and saved anything. Test for null, not falsiness.
|
|
68
|
+
*/
|
|
69
|
+
gstRate: t.gstRate == null ? "5" : String(t.gstRate),
|
|
70
|
+
chapter: t.chapter ?? "",
|
|
71
|
+
description: t.description ?? "",
|
|
72
|
+
notes: t.notes ?? "",
|
|
73
|
+
isActive: t.isActive ?? true,
|
|
74
|
+
});
|
|
75
|
+
}, [codeQuery.data]);
|
|
76
|
+
const saveMutation = useApiMutation({
|
|
77
|
+
errorMessage: "Failed to save tax code.",
|
|
78
|
+
mutationFn: async () => {
|
|
79
|
+
const payload = {
|
|
80
|
+
label: form.label,
|
|
81
|
+
hsnCode: form.hsnCode.trim() || undefined,
|
|
82
|
+
gstRate: Number(form.gstRate),
|
|
83
|
+
chapter: form.chapter || undefined,
|
|
84
|
+
description: form.description || undefined,
|
|
85
|
+
notes: form.notes || undefined,
|
|
86
|
+
isActive: form.isActive,
|
|
87
|
+
};
|
|
88
|
+
/*
|
|
89
|
+
* PATCH, not PUT — the route is `.strict()` and partial by design, so a
|
|
90
|
+
* PUT would 404. (`AdminFeatureEditorView` uses PUT because ITS route
|
|
91
|
+
* declares PUT; copying the verb rather than reading the route is how
|
|
92
|
+
* that kind of mismatch ships.)
|
|
93
|
+
*/
|
|
94
|
+
if (isEdit) {
|
|
95
|
+
return apiClient.patch(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId), payload);
|
|
96
|
+
}
|
|
97
|
+
return apiClient.post(ADMIN_ENDPOINTS.TAX_CODES, payload);
|
|
98
|
+
},
|
|
99
|
+
onSuccess: (res) => {
|
|
100
|
+
const id = res?.data?.id ??
|
|
101
|
+
res?.id ??
|
|
102
|
+
taxCodeId;
|
|
103
|
+
showToast(isEdit ? TOAST.UPDATED : TOAST.CREATED, "success");
|
|
104
|
+
if (onSaved && id)
|
|
105
|
+
onSaved(String(id));
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const deleteMutation = useApiMutation({
|
|
109
|
+
errorMessage: "Failed to delete tax code.",
|
|
110
|
+
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId)),
|
|
111
|
+
onSuccess: () => {
|
|
112
|
+
showToast(TOAST.DELETED, "success");
|
|
113
|
+
if (onDeleted)
|
|
114
|
+
onDeleted();
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const isSubmitting = saveMutation.isPending || codeQuery.isLoading;
|
|
118
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(taxCodeFormSchema, {
|
|
119
|
+
// `FieldUiMeta` has no `options` key — a select's VALUES are schema and
|
|
120
|
+
// its option LIST is supplied here. Same split as the bundle editor.
|
|
121
|
+
options: {
|
|
122
|
+
gstRate: [...GST_RATE_OPTIONS],
|
|
123
|
+
chapter: [{ value: "", label: "No chapter" }, ...HSN_CHAPTER_OPTIONS],
|
|
124
|
+
},
|
|
125
|
+
}), []);
|
|
126
|
+
const nav = useSectionFormNav(sections, form, { scope: "admin:tax-code-editor" });
|
|
127
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState(taxCodeFormSchema, {
|
|
128
|
+
sections: nav.sectionMeta,
|
|
129
|
+
onGoToSection: nav.goToSection,
|
|
130
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
131
|
+
});
|
|
132
|
+
const onSubmit = () => {
|
|
133
|
+
clearErrors();
|
|
134
|
+
const draft = visibleValues(taxCodeFormSchema, form);
|
|
135
|
+
const parsed = taxCodeFormSchema.safeParse({
|
|
136
|
+
...draft,
|
|
137
|
+
hsnCode: form.hsnCode.trim() || undefined,
|
|
138
|
+
gstRate: Number(form.gstRate),
|
|
139
|
+
chapter: form.chapter || undefined,
|
|
140
|
+
description: form.description || undefined,
|
|
141
|
+
notes: form.notes || undefined,
|
|
142
|
+
});
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
saveMutation.mutate();
|
|
148
|
+
};
|
|
149
|
+
const formSection = (_jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: form, onChange: patch, onSubmit: onSubmit, schema: taxCodeFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isSubmitting, submitLabel: isEdit ? "Save changes" : "Create tax code", destructiveAction: isEdit
|
|
150
|
+
? { label: "Delete", onClick: () => setDeleteConfirmOpen(true) }
|
|
151
|
+
: undefined })] }));
|
|
152
|
+
const deleteModal = deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Tax Code", message: DELETE_CONFIRM_TEXT, onConfirm: () => {
|
|
153
|
+
deleteMutation.mutate();
|
|
154
|
+
setDeleteConfirmOpen(false);
|
|
155
|
+
}, onClose: () => setDeleteConfirmOpen(false), isDeleting: deleteMutation.isPending }));
|
|
156
|
+
if (embedded) {
|
|
157
|
+
return (_jsxs(Div, { className: `${__O.yAuto} ${__P.p4}`, children: [formSection, deleteModal] }));
|
|
158
|
+
}
|
|
159
|
+
return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Tax Code" : "New Tax Code", sections: [formSection] }), deleteModal] }));
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ListingLayoutProps } from "../../../ui";
|
|
3
|
+
export type AdminTaxCodesViewProps = ListingLayoutProps;
|
|
4
|
+
/**
|
|
5
|
+
* `/admin/tax-codes` — the HSN + GST rate catalogue.
|
|
6
|
+
*
|
|
7
|
+
* 🛑 No bulk actions, deliberately. The two that would be offered by habit are
|
|
8
|
+
* both wrong here: a bulk DELETE would hit the referential guard row by row
|
|
9
|
+
* and report a partial failure an admin cannot act on, and a bulk
|
|
10
|
+
* activate/deactivate across a mixed selection is a rate change applied to
|
|
11
|
+
* things nobody looked at. There are ~10 rows; each one is a per-row decision.
|
|
12
|
+
*/
|
|
13
|
+
export declare function AdminTaxCodesView({ children, ...props }: AdminTaxCodesViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { sortBy } from "@mohasinac/appkit/client";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { ListingLayout } from "../../../ui";
|
|
6
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
|
|
8
|
+
import { DataListingView } from "./DataListingView";
|
|
9
|
+
import { AdminTaxCodeEditorView } from "./AdminTaxCodeEditorView";
|
|
10
|
+
const PAGE_SIZE = 50;
|
|
11
|
+
function mapTaxCodeRow(item, index) {
|
|
12
|
+
const isActive = typeof item.isActive === "boolean" ? item.isActive : true;
|
|
13
|
+
const rate = typeof item.gstRate === "number" ? item.gstRate : null;
|
|
14
|
+
const hsn = toStringValue(item.hsnCode, "");
|
|
15
|
+
return {
|
|
16
|
+
id: toStringValue(item.id, `tax-${index}`),
|
|
17
|
+
primary: toStringValue(item.label, "Untitled tax code"),
|
|
18
|
+
/*
|
|
19
|
+
* The RATE leads, because it is the number an admin is checking. `0%` is
|
|
20
|
+
* rendered explicitly rather than falling back to an em-dash: a blank
|
|
21
|
+
* would read as "not set", and 0 is a deliberate exemption — the same
|
|
22
|
+
* `undefined`-vs-`0` distinction the field itself depends on. A rate that
|
|
23
|
+
* genuinely has no value says so.
|
|
24
|
+
*/
|
|
25
|
+
secondary: [
|
|
26
|
+
rate == null ? "rate not set" : `${rate}% GST`,
|
|
27
|
+
hsn ? `HSN ${hsn}` : "no HSN (exempt)",
|
|
28
|
+
toStringValue(item.chapter, "") ? `chapter ${item.chapter}` : "",
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" — "),
|
|
32
|
+
status: isActive ? "Active" : "Inactive",
|
|
33
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `/admin/tax-codes` — the HSN + GST rate catalogue.
|
|
38
|
+
*
|
|
39
|
+
* 🛑 No bulk actions, deliberately. The two that would be offered by habit are
|
|
40
|
+
* both wrong here: a bulk DELETE would hit the referential guard row by row
|
|
41
|
+
* and report a partial failure an admin cannot act on, and a bulk
|
|
42
|
+
* activate/deactivate across a mixed selection is a rate change applied to
|
|
43
|
+
* things nobody looked at. There are ~10 rows; each one is a per-row decision.
|
|
44
|
+
*/
|
|
45
|
+
export function AdminTaxCodesView({ children, ...props }) {
|
|
46
|
+
if (React.Children.count(children) > 0) {
|
|
47
|
+
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
48
|
+
}
|
|
49
|
+
const config = {
|
|
50
|
+
portal: "admin",
|
|
51
|
+
title: "Tax Codes",
|
|
52
|
+
// No search box: this endpoint does not read `q`, and a box that accepts
|
|
53
|
+
// typing and changes nothing is worse than no box
|
|
54
|
+
// (audit-listing-search-capability's rule). ~10 rows do not need one.
|
|
55
|
+
emptyLabel: "No tax codes yet",
|
|
56
|
+
filterKeys: [],
|
|
57
|
+
defaultSort: sortBy("label", "ASC"),
|
|
58
|
+
pageSize: PAGE_SIZE,
|
|
59
|
+
queryKey: ["admin", "tax-codes", "listing"],
|
|
60
|
+
/*
|
|
61
|
+
* `?all=true` — the admin list MUST show inactive rows, because
|
|
62
|
+
* deactivating is how a code is retired and reactivating has to be
|
|
63
|
+
* possible. The route's default is active-only because its other caller is
|
|
64
|
+
* the category editor's picker, where offering an inactive code would let
|
|
65
|
+
* an admin point a category at something `findResolvable` then refuses.
|
|
66
|
+
*/
|
|
67
|
+
endpoint: `${ADMIN_ENDPOINTS.TAX_CODES}?all=true`,
|
|
68
|
+
sortOptions: [
|
|
69
|
+
{ value: "label", label: "Label A–Z" },
|
|
70
|
+
{ value: sortBy("label", "DESC"), label: "Label Z–A" },
|
|
71
|
+
{ value: "gstRate", label: "Rate, low to high" },
|
|
72
|
+
{ value: sortBy("gstRate", "DESC"), label: "Rate, high to low" },
|
|
73
|
+
{ value: "hsnCode", label: "HSN code" },
|
|
74
|
+
],
|
|
75
|
+
mapRows: (response) => toRecordArray(response.items).map(mapTaxCodeRow),
|
|
76
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
77
|
+
buildFilters: () => undefined,
|
|
78
|
+
primaryAction: {
|
|
79
|
+
label: "Add Tax Code",
|
|
80
|
+
onClick: ({ openCreatePanel }) => openCreatePanel(),
|
|
81
|
+
},
|
|
82
|
+
renderEditor: ({ editId, closePanel }) => (_jsx(AdminTaxCodeEditorView, { taxCodeId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })),
|
|
83
|
+
resolveEditorTitle: ({ isCreate }) => isCreate ? "Add Tax Code" : "Edit Tax Code",
|
|
84
|
+
};
|
|
85
|
+
return _jsx(DataListingView, { config: config });
|
|
86
|
+
}
|
|
@@ -6,7 +6,7 @@ import React, { useMemo } from "react";
|
|
|
6
6
|
import { useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { FilterChipGroup, Heading, ListingLayout, RowActionMenu, Span, Text } from "../../../ui";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
9
|
-
import { useApiMutation } from "../../../client";
|
|
9
|
+
import { useApiMutation } from "../../../client/api/useApiMutation";
|
|
10
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
12
12
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Div, Heading, Row, Text } from "../../../../ui";
|
|
3
3
|
export function AdminTopProductsTable({ products, labels = {}, formatRevenue = (n) => String(n), renderProductLink, className = "", }) {
|
|
4
|
-
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }),
|
|
4
|
+
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
|
|
5
5
|
}
|
|
@@ -6,6 +6,7 @@ import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
|
6
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
7
7
|
import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
|
|
8
8
|
import { useCountdown } from "../../../react";
|
|
9
|
+
import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
|
|
9
10
|
import { Modal } from "../../../ui";
|
|
10
11
|
import { isBuyNowAvailable } from "../../../_internal/shared/features/auctions/config";
|
|
11
12
|
import { PlaceBidFormClient, } from "./PlaceBidFormClient";
|
|
@@ -38,7 +39,15 @@ function formatCountdownLabel(remaining) {
|
|
|
38
39
|
* button can report an error or route to `/checkout?lane=auction`.
|
|
39
40
|
*/
|
|
40
41
|
export function AuctionBottomActions(props) {
|
|
41
|
-
const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
|
|
42
|
+
const { currentBid, currency, bidCount, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice } = props;
|
|
43
|
+
/*
|
|
44
|
+
* Derived client-side — see `useIsAuctionEnded`. This bar is a FIXED, always-
|
|
45
|
+
* visible row on mobile, so a stale "Place bid" here is the most prominent
|
|
46
|
+
* wrong affordance on the page. `useCountdown` below already knew the truth
|
|
47
|
+
* (it returns null past the target) but only fed a label; the bar's actions
|
|
48
|
+
* were gated on the frozen server boolean.
|
|
49
|
+
*/
|
|
50
|
+
const isEnded = useIsAuctionEnded(auctionEndDate, ssrIsEnded);
|
|
42
51
|
const [open, setOpen] = useState(false);
|
|
43
52
|
const remaining = useCountdown(auctionEndDate ?? undefined);
|
|
44
53
|
const { canSeePrices } = useCanSeePrices();
|
|
@@ -22,6 +22,7 @@ import { AuctionDetailView } from "../../products/components/AuctionDetailView";
|
|
|
22
22
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
23
23
|
import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
|
|
24
24
|
import { AuctionBottomActions } from "./AuctionBottomActions";
|
|
25
|
+
import { AuctionLiveStatusBadge } from "./AuctionLiveStatusBadge";
|
|
25
26
|
import { RelatedProducts } from "../../products/components/RelatedProducts";
|
|
26
27
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
27
28
|
import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges";
|
|
@@ -48,7 +49,7 @@ function toDescriptionHtml(raw) {
|
|
|
48
49
|
}
|
|
49
50
|
function renderAuctionInfoPanel(props) {
|
|
50
51
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
51
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }),
|
|
52
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), _jsx(AuctionLiveStatusBadge, { auctionEndDate: endDate, initialIsEnded: isEnded })] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: _jsx(GatedPrice, { label: "Sign in to see the Buy Now price", children: formatCurrency(buyNowPrice, currency) }) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
55
|
* Store reviews for the auction's seller.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AuctionLiveStatusBadgeProps {
|
|
2
|
+
/** The auction's end timestamp — the only thing that decides this. */
|
|
3
|
+
auctionEndDate: Date | string | null;
|
|
4
|
+
/** The server's answer, used as the initial value so SSR and hydration agree. */
|
|
5
|
+
initialIsEnded: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Active / Ended, decided on the client.
|
|
9
|
+
*
|
|
10
|
+
* The server-rendered version of this badge sat directly above a countdown that
|
|
11
|
+
* already flipped itself to "Ended" — so a cached auction page could render
|
|
12
|
+
* `Ends in Ended` beside a green **Active** pill. The countdown was right and the
|
|
13
|
+
* badge was stale, in the same `<Stack>`.
|
|
14
|
+
*
|
|
15
|
+
* Extracted to a client component rather than passing a derived prop down,
|
|
16
|
+
* because `AuctionDetailPageView` is a Server Component and cannot hold the
|
|
17
|
+
* timer. Colours follow the status-pair rule (Root Cause #67): a theme-relative
|
|
18
|
+
* surface with its matching theme-relative ink, never a literal `text-white`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function AuctionLiveStatusBadge({ auctionEndDate, initialIsEnded, }: AuctionLiveStatusBadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Span } from "../../../ui";
|
|
4
|
+
import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
|
|
5
|
+
/**
|
|
6
|
+
* Active / Ended, decided on the client.
|
|
7
|
+
*
|
|
8
|
+
* The server-rendered version of this badge sat directly above a countdown that
|
|
9
|
+
* already flipped itself to "Ended" — so a cached auction page could render
|
|
10
|
+
* `Ends in Ended` beside a green **Active** pill. The countdown was right and the
|
|
11
|
+
* badge was stale, in the same `<Stack>`.
|
|
12
|
+
*
|
|
13
|
+
* Extracted to a client component rather than passing a derived prop down,
|
|
14
|
+
* because `AuctionDetailPageView` is a Server Component and cannot hold the
|
|
15
|
+
* timer. Colours follow the status-pair rule (Root Cause #67): a theme-relative
|
|
16
|
+
* surface with its matching theme-relative ink, never a literal `text-white`.
|
|
17
|
+
*/
|
|
18
|
+
export function AuctionLiveStatusBadge({ auctionEndDate, initialIsEnded, }) {
|
|
19
|
+
const isEnded = useIsAuctionEnded(auctionEndDate, initialIsEnded);
|
|
20
|
+
return isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }));
|
|
21
|
+
}
|
|
@@ -45,7 +45,7 @@ export interface PlaceBidFormClientProps {
|
|
|
45
45
|
onPlaceBid: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
|
|
46
46
|
onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
|
|
47
47
|
}
|
|
48
|
-
export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
|
|
48
|
+
export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
|
|
49
49
|
/**
|
|
50
50
|
* `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
|
|
51
51
|
*
|
|
@@ -13,6 +13,7 @@ import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
|
13
13
|
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
14
14
|
import { placeBidSchema } from "../schemas/bid-input";
|
|
15
15
|
import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
|
|
16
|
+
import { useIsAuctionEnded } from "../hooks/useIsAuctionEnded";
|
|
16
17
|
import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkout/lanes";
|
|
17
18
|
import { BID_PRESET_MULTIPLIERS, isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
|
|
18
19
|
import { normalizeError } from "../../../errors/normalize";
|
|
@@ -36,7 +37,20 @@ const __P = {
|
|
|
36
37
|
/** Feature-specific last resort. Authored copy, never derived from an error. */
|
|
37
38
|
const BID_FALLBACK = "Failed to place bid. Please try again.";
|
|
38
39
|
const BUY_NOW_FALLBACK = "Buy Now failed. Please try again.";
|
|
39
|
-
export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
40
|
+
export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded: ssrIsEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
41
|
+
/*
|
|
42
|
+
* 🛑 Derived here, not trusted from the server.
|
|
43
|
+
*
|
|
44
|
+
* `isEnded` arrives baked into cached HTML. On a page cached while the auction
|
|
45
|
+
* was live it says "still open" indefinitely, so every gate below it — the
|
|
46
|
+
* amount field, the presets, the submit button, Buy Now — stayed enabled on a
|
|
47
|
+
* closed lot. The server prop is the INITIAL value only; `auctionEndDate` was
|
|
48
|
+
* already being passed in and used for display, and it is the real answer.
|
|
49
|
+
*
|
|
50
|
+
* The bid itself was never at risk: `placeBid` re-reads the product and
|
|
51
|
+
* rejects on an expired `auctionEndDate`. This fixes the affordance.
|
|
52
|
+
*/
|
|
53
|
+
const isEnded = useIsAuctionEnded(auctionEndDate, ssrIsEnded);
|
|
40
54
|
const router = useRouter();
|
|
41
55
|
// Declared with the other hooks, never inside the early return below — a
|
|
42
56
|
// conditional hook is a render-order violation, and this component early-
|
|
@@ -277,5 +291,9 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
277
291
|
export function PlaceBidModalButton(props) {
|
|
278
292
|
const { triggerLabel = "Place a bid", triggerClassName = "", ...formProps } = props;
|
|
279
293
|
const [open, setOpen] = useState(false);
|
|
280
|
-
|
|
294
|
+
// Same reason as inside the form: this trigger is THE affordance a buyer sees
|
|
295
|
+
// on a cached auction page, so it must decide for itself rather than trust a
|
|
296
|
+
// boolean that was true whenever the HTML happened to be generated.
|
|
297
|
+
const isEnded = useIsAuctionEnded(props.auctionEndDate, props.isEnded);
|
|
298
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: triggerClassName, disabled: isEnded, onClick: () => setOpen(true), children: isEnded ? "Auction Ended" : triggerLabel }), _jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...formProps }) })] }));
|
|
281
299
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Has this auction ended, decided on the CLIENT from its end timestamp.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `AuctionDetailPageView` computed `const isEnded = endDate < new Date()` on the
|
|
7
|
+
* SERVER and froze the answer into eleven places — the status badge, the Buy-Now
|
|
8
|
+
* chip, the SSE `enabled` flag, the Place-Bid button, the mobile bid card and
|
|
9
|
+
* the bottom action bar. That is correct only for as long as the rendered HTML
|
|
10
|
+
* is fresh.
|
|
11
|
+
*
|
|
12
|
+
* The page is cached. At `revalidate = 30` the window was small enough to hide
|
|
13
|
+
* the problem; it is not small enough to be *correct*, and raising the TTL (so
|
|
14
|
+
* that 95 auction pages stop regenerating up to 2,880 times a day each, which is
|
|
15
|
+
* what produced 3.1M ISR writes) makes it plainly visible: a page cached while
|
|
16
|
+
* the auction was live renders a green **Active** badge and an enabled "Place a
|
|
17
|
+
* bid" button on a lot that closed hours ago.
|
|
18
|
+
*
|
|
19
|
+
* The timestamp needed to know better was already being passed down — it was
|
|
20
|
+
* used for display only.
|
|
21
|
+
*
|
|
22
|
+
* ## What this does NOT change
|
|
23
|
+
*
|
|
24
|
+
* Server-side enforcement is independent and already correct: `placeBid` and
|
|
25
|
+
* `buyNowAuction` both re-read the product and reject on `endDate < now`, and
|
|
26
|
+
* `assertAuctionActive()` is a third copy. A stale page could never actually
|
|
27
|
+
* place a bid — the defect was always a misleading affordance, never a bad
|
|
28
|
+
* write. That is what makes this safe to adopt incrementally: every component
|
|
29
|
+
* converted is an improvement, and an unconverted one is no worse than today.
|
|
30
|
+
*
|
|
31
|
+
* ## Behaviour
|
|
32
|
+
*
|
|
33
|
+
* - Starts from `initial` (the server's answer) so the first paint is unchanged
|
|
34
|
+
* and hydration does not mismatch.
|
|
35
|
+
* - Re-checks on mount, which is where a cached page corrects itself.
|
|
36
|
+
* - Schedules ONE timer for the exact moment of expiry rather than ticking every
|
|
37
|
+
* second — a bid button does not need per-second resolution, and an interval
|
|
38
|
+
* on every auction card would be a needless wakeup. Long waits are clamped to
|
|
39
|
+
* the setTimeout 32-bit ceiling and re-armed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useIsAuctionEnded(auctionEndDate: Date | string | null | undefined, initial?: boolean): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Has this auction ended, decided on the CLIENT from its end timestamp.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* `AuctionDetailPageView` computed `const isEnded = endDate < new Date()` on the
|
|
9
|
+
* SERVER and froze the answer into eleven places — the status badge, the Buy-Now
|
|
10
|
+
* chip, the SSE `enabled` flag, the Place-Bid button, the mobile bid card and
|
|
11
|
+
* the bottom action bar. That is correct only for as long as the rendered HTML
|
|
12
|
+
* is fresh.
|
|
13
|
+
*
|
|
14
|
+
* The page is cached. At `revalidate = 30` the window was small enough to hide
|
|
15
|
+
* the problem; it is not small enough to be *correct*, and raising the TTL (so
|
|
16
|
+
* that 95 auction pages stop regenerating up to 2,880 times a day each, which is
|
|
17
|
+
* what produced 3.1M ISR writes) makes it plainly visible: a page cached while
|
|
18
|
+
* the auction was live renders a green **Active** badge and an enabled "Place a
|
|
19
|
+
* bid" button on a lot that closed hours ago.
|
|
20
|
+
*
|
|
21
|
+
* The timestamp needed to know better was already being passed down — it was
|
|
22
|
+
* used for display only.
|
|
23
|
+
*
|
|
24
|
+
* ## What this does NOT change
|
|
25
|
+
*
|
|
26
|
+
* Server-side enforcement is independent and already correct: `placeBid` and
|
|
27
|
+
* `buyNowAuction` both re-read the product and reject on `endDate < now`, and
|
|
28
|
+
* `assertAuctionActive()` is a third copy. A stale page could never actually
|
|
29
|
+
* place a bid — the defect was always a misleading affordance, never a bad
|
|
30
|
+
* write. That is what makes this safe to adopt incrementally: every component
|
|
31
|
+
* converted is an improvement, and an unconverted one is no worse than today.
|
|
32
|
+
*
|
|
33
|
+
* ## Behaviour
|
|
34
|
+
*
|
|
35
|
+
* - Starts from `initial` (the server's answer) so the first paint is unchanged
|
|
36
|
+
* and hydration does not mismatch.
|
|
37
|
+
* - Re-checks on mount, which is where a cached page corrects itself.
|
|
38
|
+
* - Schedules ONE timer for the exact moment of expiry rather than ticking every
|
|
39
|
+
* second — a bid button does not need per-second resolution, and an interval
|
|
40
|
+
* on every auction card would be a needless wakeup. Long waits are clamped to
|
|
41
|
+
* the setTimeout 32-bit ceiling and re-armed.
|
|
42
|
+
*/
|
|
43
|
+
export function useIsAuctionEnded(auctionEndDate, initial = false) {
|
|
44
|
+
const endMs = toMs(auctionEndDate);
|
|
45
|
+
const [ended, setEnded] = useState(initial);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (endMs === null) {
|
|
48
|
+
// No end date — the server's answer is the only one available.
|
|
49
|
+
setEnded(initial);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let timer;
|
|
53
|
+
const evaluate = () => {
|
|
54
|
+
const remaining = endMs - Date.now();
|
|
55
|
+
if (remaining <= 0) {
|
|
56
|
+
setEnded(true);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setEnded(false);
|
|
60
|
+
// setTimeout overflows past ~24.8 days and fires immediately; re-arm.
|
|
61
|
+
timer = setTimeout(evaluate, Math.min(remaining, 2147483000));
|
|
62
|
+
};
|
|
63
|
+
evaluate();
|
|
64
|
+
return () => {
|
|
65
|
+
if (timer)
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
};
|
|
68
|
+
}, [endMs, initial]);
|
|
69
|
+
return ended;
|
|
70
|
+
}
|
|
71
|
+
function toMs(value) {
|
|
72
|
+
if (!value)
|
|
73
|
+
return null;
|
|
74
|
+
const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
75
|
+
return Number.isFinite(ms) ? ms : null;
|
|
76
|
+
}
|
|
@@ -72,7 +72,22 @@ export async function GET(request) {
|
|
|
72
72
|
posts: result.items,
|
|
73
73
|
meta,
|
|
74
74
|
};
|
|
75
|
-
|
|
75
|
+
const response = NextResponse.json({ success: true, data: body });
|
|
76
|
+
/*
|
|
77
|
+
* The only public listing API that set NO Cache-Control — `/api/products`,
|
|
78
|
+
* `/api/stores`, `/api/events` and `/api/reviews` all do, and CLAUDE.md
|
|
79
|
+
* Rule #6.5 requires it of every public GET (20 of 378 routes complied).
|
|
80
|
+
*
|
|
81
|
+
* It mattered little while anonymous browsing never reached this route (the
|
|
82
|
+
* dynamic `/blog` page rendered server-side and seeded the client), but it
|
|
83
|
+
* is on the critical path the moment a filtered view misses the page cache.
|
|
84
|
+
* Window matched to its siblings deliberately — a blog post is no more
|
|
85
|
+
* volatile than a product.
|
|
86
|
+
*
|
|
87
|
+
* Errors below stay uncached: a 500 must not be pinned at the edge.
|
|
88
|
+
*/
|
|
89
|
+
response.headers.set("Cache-Control", "public, max-age=60, s-maxage=120, stale-while-revalidate=60");
|
|
90
|
+
return response;
|
|
76
91
|
}
|
|
77
92
|
catch (error) {
|
|
78
93
|
void normalizeError(error);
|
|
@@ -16,9 +16,9 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
16
16
|
title: string;
|
|
17
17
|
slug: string;
|
|
18
18
|
excerpt: string;
|
|
19
|
-
/**
|
|
19
|
+
/** Test-data flag — set on the shared admin-seeded test post; swept by testDataCleanup. */
|
|
20
20
|
isTestData?: boolean;
|
|
21
|
-
/** When isTestData, the cutoff after which
|
|
21
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
|
|
22
22
|
testDataExpiresAt?: Date;
|
|
23
23
|
content: string;
|
|
24
24
|
coverImage?: MediaFieldInput;
|