@mohasinac/appkit 2.7.50 → 2.7.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/_internal/server/jobs/core/__tests__/couponExpiry.test.d.ts +1 -0
  2. package/dist/_internal/server/jobs/core/__tests__/couponExpiry.test.js +65 -0
  3. package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.d.ts +1 -0
  4. package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.js +163 -0
  5. package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.d.ts +1 -0
  6. package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.js +115 -0
  7. package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.d.ts +1 -0
  8. package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.js +96 -0
  9. package/dist/client.d.ts +29 -6
  10. package/dist/client.js +16 -4
  11. package/dist/constants/api-endpoints.d.ts +42 -0
  12. package/dist/constants/api-endpoints.js +14 -0
  13. package/dist/features/_guide-cls.d.ts +13 -0
  14. package/dist/features/_guide-cls.js +14 -0
  15. package/dist/features/about/components/HelpPageView.js +29 -26
  16. package/dist/features/account/components/BuyerAccountGuideView.d.ts +1 -0
  17. package/dist/features/account/components/BuyerAccountGuideView.js +39 -0
  18. package/dist/features/account/components/BuyerAuctionsGuideView.d.ts +1 -0
  19. package/dist/features/account/components/BuyerAuctionsGuideView.js +38 -0
  20. package/dist/features/account/components/BuyerOrdersGuideView.d.ts +1 -0
  21. package/dist/features/account/components/BuyerOrdersGuideView.js +48 -0
  22. package/dist/features/account/components/BuyerShoppingGuideView.d.ts +1 -0
  23. package/dist/features/account/components/BuyerShoppingGuideView.js +38 -0
  24. package/dist/features/admin/components/AdminAnalyticsGuideView.d.ts +1 -0
  25. package/dist/features/admin/components/AdminAnalyticsGuideView.js +24 -0
  26. package/dist/features/admin/components/AdminBlogEditorView.js +100 -77
  27. package/dist/features/admin/components/AdminBundleEditorView.js +20 -14
  28. package/dist/features/admin/components/AdminBundlesView.d.ts +1 -3
  29. package/dist/features/admin/components/AdminBundlesView.js +173 -52
  30. package/dist/features/admin/components/AdminCatalogGuideView.d.ts +1 -0
  31. package/dist/features/admin/components/AdminCatalogGuideView.js +28 -0
  32. package/dist/features/admin/components/AdminContentGuideView.d.ts +1 -0
  33. package/dist/features/admin/components/AdminContentGuideView.js +36 -0
  34. package/dist/features/admin/components/AdminCouponEditorView.js +1 -1
  35. package/dist/features/admin/components/AdminGuideHubView.d.ts +5 -0
  36. package/dist/features/admin/components/AdminGuideHubView.js +79 -0
  37. package/dist/features/admin/components/AdminOrdersGuideView.d.ts +1 -0
  38. package/dist/features/admin/components/AdminOrdersGuideView.js +44 -0
  39. package/dist/features/admin/components/AdminProductsView.js +8 -3
  40. package/dist/features/admin/components/AdminSiteConfigGuideView.d.ts +1 -0
  41. package/dist/features/admin/components/AdminSiteConfigGuideView.js +21 -0
  42. package/dist/features/admin/components/AdminStoresGuideView.d.ts +1 -0
  43. package/dist/features/admin/components/AdminStoresGuideView.js +32 -0
  44. package/dist/features/admin/components/AdminTeamGuideView.d.ts +1 -0
  45. package/dist/features/admin/components/AdminTeamGuideView.js +33 -0
  46. package/dist/features/admin/components/AdminTrustGuideView.d.ts +1 -0
  47. package/dist/features/admin/components/AdminTrustGuideView.js +40 -0
  48. package/dist/features/admin/components/AdminUsersGuideView.d.ts +1 -0
  49. package/dist/features/admin/components/AdminUsersGuideView.js +38 -0
  50. package/dist/features/categories/components/CategoryBundlesListing.js +3 -33
  51. package/dist/features/categories/components/index.d.ts +0 -2
  52. package/dist/features/categories/components/index.js +0 -1
  53. package/dist/features/events/components/AdminEventEditorView.js +223 -269
  54. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +9 -0
  55. package/dist/features/grouped/repository/grouped-listings.repository.js +12 -0
  56. package/dist/features/media/upload/MediaUploadField.js +1 -1
  57. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -1
  58. package/dist/features/products/components/AuctionsIndexListing.js +3 -1
  59. package/dist/features/products/components/MarketplaceBundleCard.d.ts +21 -0
  60. package/dist/features/products/components/MarketplaceBundleCard.js +56 -0
  61. package/dist/features/products/components/ProductsIndexListing.js +3 -1
  62. package/dist/features/products/components/index.d.ts +2 -4
  63. package/dist/features/products/components/index.js +1 -2
  64. package/dist/features/promotions/schemas/firestore.js +1 -1
  65. package/dist/features/reviews/components/index.d.ts +0 -2
  66. package/dist/features/reviews/components/index.js +0 -1
  67. package/dist/features/seller/components/BrandInlineSelect.js +23 -3
  68. package/dist/features/seller/components/CategoryInlineSelect.js +20 -3
  69. package/dist/features/seller/components/SellerAnalyticsAlertsView.d.ts +6 -0
  70. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +124 -0
  71. package/dist/features/seller/components/SellerAnalyticsView.d.ts +2 -0
  72. package/dist/features/seller/components/SellerAnalyticsView.js +17 -9
  73. package/dist/features/seller/components/SellerBundlesView.d.ts +5 -0
  74. package/dist/features/seller/components/SellerBundlesView.js +104 -0
  75. package/dist/features/seller/components/SellerClassifiedView.d.ts +5 -0
  76. package/dist/features/seller/components/SellerClassifiedView.js +113 -0
  77. package/dist/features/seller/components/SellerDashboardView.js +19 -7
  78. package/dist/features/seller/components/SellerDigitalCodesView.d.ts +5 -0
  79. package/dist/features/seller/components/SellerDigitalCodesView.js +115 -0
  80. package/dist/features/seller/components/SellerGoogleReviewsView.d.ts +14 -0
  81. package/dist/features/seller/components/SellerGoogleReviewsView.js +95 -0
  82. package/dist/features/seller/components/SellerGroupedListingsView.d.ts +6 -0
  83. package/dist/features/seller/components/SellerGroupedListingsView.js +112 -0
  84. package/dist/features/seller/components/SellerLiveView.d.ts +5 -0
  85. package/dist/features/seller/components/SellerLiveView.js +113 -0
  86. package/dist/features/seller/components/SellerOrdersView.js +18 -2
  87. package/dist/features/seller/components/SellerPayoutMethodsView.d.ts +7 -0
  88. package/dist/features/seller/components/SellerPayoutMethodsView.js +120 -0
  89. package/dist/features/seller/components/SellerPayoutSettingsView.js +50 -12
  90. package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
  91. package/dist/features/seller/components/SellerProductShell.js +9 -7
  92. package/dist/features/seller/components/SellerProductsFilterDrawer.js +3 -1
  93. package/dist/features/seller/components/SellerProductsView.js +48 -54
  94. package/dist/features/seller/components/SellerShippingConfigsView.d.ts +7 -0
  95. package/dist/features/seller/components/SellerShippingConfigsView.js +149 -0
  96. package/dist/features/seller/components/SellerShippingView.js +37 -10
  97. package/dist/features/seller/components/SellerStoreCategoriesView.d.ts +9 -0
  98. package/dist/features/seller/components/SellerStoreCategoriesView.js +122 -0
  99. package/dist/features/seller/components/SellerStorefrontView.d.ts +0 -7
  100. package/dist/features/seller/components/SellerStorefrontView.js +26 -29
  101. package/dist/features/seller/components/SellerTemplatesView.d.ts +10 -0
  102. package/dist/features/seller/components/SellerTemplatesView.js +265 -0
  103. package/dist/features/seller/components/index.d.ts +26 -0
  104. package/dist/features/seller/components/index.js +13 -0
  105. package/dist/features/stores/api/[storeSlug]/reviews/route.js +49 -7
  106. package/dist/features/stores/components/StoreCapabilitiesGuideView.d.ts +3 -0
  107. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +101 -0
  108. package/dist/features/stores/components/StoreFinanceGuideView.d.ts +3 -0
  109. package/dist/features/stores/components/StoreFinanceGuideView.js +79 -0
  110. package/dist/features/stores/components/StoreGuideHubView.d.ts +5 -0
  111. package/dist/features/stores/components/StoreGuideHubView.js +89 -0
  112. package/dist/features/stores/components/StoreListingsGuideView.d.ts +3 -0
  113. package/dist/features/stores/components/StoreListingsGuideView.js +151 -0
  114. package/dist/features/stores/components/StoreOrdersGuideView.d.ts +3 -0
  115. package/dist/features/stores/components/StoreOrdersGuideView.js +122 -0
  116. package/dist/features/stores/components/StoreReviewsListing.js +100 -23
  117. package/dist/features/stores/components/StoreSettingsGuideView.d.ts +3 -0
  118. package/dist/features/stores/components/StoreSettingsGuideView.js +56 -0
  119. package/dist/features/stores/components/index.d.ts +12 -1
  120. package/dist/features/stores/components/index.js +6 -1
  121. package/dist/features/stores/hooks/useStores.d.ts +5 -0
  122. package/dist/features/stores/hooks/useStores.js +10 -0
  123. package/dist/features/stores/manifest.js +1 -1
  124. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +13 -2
  125. package/dist/index.d.ts +54 -9
  126. package/dist/index.js +58 -15
  127. package/dist/next/routing/route-map.d.ts +55 -0
  128. package/dist/next/routing/route-map.js +25 -0
  129. package/dist/providers/db-firebase/__tests__/filter-aliases.test.d.ts +1 -0
  130. package/dist/providers/db-firebase/__tests__/filter-aliases.test.js +93 -0
  131. package/dist/providers/db-firebase/sieve.d.ts +49 -0
  132. package/dist/providers/db-firebase/sieve.js +61 -8
  133. package/dist/repositories/index.d.ts +1 -0
  134. package/dist/repositories/index.js +2 -0
  135. package/dist/tailwind-utilities.css +1 -1
  136. package/dist/ui/components/ListingToolbar.d.ts +8 -1
  137. package/dist/ui/components/ListingToolbar.js +4 -2
  138. package/dist/ui/components/__tests__/BulkActionBar.test.d.ts +1 -0
  139. package/dist/ui/components/__tests__/BulkActionBar.test.js +96 -0
  140. package/dist/ui/components/__tests__/ListingToolbar.test.d.ts +1 -0
  141. package/dist/ui/components/__tests__/ListingToolbar.test.js +125 -0
  142. package/dist/ui/index.d.ts +1 -1
  143. package/package.json +8 -2
@@ -1,13 +1,12 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { useMutation, useQuery } from "@tanstack/react-query";
5
- import { Button, Form, Heading, Input, RichTextEditor, RichTextRenderer, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
6
- import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
7
- import { ImageUpload } from "../../media/upload/ImageUpload";
8
- import { useMediaUpload } from "../../media";
5
+ import { Button, ConfirmDeleteModal, Div, Heading, Input, RichTextEditor, RichTextRenderer, Select, StackedViewShell, TagInput, Text, Toggle, useToast, } from "../../../ui";
6
+ import { ImageUpload, useMediaUpload } from "../../media";
9
7
  import { apiClient } from "../../../http";
10
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
+ import { StepForm } from "../../shell";
11
10
  // --- Helpers -----------------------------------------------------------------
12
11
  const STATUS_OPTIONS = [
13
12
  { label: "Draft", value: "draft" },
@@ -21,6 +20,22 @@ const CATEGORY_OPTIONS = [
21
20
  { label: "Updates", value: "updates" },
22
21
  { label: "Community", value: "community" },
23
22
  ];
23
+ const DEFAULT_DRAFT = {
24
+ title: "",
25
+ slug: "",
26
+ excerpt: "",
27
+ content: "",
28
+ coverImage: "",
29
+ youtubeId: "",
30
+ category: "news",
31
+ tags: [],
32
+ status: "published",
33
+ isFeatured: true,
34
+ authorName: "Admin",
35
+ publishedAt: "",
36
+ metaTitle: "",
37
+ metaDescription: "",
38
+ };
24
39
  function toSlug(str) {
25
40
  const base = str
26
41
  .toLowerCase()
@@ -42,23 +57,15 @@ function toDateInputValue(val) {
42
57
  // --- Component ---------------------------------------------------------------
43
58
  export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...rest }) {
44
59
  const isEdit = Boolean(postId);
45
- const [title, setTitle] = React.useState("");
46
- const [slug, setSlug] = React.useState("");
60
+ const [draft, setDraft] = React.useState(DEFAULT_DRAFT);
47
61
  const [slugManual, setSlugManual] = React.useState(false);
48
- const [excerpt, setExcerpt] = React.useState("");
49
- const [content, setContent] = React.useState("");
50
- const [coverImage, setCoverImage] = React.useState("");
51
- const [category, setCategory] = React.useState("news");
52
- const [tags, setTags] = React.useState([]);
53
- const [status, setStatus] = React.useState("draft");
54
- const [isFeatured, setIsFeatured] = React.useState(false);
55
- const [authorName, setAuthorName] = React.useState("Admin");
56
- const [publishedAt, setPublishedAt] = React.useState("");
57
- const [metaTitle, setMetaTitle] = React.useState("");
58
- const [metaDescription, setMetaDescription] = React.useState("");
62
+ const [currentStep, setCurrentStep] = React.useState(0);
63
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
59
64
  const { showToast } = useToast();
60
65
  const { upload } = useMediaUpload();
61
- const { shellCtx, setFieldError, clearErrors } = useFormShellState();
66
+ const update = React.useCallback((partial) => {
67
+ setDraft((prev) => ({ ...prev, ...partial }));
68
+ }, []);
62
69
  // --- load existing post (edit mode) ---
63
70
  const postQuery = useQuery({
64
71
  queryKey: ["admin", "blog", postId],
@@ -72,49 +79,46 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
72
79
  const p = postQuery.data;
73
80
  if (!p)
74
81
  return;
75
- setTitle(p.title ?? "");
76
- setSlug(p.slug ?? "");
82
+ setDraft({
83
+ title: p.title ?? "",
84
+ slug: p.slug ?? "",
85
+ excerpt: p.excerpt ?? "",
86
+ content: p.content ?? "",
87
+ coverImage: typeof p.coverImage === "string" ? p.coverImage : p.coverImage?.url ?? "",
88
+ youtubeId: p.youtubeId ?? "",
89
+ category: p.category ?? "news",
90
+ tags: Array.isArray(p.tags) ? p.tags : [],
91
+ status: p.status ?? "published",
92
+ isFeatured: p.isFeatured ?? true,
93
+ authorName: p.authorName ?? "Admin",
94
+ publishedAt: toDateInputValue(p.publishedAt),
95
+ metaTitle: p.metaTitle ?? "",
96
+ metaDescription: p.metaDescription ?? "",
97
+ });
77
98
  setSlugManual(true);
78
- setExcerpt(p.excerpt ?? "");
79
- setContent(p.content ?? "");
80
- setCoverImage(typeof p.coverImage === "string"
81
- ? p.coverImage
82
- : p.coverImage?.url ?? "");
83
- setCategory(p.category ?? "news");
84
- setTags(Array.isArray(p.tags) ? p.tags : []);
85
- setStatus(p.status ?? "draft");
86
- setIsFeatured(p.isFeatured ?? false);
87
- setAuthorName(p.authorName ?? "Admin");
88
- setPublishedAt(toDateInputValue(p.publishedAt));
89
- setMetaTitle(p.metaTitle ?? "");
90
- setMetaDescription(p.metaDescription ?? "");
91
99
  }, [postQuery.data]);
92
- const handleTitleChange = (value) => {
93
- setTitle(value);
94
- if (!slugManual)
95
- setSlug(toSlug(value));
96
- };
97
100
  // --- save ---
98
101
  const saveMutation = useMutation({
99
102
  mutationFn: async () => {
100
103
  const payload = {
101
- title,
102
- slug: slug || toSlug(title),
103
- excerpt,
104
- content,
105
- coverImage: coverImage || undefined,
106
- category,
107
- tags,
108
- status,
109
- isFeatured,
110
- authorName,
111
- publishedAt: status === "published" && !publishedAt
104
+ title: draft.title,
105
+ slug: draft.slug || toSlug(draft.title),
106
+ excerpt: draft.excerpt,
107
+ content: draft.content,
108
+ coverImage: draft.coverImage || undefined,
109
+ youtubeId: draft.youtubeId || undefined,
110
+ category: draft.category,
111
+ tags: draft.tags,
112
+ status: draft.status,
113
+ isFeatured: draft.isFeatured,
114
+ authorName: draft.authorName,
115
+ publishedAt: draft.status === "published" && !draft.publishedAt
112
116
  ? new Date().toISOString()
113
- : publishedAt
114
- ? new Date(publishedAt).toISOString()
117
+ : draft.publishedAt
118
+ ? new Date(draft.publishedAt).toISOString()
115
119
  : undefined,
116
- metaTitle: metaTitle || undefined,
117
- metaDescription: metaDescription || undefined,
120
+ metaTitle: draft.metaTitle || undefined,
121
+ metaDescription: draft.metaDescription || undefined,
118
122
  };
119
123
  if (isEdit) {
120
124
  return apiClient.patch(ADMIN_ENDPOINTS.BLOG_BY_ID(postId), payload);
@@ -122,7 +126,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
122
126
  return apiClient.post(ADMIN_ENDPOINTS.BLOG, {
123
127
  ...payload,
124
128
  authorId: "admin",
125
- readTimeMinutes: Math.max(1, Math.round(content.replace(/<[^>]*>/g, "").split(/\s+/).length / 200)),
129
+ readTimeMinutes: Math.max(1, Math.round(draft.content.replace(/<[^>]*>/g, "").split(/\s+/).filter(Boolean).length / 200)),
126
130
  views: 0,
127
131
  });
128
132
  },
@@ -146,30 +150,49 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
146
150
  },
147
151
  onError: (err) => showToast(err?.message ?? "Failed to delete post.", "error"),
148
152
  });
149
- const isSubmitting = saveMutation.isPending || postQuery.isLoading;
150
- const canSave = Boolean(title);
151
- const formSection = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
152
- e.preventDefault();
153
- clearErrors();
154
- if (!title.trim()) {
155
- setFieldError("title", "Title is required");
156
- return;
157
- }
158
- saveMutation.mutate();
159
- }, className: "space-y-5", children: [_jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => handleTitleChange(v), required: true, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
160
- setSlug(e.target.value);
161
- setSlugManual(true);
162
- }, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: category, onValueChange: (v) => setCategory(v) }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: (v) => setStatus(v) })] }), _jsx(Input, { label: "Excerpt", value: excerpt, onChange: (e) => setExcerpt(e.target.value), placeholder: "Short summary shown in listings and cards" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "blog", true, { type: "blog-cover", title: title || slug, category }), onChange: setCoverImage }), _jsxs("div", { className: "space-y-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Content" }), _jsx(RichTextEditor, { value: content, onChange: setContent, placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] }), _jsx(TagInput, { label: "Tags", value: tags, onChange: setTags, placeholder: "e.g. pokemon, grading, tcg" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Input, { label: "Author name", value: authorName, onChange: (e) => setAuthorName(e.target.value), placeholder: "Author display name" }), _jsx(Input, { label: "Publish date (optional)", value: publishedAt, onChange: (e) => setPublishedAt(e.target.value), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Toggle, { label: "Featured post", checked: isFeatured, onChange: setIsFeatured }), _jsx(Input, { label: "Meta title (optional)", value: metaTitle, onChange: (e) => setMetaTitle(e.target.value), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta description (optional)", value: metaDescription, onChange: (e) => setMetaDescription(e.target.value), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !canSave || isSubmitting, children: isEdit ? "Save changes" : "Create post" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
163
- if (confirm("Delete this post? This cannot be undone.")) {
164
- deleteMutation.mutate();
165
- }
166
- }, children: "Delete post" }))] })] }, "blog-form") }));
153
+ const isLoading = saveMutation.isPending || postQuery.isLoading;
154
+ const steps = [
155
+ {
156
+ label: "Content",
157
+ validate: (values) => !values.title.trim() ? "Title is required" : null,
158
+ render: ({ values, onChange }) => (_jsxs("div", { className: "space-y-5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Content" }), _jsx(Input, { label: "Title", value: values.title, onChange: (e) => {
159
+ const v = e.target.value;
160
+ onChange({ title: v, ...(!slugManual && { slug: toSlug(v) }) });
161
+ }, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: values.slug, onChange: (e) => {
162
+ setSlugManual(true);
163
+ onChange({ slug: e.target.value });
164
+ }, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs("div", { className: "space-y-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Content" }), _jsx(RichTextEditor, { value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
165
+ },
166
+ {
167
+ label: "Media",
168
+ render: ({ values, onChange }) => (_jsxs("div", { className: "space-y-5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(ImageUpload, { label: "Cover Image", currentImage: values.coverImage, onUpload: (file) => upload(file, "blog", true, {
169
+ type: "blog-cover",
170
+ title: values.title || values.slug,
171
+ category: values.category,
172
+ }), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "YouTube Video ID (optional)", value: values.youtubeId, onChange: (e) => onChange({ youtubeId: e.target.value }), placeholder: "e.g. dQw4w9WgXcQ", helperText: "The 11-character video ID from the YouTube URL." })] })),
173
+ },
174
+ {
175
+ label: "SEO & Tags",
176
+ render: ({ values, onChange }) => {
177
+ const readTime = Math.max(1, Math.round(values.content.replace(/<[^>]*>/g, "").split(/\s+/).filter(Boolean).length / 200));
178
+ return (_jsxs("div", { className: "space-y-5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "SEO & Tags" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: values.category, onValueChange: (v) => onChange({ category: v }) }), _jsxs(Div, { children: [_jsx(Text, { className: "text-sm font-medium mb-1.5", children: "Estimated Read Time" }), _jsxs(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: ["~", readTime, " min (auto-calculated from content)"] })] })] }), _jsx(TagInput, { label: "Tags", value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, grading, tcg" }), _jsx(Input, { label: "Meta Title (optional)", value: values.metaTitle, onChange: (e) => onChange({ metaTitle: e.target.value }), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta Description (optional)", value: values.metaDescription, onChange: (e) => onChange({ metaDescription: e.target.value }), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 })] }));
179
+ },
180
+ },
181
+ {
182
+ label: "Publish",
183
+ render: ({ values, onChange }) => (_jsxs("div", { className: "space-y-5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Publish Settings" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: values.status, onValueChange: (v) => onChange({ status: v }) }), _jsx(Input, { label: "Publish date (optional)", value: values.publishedAt, onChange: (e) => onChange({ publishedAt: e.target.value }), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Input, { label: "Author Name", value: values.authorName, onChange: (e) => onChange({ authorName: e.target.value }), placeholder: "Author display name" }), _jsx(Toggle, { label: "Featured post", checked: values.isFeatured, onChange: (checked) => onChange({ isFeatured: checked }) }), isEdit && (_jsx("div", { className: "pt-4 border-t border-[var(--appkit-color-border)]", children: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => setShowDeleteConfirm(true), isLoading: deleteMutation.isPending, children: "Delete post" }) }))] })),
184
+ },
185
+ ];
186
+ const deleteModal = (_jsx(ConfirmDeleteModal, { isOpen: showDeleteConfirm, onConfirm: () => {
187
+ setShowDeleteConfirm(false);
188
+ deleteMutation.mutate();
189
+ }, onClose: () => setShowDeleteConfirm(false), title: "Delete post", message: "This will permanently delete the blog post. This cannot be undone." }));
190
+ const formContent = (_jsxs(_Fragment, { children: [_jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: () => { saveMutation.mutate(); }, formId: "admin-blog", currentStep: currentStep, onStepChange: setCurrentStep, completeLabel: isEdit ? "Save Changes" : "Create Post", isLoading: isLoading }), deleteModal] }));
167
191
  if (embedded) {
168
- return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
192
+ return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
169
193
  }
170
- // S-STORE-9B-D rendered preview pane alongside the editor.
171
- const previewSection = (_jsxs("div", { className: "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] p-5 max-h-[calc(100vh-12rem)] overflow-y-auto", children: [_jsx("div", { className: "flex items-center gap-2 mb-3 text-xs font-semibold uppercase tracking-wide text-[var(--appkit-color-text-muted)]", children: _jsx("span", { children: "Live preview" }) }), coverImage ? (_jsx("img", { src: coverImage, alt: "", className: "w-full rounded-md mb-4 object-cover max-h-64" })) : null, _jsx(Heading, { level: 1, className: "text-2xl font-bold mb-1", children: title || "Untitled post" }), _jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] mb-4", children: [authorName || "Anonymous", category ? ` · ${category}` : ""] }), excerpt ? (_jsx(Text, { className: "italic text-[var(--appkit-color-text-secondary)] mb-4", children: excerpt })) : null, _jsx(RichTextRenderer, { html: content || "<em>No content yet…</em>" }), tags.length > 0 ? (_jsx("div", { className: "mt-6 flex flex-wrap gap-1.5", children: tags.map((t) => (_jsxs("span", { className: "px-2 py-0.5 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] text-xs", children: ["#", t] }, t))) })) : null] }, "preview"));
194
+ const previewSection = (_jsxs("div", { className: "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] p-5 max-h-[calc(100vh-12rem)] overflow-y-auto", children: [_jsx("div", { className: "flex items-center gap-2 mb-3 text-xs font-semibold uppercase tracking-wide text-[var(--appkit-color-text-muted)]", children: _jsx("span", { children: "Live preview" }) }), draft.coverImage ? (_jsx("img", { src: draft.coverImage, alt: "", className: "w-full rounded-md mb-4 object-cover max-h-64" })) : null, _jsx(Heading, { level: 1, className: "text-2xl font-bold mb-1", children: draft.title || "Untitled post" }), _jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] mb-4", children: [draft.authorName || "Anonymous", draft.category ? ` · ${draft.category}` : ""] }), draft.excerpt ? (_jsx(Text, { className: "italic text-[var(--appkit-color-text-secondary)] mb-4", children: draft.excerpt })) : null, _jsx(RichTextRenderer, { html: draft.content || "<em>No content yet…</em>" }), draft.tags.length > 0 ? (_jsx("div", { className: "mt-6 flex flex-wrap gap-1.5", children: draft.tags.map((t) => (_jsxs("span", { className: "px-2 py-0.5 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] text-xs", children: ["#", t] }, t))) })) : null] }, "preview"));
172
195
  return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Post" : "New Blog Post", sections: [
173
- _jsxs("div", { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-6", children: [_jsx("div", { children: formSection }), _jsx("div", { className: "mt-6 lg:mt-0 lg:sticky lg:top-4 lg:self-start", children: previewSection })] }, "split"),
196
+ _jsxs("div", { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-6", children: [_jsx("div", { children: formContent }), _jsx("div", { className: "mt-6 lg:mt-0 lg:sticky lg:top-4 lg:self-start", children: previewSection })] }, "split"),
174
197
  ] }));
175
198
  }
@@ -14,6 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  */
15
15
  import { useCallback, useEffect, useMemo, useState } from "react";
16
16
  import { Button, Checkbox, Container, Heading, Input, Row, Section, Select, Stack, Text, Textarea, } from "../../../ui";
17
+ import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
17
18
  import { BundleItemsPicker, defaultBundleItemsFetch, } from "../../categories/components/BundleItemsPicker";
18
19
  import { BundleDynamicRuleEditor } from "../../categories/components/BundleDynamicRuleEditor";
19
20
  import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
@@ -76,8 +77,9 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
76
77
  const [loading, setLoading] = useState(isEdit);
77
78
  const [saving, setSaving] = useState(false);
78
79
  const [deleting, setDeleting] = useState(false);
79
- const [error, setError] = useState(null);
80
+ const [apiError, setApiError] = useState(null);
80
81
  const [metadata, setMetadata] = useState({});
82
+ const { shellCtx, setFieldError, clearErrors } = useFormShellState();
81
83
  // Load existing bundle on mount when editing
82
84
  useEffect(() => {
83
85
  if (!bundleId)
@@ -97,7 +99,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
97
99
  .catch((err) => {
98
100
  if (cancelled)
99
101
  return;
100
- setError(err instanceof Error
102
+ setApiError(err instanceof Error
101
103
  ? err.message
102
104
  : BUNDLE_COPY.adminEditor.errors.loadFailed);
103
105
  })
@@ -110,16 +112,20 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
110
112
  };
111
113
  }, [bundleId]);
112
114
  const handleSave = useCallback(async () => {
113
- setError(null);
115
+ clearErrors();
116
+ setApiError(null);
114
117
  const priceInPaise = parsePriceRupees(form.priceRupees);
118
+ let hasError = false;
115
119
  if (!form.name.trim()) {
116
- setError(BUNDLE_COPY.adminEditor.errors.nameRequired);
117
- return;
120
+ setFieldError("name", BUNDLE_COPY.adminEditor.errors.nameRequired);
121
+ hasError = true;
118
122
  }
119
123
  if (priceInPaise === null) {
120
- setError(BUNDLE_COPY.adminEditor.errors.priceInvalid);
121
- return;
124
+ setFieldError("price", BUNDLE_COPY.adminEditor.errors.priceInvalid);
125
+ hasError = true;
122
126
  }
127
+ if (hasError)
128
+ return;
123
129
  setSaving(true);
124
130
  try {
125
131
  // SB-UNI-5 2026-05-13 — static vs dynamic rule branching.
@@ -171,14 +177,14 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
171
177
  }
172
178
  }
173
179
  catch (err) {
174
- setError(err instanceof Error
180
+ setApiError(err instanceof Error
175
181
  ? err.message
176
182
  : BUNDLE_COPY.adminEditor.errors.saveFailed);
177
183
  }
178
184
  finally {
179
185
  setSaving(false);
180
186
  }
181
- }, [form, bundleId, isEdit, onSaved]);
187
+ }, [form, bundleId, isEdit, onSaved, clearErrors, setFieldError]);
182
188
  const handleDelete = useCallback(async () => {
183
189
  if (!bundleId)
184
190
  return;
@@ -186,7 +192,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
186
192
  return;
187
193
  }
188
194
  setDeleting(true);
189
- setError(null);
195
+ setApiError(null);
190
196
  try {
191
197
  const res = await fetch(`/api/admin/bundles/${encodeURIComponent(bundleId)}`, { method: "DELETE" });
192
198
  if (!res.ok)
@@ -194,7 +200,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
194
200
  onDeleted?.();
195
201
  }
196
202
  catch (err) {
197
- setError(err instanceof Error
203
+ setApiError(err instanceof Error
198
204
  ? err.message
199
205
  : BUNDLE_COPY.adminEditor.errors.deleteFailed);
200
206
  }
@@ -206,7 +212,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
206
212
  if (loading) {
207
213
  return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "lg", children: _jsx(Text, { children: BUNDLE_COPY.adminEditor.loading }) }) }));
208
214
  }
209
- return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsx(Heading, { level: 1, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: isEdit
210
- ? BUNDLE_COPY.adminEditorTitleEdit
211
- : BUNDLE_COPY.adminEditorTitleNew }), isEdit && (_jsx(Button, { variant: "danger", onClick: handleDelete, disabled: deleting, children: BUNDLE_COPY.adminEditor.deleteButton(deleting) }))] }), error && (_jsx(Text, { color: "danger", role: "alert", children: error })), _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.nameLabel }), _jsx(Input, { type: "text", value: form.name, onChange: (e) => setForm((f) => ({ ...f, name: e.target.value })), placeholder: BUNDLE_COPY.adminEditor.fields.namePlaceholder, disabled: saving })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.descriptionLabel }), _jsx(Textarea, { value: form.description, onChange: (e) => setForm((f) => ({ ...f, description: e.target.value })), placeholder: BUNDLE_COPY.adminEditor.fields.descriptionPlaceholder, rows: 4, disabled: saving })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.priceLabel }), _jsx(Input, { type: "number", inputMode: "decimal", min: 1, step: 1, value: form.priceRupees, onChange: (e) => setForm((f) => ({ ...f, priceRupees: e.target.value })), placeholder: BUNDLE_COPY.adminEditor.fields.pricePlaceholder, disabled: saving }), _jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.adminEditor.fields.pricePaiseHint(parsePriceRupees(form.priceRupees)) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.coverImageLabel }), _jsx(Input, { type: "url", value: form.coverImage, onChange: (e) => setForm((f) => ({ ...f, coverImage: e.target.value })), placeholder: "https://\u2026", disabled: saving })] }), _jsx(Checkbox, { checked: form.isActive, onChange: (e) => setForm((f) => ({ ...f, isActive: e.target.checked })), disabled: saving, label: BUNDLE_COPY.adminEditor.fields.activeLabel }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.ruleTypeLabel }), _jsx(Select, { options: RULE_TYPE_OPTIONS, value: form.ruleType, onValueChange: (next) => setForm((f) => ({ ...f, ruleType: next })), disabled: saving, "aria-label": BUNDLE_COPY.adminEditor.ruleTypeLabel })] }), form.ruleType === "static" ? (_jsx(BundleItemsPicker, { value: form.productIds, onChange: (next) => setForm((f) => ({ ...f, productIds: next })), fetchProducts: fetchProducts, initialMetadata: metadata })) : (_jsx(BundleDynamicRuleEditor, { value: form.dynamicRule, onChange: (next) => setForm((f) => ({ ...f, dynamicRule: next })), disabled: saving }))] }), _jsx(Row, { gap: "sm", align: "center", justify: "end", children: _jsx(Button, { variant: "primary", onClick: handleSave, disabled: saving, "aria-busy": saving, children: BUNDLE_COPY.adminEditor.saveButton(saving, isEdit) }) })] }) }) }));
215
+ return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Section, { className: "py-10", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsx(Heading, { level: 1, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: isEdit
216
+ ? BUNDLE_COPY.adminEditorTitleEdit
217
+ : BUNDLE_COPY.adminEditorTitleNew }), isEdit && (_jsx(Button, { variant: "danger", onClick: handleDelete, disabled: deleting, children: BUNDLE_COPY.adminEditor.deleteButton(deleting) }))] }), apiError && (_jsx(Text, { color: "danger", role: "alert", children: apiError })), _jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "name", label: BUNDLE_COPY.adminEditor.fields.nameLabel, value: form.name, onChange: (v) => setForm((f) => ({ ...f, name: v })), placeholder: BUNDLE_COPY.adminEditor.fields.namePlaceholder, disabled: saving, required: true }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.descriptionLabel }), _jsx(Textarea, { value: form.description, onChange: (e) => setForm((f) => ({ ...f, description: e.target.value })), placeholder: BUNDLE_COPY.adminEditor.fields.descriptionPlaceholder, rows: 4, disabled: saving })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(FieldInput, { name: "price", label: BUNDLE_COPY.adminEditor.fields.priceLabel, type: "number", inputMode: "decimal", min: 1, step: 1, value: form.priceRupees, onChange: (v) => setForm((f) => ({ ...f, priceRupees: v })), placeholder: BUNDLE_COPY.adminEditor.fields.pricePlaceholder, disabled: saving, required: true }), _jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.adminEditor.fields.pricePaiseHint(parsePriceRupees(form.priceRupees)) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.fields.coverImageLabel }), _jsx(Input, { type: "url", value: form.coverImage, onChange: (e) => setForm((f) => ({ ...f, coverImage: e.target.value })), placeholder: "https://\u2026", disabled: saving })] }), _jsx(Checkbox, { checked: form.isActive, onChange: (e) => setForm((f) => ({ ...f, isActive: e.target.checked })), disabled: saving, label: BUNDLE_COPY.adminEditor.fields.activeLabel }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: BUNDLE_COPY.adminEditor.ruleTypeLabel }), _jsx(Select, { options: RULE_TYPE_OPTIONS, value: form.ruleType, onValueChange: (next) => setForm((f) => ({ ...f, ruleType: next })), disabled: saving, "aria-label": BUNDLE_COPY.adminEditor.ruleTypeLabel })] }), form.ruleType === "static" ? (_jsx(BundleItemsPicker, { value: form.productIds, onChange: (next) => setForm((f) => ({ ...f, productIds: next })), fetchProducts: fetchProducts, initialMetadata: metadata })) : (_jsx(BundleDynamicRuleEditor, { value: form.dynamicRule, onChange: (next) => setForm((f) => ({ ...f, dynamicRule: next })), disabled: saving }))] }), _jsx(Row, { gap: "sm", align: "center", justify: "end", children: _jsx(Button, { variant: "primary", onClick: handleSave, disabled: saving, "aria-busy": saving, children: BUNDLE_COPY.adminEditor.saveButton(saving, isEdit) }) })] }) }) }) }));
212
218
  }
@@ -1,9 +1,7 @@
1
1
  export interface AdminBundlesViewProps {
2
- /** Builds the href for the row-click edit action. */
3
2
  getEditHref: (row: {
4
3
  id: string;
5
4
  }) => string;
6
- /** Builds the href for the "New bundle" CTA. */
7
5
  newHref: string;
8
6
  }
9
- export declare function AdminBundlesView({ getEditHref, newHref, }: AdminBundlesViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function AdminBundlesView({ getEditHref, newHref }: AdminBundlesViewProps): import("react/jsx-runtime").JSX.Element;