@mohasinac/appkit 2.7.45 → 2.7.47
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/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +1 -0
- package/dist/core/unit-of-work.js +3 -4
- package/dist/features/account/components/AddressForm.js +23 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +18 -11
- package/dist/features/admin/components/AdminBrandEditorView.js +18 -11
- package/dist/features/admin/components/AdminCarouselEditorView.js +18 -8
- package/dist/features/admin/components/AdminCategoryEditorView.js +18 -11
- package/dist/features/admin/components/AdminCouponEditorView.js +18 -11
- package/dist/features/admin/components/AdminFaqEditorView.js +18 -11
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +18 -11
- package/dist/features/auth/actions/profile-actions.js +4 -1
- package/dist/features/auth/repository/user.repository.d.ts +1 -0
- package/dist/features/auth/repository/user.repository.js +3 -0
- package/dist/features/blog/api/route.js +3 -0
- package/dist/features/blog/components/BlogIndexListing.js +2 -0
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/categories/api/route.js +1 -1
- package/dist/features/categories/components/BundleDetailView.js +5 -3
- package/dist/features/categories/components/CategoryBundlesListing.js +77 -41
- package/dist/features/categories/components/CategoryDetailPageView.js +12 -9
- package/dist/features/categories/components/CategoryProductsListing.js +3 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +3 -4
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +3 -4
- package/dist/features/events/actions/event-actions.d.ts +2 -3
- package/dist/features/events/actions/event-actions.js +3 -5
- package/dist/features/events/api/route.js +7 -7
- package/dist/features/events/components/AdminEventEditorView.js +80 -6
- package/dist/features/events/components/AdminEventEntriesView.js +33 -4
- package/dist/features/events/components/EventFilters.d.ts +3 -0
- package/dist/features/events/components/EventFilters.js +1 -0
- package/dist/features/events/components/EventLeaderboard.js +1 -1
- package/dist/features/events/components/EventsIndexListing.js +10 -7
- package/dist/features/events/repository/event-entry.repository.d.ts +2 -2
- package/dist/features/events/repository/event-entry.repository.js +32 -7
- package/dist/features/events/types/index.d.ts +3 -1
- package/dist/features/homepage/components/BrandsSection.js +2 -2
- package/dist/features/homepage/components/CustomCardsSection.js +1 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/live/components/LiveItemsIndexListing.js +3 -4
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -4
- package/dist/features/products/components/AuctionsIndexListing.js +3 -4
- package/dist/features/products/components/PrizeDrawsIndexListing.js +3 -8
- package/dist/features/products/components/ProductDetailPageView.js +17 -2
- package/dist/features/products/components/ProductsIndexListing.js +10 -6
- package/dist/features/products/schemas/firestore.d.ts +2 -0
- package/dist/features/reviews/actions/review-actions.js +2 -0
- package/dist/features/reviews/components/ReviewDetailShell.js +3 -2
- package/dist/features/reviews/components/ReviewFilters.js +1 -1
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -2
- package/dist/features/reviews/hooks/useReviews.js +2 -0
- package/dist/features/reviews/schemas/firestore.d.ts +4 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/components/ScamRegistryView.js +41 -8
- package/dist/features/seller/components/SellerProductShell.js +27 -3
- package/dist/features/shell/StepForm.d.ts +6 -2
- package/dist/features/shell/StepForm.js +13 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +3 -4
- package/dist/features/stores/components/StoreDetailLayoutView.js +13 -5
- package/dist/features/stores/components/StorePreOrdersListing.js +3 -4
- package/dist/features/stores/components/StoreProductsListing.js +3 -4
- package/dist/index.d.ts +1 -0
- package/dist/providers/db-firebase/helpers.d.ts +4 -6
- package/dist/providers/db-firebase/helpers.js +18 -5
- package/dist/providers/db-firebase/sieve.js +26 -1
- package/dist/react/hooks/usePendingFilters.d.ts +2 -0
- package/dist/react/hooks/usePendingFilters.js +9 -0
- package/dist/react/hooks/usePendingTable.d.ts +5 -0
- package/dist/react/hooks/usePendingTable.js +1 -0
- package/dist/seed/homepage-sections-seed-data.js +2 -2
- package/dist/seed/products-standard-seed-data.js +1 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Drawer.style.css +13 -0
- package/dist/ui/components/FilterDrawer.d.ts +4 -2
- package/dist/ui/components/FilterDrawer.js +2 -2
- package/dist/ui/components/Toast.d.ts +4 -1
- package/dist/ui/components/Toast.js +3 -3
- package/dist/ui/forms/FormShell.d.ts +19 -1
- package/dist/ui/forms/FormShell.js +92 -0
- package/dist/ui/forms/index.d.ts +2 -2
- package/dist/ui/forms/index.js +1 -1
- package/package.json +2 -1
package/dist/client.d.ts
CHANGED
|
@@ -57,8 +57,8 @@ export { OtpInput } from "./ui/components/OtpInput";
|
|
|
57
57
|
export type { OtpInputProps } from "./ui/components/OtpInput";
|
|
58
58
|
export { DateInput, DateRangeInput } from "./ui/components/DateInput";
|
|
59
59
|
export type { DateInputProps, DateRangeInputProps } from "./ui/components/DateInput";
|
|
60
|
-
export type { FormShellProps, FormShellStep, FormShellContextValue } from "./ui/forms";
|
|
61
|
-
export { FormShell, useFormShell } from "./ui/forms";
|
|
60
|
+
export type { FormShellProps, FormShellProviderProps, FormShellStep, FormShellContextValue, UseFormShellStateResult } from "./ui/forms";
|
|
61
|
+
export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./ui/forms";
|
|
62
62
|
export type { FieldInputProps } from "./ui/forms";
|
|
63
63
|
export { FieldInput } from "./ui/forms";
|
|
64
64
|
export type { FieldSelectProps } from "./ui/forms";
|
package/dist/client.js
CHANGED
|
@@ -123,7 +123,7 @@ export { Checkbox } from "./ui/components/Checkbox";
|
|
|
123
123
|
export { Input } from "./ui/components/Input";
|
|
124
124
|
export { OtpInput } from "./ui/components/OtpInput";
|
|
125
125
|
export { DateInput, DateRangeInput } from "./ui/components/DateInput";
|
|
126
|
-
export { FormShell, useFormShell } from "./ui/forms";
|
|
126
|
+
export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./ui/forms";
|
|
127
127
|
export { FieldInput } from "./ui/forms";
|
|
128
128
|
export { FieldSelect } from "./ui/forms";
|
|
129
129
|
export { FieldCheckbox } from "./ui/forms";
|
|
@@ -70,6 +70,7 @@ export declare const PRODUCT_FIELDS: {
|
|
|
70
70
|
readonly PRIZE_REVEAL_WINDOW_START: "prizeRevealWindowStart";
|
|
71
71
|
readonly PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd";
|
|
72
72
|
readonly PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline";
|
|
73
|
+
readonly IS_PART_OF_BUNDLE: "isPartOfBundle";
|
|
73
74
|
readonly CREATED_AT: "createdAt";
|
|
74
75
|
readonly UPDATED_AT: "updatedAt";
|
|
75
76
|
readonly STATUS_VALUES: {
|
|
@@ -75,6 +75,7 @@ export const PRODUCT_FIELDS = {
|
|
|
75
75
|
PRIZE_REVEAL_WINDOW_START: "prizeRevealWindowStart",
|
|
76
76
|
PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd",
|
|
77
77
|
PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline",
|
|
78
|
+
IS_PART_OF_BUNDLE: "isPartOfBundle",
|
|
78
79
|
CREATED_AT: "createdAt",
|
|
79
80
|
UPDATED_AT: "updatedAt",
|
|
80
81
|
STATUS_VALUES: {
|
|
@@ -103,10 +103,9 @@ export class UnitOfWork {
|
|
|
103
103
|
serverLogger.debug("[UnitOfWork] Batch write committed successfully");
|
|
104
104
|
}
|
|
105
105
|
catch (error) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
throw new DatabaseError("Batch write failed", error);
|
|
106
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
107
|
+
serverLogger.error("[UnitOfWork] Batch write failed", { error: detail });
|
|
108
|
+
throw new DatabaseError(`Batch write failed: ${detail}`, error);
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Checkbox, FormField, FormGroup } from "../../../ui";
|
|
5
|
+
import { useFormShellState } from "../../../ui/forms";
|
|
5
6
|
import { THEME_CONSTANTS } from "../../../tokens";
|
|
6
7
|
const DEFAULT_LABELS = {
|
|
7
8
|
label: "Address Label",
|
|
@@ -33,6 +34,7 @@ export function AddressForm({ initialData, onSubmit, onCancel, isLoading = false
|
|
|
33
34
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
34
35
|
const mergedPlaceholders = { ...DEFAULT_PLACEHOLDERS, ...placeholders };
|
|
35
36
|
const effectiveSubmitLabel = submitLabel ?? mergedLabels.save;
|
|
37
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
36
38
|
const [formData, setFormData] = useState({
|
|
37
39
|
label: initialData?.label || "",
|
|
38
40
|
fullName: initialData?.fullName || "",
|
|
@@ -47,10 +49,30 @@ export function AddressForm({ initialData, onSubmit, onCancel, isLoading = false
|
|
|
47
49
|
});
|
|
48
50
|
const handleChange = (field, value) => {
|
|
49
51
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
52
|
+
if (typeof value === "string" && value.trim())
|
|
53
|
+
setFieldError(field, null);
|
|
50
54
|
};
|
|
51
55
|
const handleSubmit = async (e) => {
|
|
52
56
|
e.preventDefault();
|
|
57
|
+
clearErrors();
|
|
58
|
+
const errs = {};
|
|
59
|
+
if (!formData.fullName.trim())
|
|
60
|
+
errs["fullName"] = "Full name is required";
|
|
61
|
+
if (!formData.phone.trim())
|
|
62
|
+
errs["phone"] = "Phone number is required";
|
|
63
|
+
if (!formData.addressLine1.trim())
|
|
64
|
+
errs["addressLine1"] = "Address is required";
|
|
65
|
+
if (!formData.city.trim())
|
|
66
|
+
errs["city"] = "City is required";
|
|
67
|
+
if (!formData.state.trim())
|
|
68
|
+
errs["state"] = "State is required";
|
|
69
|
+
if (!formData.postalCode.trim())
|
|
70
|
+
errs["postalCode"] = "Postal code is required";
|
|
71
|
+
if (Object.keys(errs).length > 0) {
|
|
72
|
+
Object.entries(errs).forEach(([k, v]) => setFieldError(k, v));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
53
75
|
await onSubmit(formData);
|
|
54
76
|
};
|
|
55
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsx(FormField, { label: mergedLabels.label, name: "label", type: "text", value: formData.label, onChange: (value) => handleChange("label", value), placeholder: mergedPlaceholders.label, required: true }), _jsx(FormField, { label: mergedLabels.fullName, name: "fullName", type: "text", value: formData.fullName, onChange: (value) => handleChange("fullName", value), placeholder: mergedPlaceholders.fullName, required: true }), _jsx(FormField, { label: mergedLabels.phone, name: "phone", type: "tel", value: formData.phone, onChange: (value) => handleChange("phone", value), placeholder: mergedPlaceholders.phone, required: true }), _jsx(FormField, { label: mergedLabels.addressLine1, name: "addressLine1", type: "text", value: formData.addressLine1, onChange: (value) => handleChange("addressLine1", value), placeholder: mergedPlaceholders.addressLine1, required: true }), _jsx(FormField, { label: mergedLabels.addressLine2, name: "addressLine2", type: "text", value: formData.addressLine2, onChange: (value) => handleChange("addressLine2", value), placeholder: mergedPlaceholders.addressLine2 }), _jsxs(FormGroup, { columns: 3, children: [_jsx(FormField, { label: mergedLabels.city, name: "city", type: "text", value: formData.city, onChange: (value) => handleChange("city", value), placeholder: mergedPlaceholders.city, required: true }), _jsx(FormField, { label: mergedLabels.state, name: "state", type: "text", value: formData.state, onChange: (value) => handleChange("state", value), placeholder: mergedPlaceholders.state, required: true }), _jsx(FormField, { label: mergedLabels.postalCode, name: "postalCode", type: "text", value: formData.postalCode, onChange: (value) => handleChange("postalCode", value), placeholder: mergedPlaceholders.postalCode, required: true })] }), _jsx(FormField, { label: mergedLabels.country, name: "country", type: "text", value: formData.country, onChange: (value) => handleChange("country", value), placeholder: mergedPlaceholders.country, required: true }), _jsx(Checkbox, { checked: formData.isDefault, onChange: (e) => handleChange("isDefault", e.target.checked), label: mergedLabels.setDefault }), _jsxs("div", { className: `flex items-center justify-start ${THEME_CONSTANTS.spacing.gap.xs} pt-2`, "data-section": "addressform-div-223", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: isLoading, children: mergedLabels.cancel }), _jsx(Button, { type: "submit", variant: "primary", disabled: isLoading, children: isLoading ? mergedLabels.loading : effectiveSubmitLabel })] })] }));
|
|
77
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsx(FormField, { label: mergedLabels.label, name: "label", type: "text", value: formData.label, onChange: (value) => handleChange("label", value), placeholder: mergedPlaceholders.label, required: true }), _jsx(FormField, { label: mergedLabels.fullName, name: "fullName", type: "text", value: formData.fullName, onChange: (value) => handleChange("fullName", value), placeholder: mergedPlaceholders.fullName, required: true, error: shellCtx.errors["fullName"] }), _jsx(FormField, { label: mergedLabels.phone, name: "phone", type: "tel", value: formData.phone, onChange: (value) => handleChange("phone", value), placeholder: mergedPlaceholders.phone, required: true, error: shellCtx.errors["phone"] }), _jsx(FormField, { label: mergedLabels.addressLine1, name: "addressLine1", type: "text", value: formData.addressLine1, onChange: (value) => handleChange("addressLine1", value), placeholder: mergedPlaceholders.addressLine1, required: true, error: shellCtx.errors["addressLine1"] }), _jsx(FormField, { label: mergedLabels.addressLine2, name: "addressLine2", type: "text", value: formData.addressLine2, onChange: (value) => handleChange("addressLine2", value), placeholder: mergedPlaceholders.addressLine2 }), _jsxs(FormGroup, { columns: 3, children: [_jsx(FormField, { label: mergedLabels.city, name: "city", type: "text", value: formData.city, onChange: (value) => handleChange("city", value), placeholder: mergedPlaceholders.city, required: true, error: shellCtx.errors["city"] }), _jsx(FormField, { label: mergedLabels.state, name: "state", type: "text", value: formData.state, onChange: (value) => handleChange("state", value), placeholder: mergedPlaceholders.state, required: true, error: shellCtx.errors["state"] }), _jsx(FormField, { label: mergedLabels.postalCode, name: "postalCode", type: "text", value: formData.postalCode, onChange: (value) => handleChange("postalCode", value), placeholder: mergedPlaceholders.postalCode, required: true, error: shellCtx.errors["postalCode"] })] }), _jsx(FormField, { label: mergedLabels.country, name: "country", type: "text", value: formData.country, onChange: (value) => handleChange("country", value), placeholder: mergedPlaceholders.country, required: true }), _jsx(Checkbox, { checked: formData.isDefault, onChange: (e) => handleChange("isDefault", e.target.checked), label: mergedLabels.setDefault }), _jsxs("div", { className: `flex items-center justify-start ${THEME_CONSTANTS.spacing.gap.xs} pt-2`, "data-section": "addressform-div-223", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: isLoading, children: mergedLabels.cancel }), _jsx(Button, { type: "submit", variant: "primary", disabled: isLoading, children: isLoading ? mergedLabels.loading : effectiveSubmitLabel })] })] }));
|
|
56
78
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, RichTextEditor, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
8
|
import { useMediaUpload } from "../../media";
|
|
8
9
|
import { apiClient } from "../../../http";
|
|
@@ -57,6 +58,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
57
58
|
const [metaDescription, setMetaDescription] = React.useState("");
|
|
58
59
|
const { showToast } = useToast();
|
|
59
60
|
const { upload } = useMediaUpload();
|
|
61
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
60
62
|
// --- load existing post (edit mode) ---
|
|
61
63
|
const postQuery = useQuery({
|
|
62
64
|
queryKey: ["admin", "blog", postId],
|
|
@@ -146,17 +148,22 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
146
148
|
});
|
|
147
149
|
const isSubmitting = saveMutation.isPending || postQuery.isLoading;
|
|
148
150
|
const canSave = Boolean(title);
|
|
149
|
-
const formSection = (_jsxs(Form, { onSubmit: (e) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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") }));
|
|
160
167
|
if (embedded) {
|
|
161
168
|
return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
|
|
162
169
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, StackedViewShell, Toggle, useToast, } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
8
|
import { useMediaUpload } from "../../media";
|
|
8
9
|
import { apiClient } from "../../../http";
|
|
@@ -23,6 +24,7 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
|
|
|
23
24
|
const [isActive, setIsActive] = React.useState(true);
|
|
24
25
|
const [displayOrder, setDisplayOrder] = React.useState("");
|
|
25
26
|
const { showToast } = useToast();
|
|
27
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
26
28
|
const brandQuery = useQuery({
|
|
27
29
|
queryKey: ["admin", "brand", brandId],
|
|
28
30
|
queryFn: async () => {
|
|
@@ -88,17 +90,22 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
|
|
|
88
90
|
});
|
|
89
91
|
const { upload } = useMediaUpload();
|
|
90
92
|
const isSubmitting = saveMutation.isPending || brandQuery.isLoading;
|
|
91
|
-
const formSection = (_jsxs(Form, { onSubmit: (e) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
const formSection = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
clearErrors();
|
|
96
|
+
if (!name.trim()) {
|
|
97
|
+
setFieldError("name", "Brand name is required");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
saveMutation.mutate();
|
|
101
|
+
}, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Brand name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Hot Wheels" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
|
|
102
|
+
setSlug(e.target.value);
|
|
103
|
+
setSlugManual(true);
|
|
104
|
+
}, placeholder: "brand-hot-wheels", helperText: "Auto-generated from name. Must start with 'brand-'." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description of the brand" }), _jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(ImageUpload, { label: "Logo", currentImage: logoURL, onUpload: (file) => upload(file, "brands", true, { type: "brand-logo", brand: name || slug }), onChange: setLogoURL }), _jsx(ImageUpload, { label: "Banner", currentImage: bannerURL, onUpload: (file) => upload(file, "brands", true, { type: "brand-banner", brand: name || slug }), onChange: setBannerURL })] }), _jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(Input, { label: "Website", value: website, onChange: (e) => setWebsite(e.target.value), placeholder: "https://brand.com", type: "url" }), _jsx(Input, { label: "Display order", value: displayOrder, onChange: (e) => setDisplayOrder(e.target.value), type: "number", min: 0, placeholder: "0" })] }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create brand" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
105
|
+
if (confirm("Delete this brand? This cannot be undone.")) {
|
|
106
|
+
deleteMutation.mutate();
|
|
107
|
+
}
|
|
108
|
+
}, children: "Delete brand" }))] })] }, "brand-form") }));
|
|
102
109
|
if (embedded) {
|
|
103
110
|
return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
|
|
104
111
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Alert, Button, Form, FormActions, Input, Select, StackedViewShell, Toggle, Div, Text, Heading, Row, } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { apiClient } from "../../../http";
|
|
7
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
9
|
const CLS_PANEL = "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 space-y-3";
|
|
@@ -103,6 +104,7 @@ export function AdminCarouselEditorView({ slideId, onSaved, onDeleted, onCancel,
|
|
|
103
104
|
const [overlayBtnNewTab, setOverlayBtnNewTab] = React.useState(false);
|
|
104
105
|
const [errorMsg, setErrorMsg] = React.useState("");
|
|
105
106
|
const [successMsg, setSuccessMsg] = React.useState("");
|
|
107
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
106
108
|
const slideQuery = useQuery({
|
|
107
109
|
queryKey: ["admin", "carousel", slideId],
|
|
108
110
|
queryFn: async () => {
|
|
@@ -195,13 +197,21 @@ export function AdminCarouselEditorView({ slideId, onSaved, onDeleted, onCancel,
|
|
|
195
197
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Carousel Slide" : "New Carousel Slide", sections: [
|
|
196
198
|
errorMsg ? _jsx(Alert, { variant: "error", children: errorMsg }, "err") : null,
|
|
197
199
|
successMsg ? _jsx(Alert, { variant: "success", children: successMsg }, "ok") : null,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
clearErrors();
|
|
203
|
+
if (!title.trim()) {
|
|
204
|
+
setFieldError("title", "Title is required");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
saveMutation.mutate();
|
|
208
|
+
}, className: "space-y-6", children: [_jsxs(Div, { className: CLS_PANEL, children: [_jsx(Heading, { level: 3, className: CLS_SECTION_HEADING, children: "Slide info" }), _jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => setTitle(v), required: true, placeholder: "e.g. Hot Wheels RLC Exclusives" }), _jsx(Toggle, { label: "Active (visible on homepage)", checked: active, onChange: setActive }), _jsx(Input, { label: "Display order", type: "number", value: order, onChange: (e) => setOrder(e.target.value), min: 0, placeholder: "1" }), _jsx(Select, { label: "Slide height", value: height, onChange: (e) => setHeight(e.target.value), options: HEIGHT_OPTIONS }), _jsx(Input, { label: "Autoplay delay (ms)", type: "number", value: autoplayDelayMs, onChange: (e) => setAutoplayDelayMs(e.target.value), min: 1000, max: 15000, placeholder: "4000", helperText: "How long this slide shows before auto-advancing. Default: 4000 ms." })] }), _jsxs(Div, { className: CLS_PANEL, children: [_jsx(Heading, { level: 3, className: CLS_SECTION_HEADING, children: "Background" }), _jsx(BackgroundEditor, { value: background, onChange: setBackground, prefix: "slide" })] }), _jsxs(Div, { className: CLS_PANEL, children: [_jsx(Heading, { level: 3, className: CLS_SECTION_HEADING, children: "Overlay text (optional)" }), _jsx(Text, { className: "text-sm text-zinc-500", children: "Centred text layered over the background. Leave blank to use cards only." }), _jsx(Input, { label: "Heading", value: overlayTitle, onChange: (e) => setOverlayTitle(e.target.value), placeholder: "India's #1 Collectibles Marketplace" }), _jsx(Input, { label: "Subtitle", value: overlaySubtitle, onChange: (e) => setOverlaySubtitle(e.target.value), placeholder: "Pok\u00E9mon TCG \u00B7 Hot Wheels \u00B7 Beyblade X" }), _jsx(Input, { label: "Description", value: overlayDesc, onChange: (e) => setOverlayDesc(e.target.value), placeholder: "One sentence description..." }), _jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-800 p-3 space-y-2", children: [_jsx(Text, { className: "text-sm font-medium", children: "CTA button" }), _jsx(Input, { label: "Button text", value: overlayBtnText, onChange: (e) => setOverlayBtnText(e.target.value), placeholder: "Shop Now" }), _jsx(Input, { label: "Button link", value: overlayBtnLink, onChange: (e) => setOverlayBtnLink(e.target.value), placeholder: "/products" }), _jsx(Select, { label: "Variant", value: overlayBtnVariant, onChange: (e) => setOverlayBtnVariant(e.target.value), options: VARIANT_OPTIONS.filter((v) => ["primary", "secondary", "outline"].includes(v.value)) }), _jsx(Toggle, { label: "Open in new tab", checked: overlayBtnNewTab, onChange: setOverlayBtnNewTab })] })] }), _jsxs(Div, { className: CLS_PANEL, children: [_jsxs(Row, { className: CLS_ROW_BETWEEN, children: [_jsx(Heading, { level: 3, className: CLS_SECTION_HEADING, children: "Cards (0\u20132)" }), cards.length < 2 && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
209
|
+
const usedZones = cards.map((c) => c.zone);
|
|
210
|
+
const freeZone = [1, 2, 3, 4, 5, 6].find((z) => !usedZones.includes(z)) ?? 1;
|
|
211
|
+
setCards([...cards, makeCard(freeZone)]);
|
|
212
|
+
}, children: "+ Add card" }))] }), cards.length === 0 && (_jsx(Text, { className: "text-sm text-zinc-400", children: "No cards \u2014 the overlay text covers the whole slide." })), cards.map((card, i) => (_jsx(CardEditor, { card: card, index: i, otherZones: occupiedZones.filter((z) => z !== card.zone), onChange: (updated) => setCards(cards.map((c, ci) => (ci === i ? updated : c))), onRemove: () => setCards(cards.filter((_, ci) => ci !== i)) }, card.id)))] }), _jsxs(FormActions, { children: [_jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !title || saveMutation.isPending, children: isEdit ? "Save changes" : "Create slide" }), onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: "Cancel" })), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
213
|
+
if (confirm("Delete this slide? This cannot be undone."))
|
|
214
|
+
deleteMutation.mutate();
|
|
215
|
+
}, children: "Delete slide" }))] })] }, "slide-form") }),
|
|
206
216
|
] }));
|
|
207
217
|
}
|
|
@@ -6,6 +6,7 @@ import { Button, Card, CardBody, ConfirmDeleteModal, Form, InlineCreateSelect, I
|
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
8
|
import { CategoryQuickCreateForm } from "./CategoryQuickCreateForm";
|
|
9
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
9
10
|
async function loadCategoryOptions(query, page) {
|
|
10
11
|
const params = new URLSearchParams({ page: String(page), pageSize: "25" });
|
|
11
12
|
if (query)
|
|
@@ -38,6 +39,7 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
|
|
|
38
39
|
const [isActive, setIsActive] = React.useState(true);
|
|
39
40
|
const [showInMenu, setShowInMenu] = React.useState(true);
|
|
40
41
|
const { showToast } = useToast();
|
|
42
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
41
43
|
const categoryQuery = useQuery({
|
|
42
44
|
queryKey: ["admin", "category", categoryId],
|
|
43
45
|
queryFn: async () => {
|
|
@@ -101,17 +103,22 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
|
|
|
101
103
|
});
|
|
102
104
|
const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
|
|
103
105
|
const actionSidebar = (_jsxs(Card, { variant: "outlined", padding: "md", className: "space-y-3", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: isEdit ? (isActive ? "Active" : "Inactive") : "New" }), _jsx(Button, { type: "submit", form: "category-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete category" }))] }));
|
|
104
|
-
const formContent = (_jsxs(Form, { id: "category-editor-form", onSubmit: (e) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
const formContent = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { id: "category-editor-form", onSubmit: (e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
clearErrors();
|
|
109
|
+
if (!name.trim()) {
|
|
110
|
+
setFieldError("name", "Category name is required");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
saveMutation.mutate();
|
|
114
|
+
}, className: "space-y-6", children: [_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Identity" }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Toys & Games" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
|
|
115
|
+
setSlug(e.target.value);
|
|
116
|
+
setSlugManual(true);
|
|
117
|
+
}, placeholder: "toys-and-games", helperText: "Auto-generated from name. Used in URLs." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description of the category" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Parent category" }), _jsx(InlineCreateSelect, { value: parentId || null, onChange: (v) => setParentId(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026 (leave empty for root)", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: "Parent category", createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, n) => { setParentId(id); onCreated({ value: id, label: n }); }, onCancel: onCancel })) }), _jsx(Text, { className: "text-xs text-neutral-500 dark:text-neutral-400", children: "Leave empty to create a root category." })] })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Display" }), _jsxs("div", { className: "space-y-4", children: [_jsx(Input, { label: "Display order", value: order, onChange: (e) => setOrder(e.target.value), type: "number", min: 0, placeholder: "0" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsx(Toggle, { label: "Show in menu", checked: showInMenu, onChange: setShowInMenu })] })] }), _jsxs("div", { className: "flex gap-3 lg:hidden", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
118
|
+
if (confirm("Delete this category? Products in this category will become uncategorized.")) {
|
|
119
|
+
deleteMutation.mutate();
|
|
120
|
+
}
|
|
121
|
+
}, children: "Delete category" }))] })] }, "cat-form") }));
|
|
115
122
|
if (embedded) {
|
|
116
123
|
return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
|
|
117
124
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, Select, StackedViewShell, Toggle, useToast, } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { apiClient } from "../../../http";
|
|
7
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
9
|
const TYPE_OPTIONS = [
|
|
@@ -60,6 +61,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
60
61
|
const [combinable, setCombinable] = React.useState(false);
|
|
61
62
|
const [appliesToAuctions, setAppliesToAuctions] = React.useState(false);
|
|
62
63
|
const { showToast } = useToast();
|
|
64
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
63
65
|
// --- load existing data (edit mode) ---
|
|
64
66
|
const couponQuery = useQuery({
|
|
65
67
|
queryKey: ["admin", "coupon", couponId],
|
|
@@ -169,17 +171,22 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
169
171
|
: type === "fixed"
|
|
170
172
|
? "Discount amount (paise)"
|
|
171
173
|
: "Discount value";
|
|
172
|
-
const formSection = (_jsxs(Form, { onSubmit: (e) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
const formSection = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
clearErrors();
|
|
177
|
+
if (!name.trim()) {
|
|
178
|
+
setFieldError("name", "Campaign name is required");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
saveMutation.mutate();
|
|
182
|
+
}, className: "space-y-5", children: [_jsx(Select, { label: "Coupon type", options: TYPE_OPTIONS, value: type, onValueChange: (v) => setType(v), required: true }), _jsx(FieldInput, { name: "name", label: "Campaign name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Summer Sale 20%" }), !isEdit && (_jsx(Input, { label: "Coupon code", value: code, onChange: (e) => {
|
|
183
|
+
setCode(toCouponCode(e.target.value));
|
|
184
|
+
setCodeManual(true);
|
|
185
|
+
}, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "Allow stacking with seller coupons", checked: combinable, onChange: setCombinable }), _jsx(Toggle, { label: "Applies to auctions", checked: appliesToAuctions, onChange: setAppliesToAuctions }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !canSave || isSubmitting, children: isEdit ? "Save changes" : "Create coupon" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
186
|
+
if (confirm("Delete this coupon? This cannot be undone.")) {
|
|
187
|
+
deleteMutation.mutate();
|
|
188
|
+
}
|
|
189
|
+
}, children: "Delete coupon" }))] })] }, "coupon-form") }));
|
|
183
190
|
if (embedded) {
|
|
184
191
|
return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
|
|
185
192
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, RichTextEditor, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { apiClient } from "../../../http";
|
|
7
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
9
|
// --- Constants ---------------------------------------------------------------
|
|
@@ -38,6 +39,7 @@ export function AdminFaqEditorView({ faqId, onSaved, onDeleted, embedded, ...res
|
|
|
38
39
|
const [showOnHomepage, setShowOnHomepage] = React.useState(false);
|
|
39
40
|
const [showInFooter, setShowInFooter] = React.useState(false);
|
|
40
41
|
const { showToast } = useToast();
|
|
42
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
41
43
|
// --- load existing FAQ (edit mode) ---
|
|
42
44
|
const faqQuery = useQuery({
|
|
43
45
|
queryKey: ["admin", "faqs", faqId],
|
|
@@ -112,17 +114,22 @@ export function AdminFaqEditorView({ faqId, onSaved, onDeleted, embedded, ...res
|
|
|
112
114
|
});
|
|
113
115
|
const isSubmitting = saveMutation.isPending || faqQuery.isLoading;
|
|
114
116
|
const canSave = Boolean(question.trim()) && Boolean(answer.trim());
|
|
115
|
-
const formSection = (_jsxs(Form, { onSubmit: (e) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
const formSection = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
clearErrors();
|
|
120
|
+
if (!question.trim()) {
|
|
121
|
+
setFieldError("question", "Question is required");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
saveMutation.mutate();
|
|
125
|
+
}, className: "space-y-5", children: [_jsx(FieldInput, { name: "question", label: "Question", value: question, onChange: (v) => handleQuestionChange(v), required: true, placeholder: "e.g. How does bidding work on LetItRip?" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
|
|
126
|
+
setSlug(e.target.value);
|
|
127
|
+
setSlugManual(true);
|
|
128
|
+
}, placeholder: "faq-how-does-bidding-work", helperText: "Auto-generated from question. Must start with 'faq-'." }), _jsxs("div", { className: "space-y-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Answer" }), _jsx(RichTextEditor, { value: answer, onChange: setAnswer, placeholder: "Write a clear, helpful answer...", minHeightClassName: "min-h-[200px]" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: category, onValueChange: setCategory }), _jsx(Input, { label: "Display order", value: String(order), onChange: (e) => setOrder(parseInt(e.target.value, 10) || 0), type: "number", min: 0, helperText: "Lower = shown first within category." })] }), _jsx(Input, { label: "Priority", value: String(priority), onChange: (e) => setPriority(parseInt(e.target.value, 10) || 0), type: "number", min: 0, helperText: "Higher priority FAQs appear first in search results." }), _jsx(TagInput, { label: "Tags", value: tags, onChange: setTags, placeholder: "e.g. shipping, pokemon, returns" }), _jsxs("div", { className: "space-y-3 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Visibility" }), _jsx(Toggle, { label: "Active (visible to users)", checked: isActive, onChange: setIsActive }), _jsx(Toggle, { label: "Pinned (always shown at top)", checked: isPinned, onChange: setIsPinned }), _jsx(Toggle, { label: "Show on homepage FAQ section", checked: showOnHomepage, onChange: setShowOnHomepage }), _jsx(Toggle, { label: "Show in footer FAQ links", checked: showInFooter, onChange: setShowInFooter })] }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !canSave || isSubmitting, children: isEdit ? "Save changes" : "Create FAQ" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
129
|
+
if (confirm("Delete this FAQ? This cannot be undone.")) {
|
|
130
|
+
deleteMutation.mutate();
|
|
131
|
+
}
|
|
132
|
+
}, children: "Delete FAQ" }))] })] }, "faq-form") }));
|
|
126
133
|
if (embedded) {
|
|
127
134
|
return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
|
|
128
135
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, StackedViewShell, useToast, } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
8
|
import { useMediaUpload } from "../../media";
|
|
8
9
|
import { apiClient } from "../../../http";
|
|
@@ -67,17 +68,23 @@ export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDelet
|
|
|
67
68
|
});
|
|
68
69
|
const { upload } = useMediaUpload();
|
|
69
70
|
const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
|
|
71
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
70
72
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Sub-listing Category" : "New Sub-listing Category", sections: [
|
|
71
|
-
_jsxs(Form, { onSubmit: (e) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
clearErrors();
|
|
76
|
+
if (!name.trim()) {
|
|
77
|
+
setFieldError("name", "Category name is required");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
saveMutation.mutate();
|
|
81
|
+
}, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Base Set Charizard 108/120" }), _jsx(Input, { label: "Item code", value: itemCode, onChange: (e) => setItemCode(e.target.value), placeholder: "e.g. PSA 10, 108/120, STH", helperText: "Grade, card number, or series code. Optional." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description shown on the public category page" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "sublisting-categories", true, {
|
|
82
|
+
type: "category-image",
|
|
83
|
+
name: name || "sublisting",
|
|
84
|
+
}), onChange: setCoverImage }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
85
|
+
if (confirm("Delete this category? All linked listings will be unlinked. This cannot be undone.")) {
|
|
86
|
+
deleteMutation.mutate();
|
|
87
|
+
}
|
|
88
|
+
}, children: "Delete" }))] })] }, "sc-editor-form") }, "sc-ctx"),
|
|
82
89
|
] }));
|
|
83
90
|
}
|
|
@@ -23,7 +23,10 @@ export async function getUserSessions(userId) {
|
|
|
23
23
|
return sessionRepository.findAllByUser(userId);
|
|
24
24
|
}
|
|
25
25
|
export async function getPublicUserProfile(userId) {
|
|
26
|
-
|
|
26
|
+
let user = await userRepository.findById(userId);
|
|
27
|
+
// Fallback: userId may be a Firebase Auth UID rather than the document slug
|
|
28
|
+
if (!user)
|
|
29
|
+
user = await userRepository.findByUid(userId);
|
|
27
30
|
if (!user)
|
|
28
31
|
return null;
|
|
29
32
|
return {
|
|
@@ -10,6 +10,7 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
|
|
|
10
10
|
protected mapDoc<D = UserDocument>(snap: DocumentSnapshot): D;
|
|
11
11
|
createWithId(id: string, data: Partial<UserDocument>): Promise<UserDocument>;
|
|
12
12
|
create(input: Omit<UserDocument, "id" | "createdAt" | "updatedAt">): Promise<UserDocument>;
|
|
13
|
+
findByUid(uid: string): Promise<UserDocument | null>;
|
|
13
14
|
findByEmail(email: string): Promise<UserDocument | null>;
|
|
14
15
|
findByPhone(phoneNumber: string): Promise<UserDocument | null>;
|
|
15
16
|
findByRole(role: UserRole): Promise<UserDocument[]>;
|
|
@@ -58,6 +58,9 @@ export class UserRepository extends BaseRepository {
|
|
|
58
58
|
.set(prepareForFirestore(encrypted));
|
|
59
59
|
return { id, ...userData };
|
|
60
60
|
}
|
|
61
|
+
async findByUid(uid) {
|
|
62
|
+
return this.findOneBy(USER_FIELDS.UID, uid);
|
|
63
|
+
}
|
|
61
64
|
async findByEmail(email) {
|
|
62
65
|
return this.findOneBy(USER_FIELDS.EMAIL_INDEX, piiBlindIndex(email));
|
|
63
66
|
}
|
|
@@ -29,6 +29,9 @@ export async function GET(request) {
|
|
|
29
29
|
const category = param(url, "category");
|
|
30
30
|
if (category)
|
|
31
31
|
parts.push(`category==${category}`);
|
|
32
|
+
const tags = param(url, "tags");
|
|
33
|
+
if (tags)
|
|
34
|
+
parts.push(`tags@=${tags}`);
|
|
32
35
|
const q = param(url, "q");
|
|
33
36
|
if (q)
|
|
34
37
|
parts.push(`title@=*${q}`);
|
|
@@ -94,11 +94,13 @@ export function BlogIndexListing({ initialData }) {
|
|
|
94
94
|
}, [table]);
|
|
95
95
|
const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
|
|
96
96
|
const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
|
|
97
|
+
!!table.get(TABLE_KEYS.TAGS) ||
|
|
97
98
|
table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
|
|
98
99
|
activeFilterCount > 0;
|
|
99
100
|
const params = {
|
|
100
101
|
q: table.get(TABLE_KEYS.QUERY) || undefined,
|
|
101
102
|
category: (table.get(TABLE_KEYS.CATEGORY) || undefined),
|
|
103
|
+
tags: table.get(TABLE_KEYS.TAGS) || undefined,
|
|
102
104
|
sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
|
|
103
105
|
page: table.getNumber(TABLE_KEYS.PAGE, 1),
|
|
104
106
|
perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, PAGE_SIZE),
|