@mohasinac/appkit 2.4.3 → 2.4.5
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/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/admin/components/AdminSublistingCategoriesView.d.ts +1 -0
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +56 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.d.ts +7 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +83 -0
- package/dist/features/admin/components/index.d.ts +3 -0
- package/dist/features/admin/components/index.js +2 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +9 -1
- package/dist/features/auctions/schemas/index.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
- package/dist/features/products/components/CustomFieldsEditor.d.ts +8 -0
- package/dist/features/products/components/CustomFieldsEditor.js +33 -0
- package/dist/features/products/components/CustomSectionTabContent.d.ts +4 -0
- package/dist/features/products/components/CustomSectionTabContent.js +13 -0
- package/dist/features/products/components/CustomSectionsEditor.d.ts +6 -0
- package/dist/features/products/components/CustomSectionsEditor.js +27 -0
- package/dist/features/products/components/ProductDetailPageView.js +9 -1
- package/dist/features/products/components/ProductForm.js +4 -2
- package/dist/features/products/components/ProductTabsShell.d.ts +8 -1
- package/dist/features/products/components/ProductTabsShell.js +19 -9
- package/dist/features/products/components/SublistingCarouselSection.d.ts +6 -0
- package/dist/features/products/components/SublistingCarouselSection.js +53 -0
- package/dist/features/products/components/SublistingCategorySelect.d.ts +7 -0
- package/dist/features/products/components/SublistingCategorySelect.js +40 -0
- package/dist/features/products/components/index.d.ts +6 -1
- package/dist/features/products/components/index.js +3 -0
- package/dist/features/products/repository/sublisting-categories.repository.d.ts +16 -0
- package/dist/features/products/repository/sublisting-categories.repository.js +126 -0
- package/dist/features/products/schemas/firestore.d.ts +20 -2
- package/dist/features/products/schemas/firestore.js +8 -0
- package/dist/features/products/schemas/index.d.ts +147 -56
- package/dist/features/products/schemas/index.js +24 -0
- package/dist/features/products/schemas/sublisting-categories.d.ts +45 -0
- package/dist/features/products/schemas/sublisting-categories.js +16 -0
- package/dist/features/products/types/index.d.ts +5 -0
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -1
- package/dist/next/routing/route-map.d.ts +6 -0
- package/dist/next/routing/route-map.js +3 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +1 -0
- package/dist/seed/sublisting-categories-seed-data.d.ts +5 -5
- package/dist/seed/sublisting-categories-seed-data.js +81 -237
- package/dist/styles.css +1 -0
- package/dist/tailwind-input.css +4 -0
- package/dist/tailwind-utilities.css +1 -0
- package/package.json +5 -4
|
@@ -113,6 +113,8 @@ export declare const ADMIN_ENDPOINTS: {
|
|
|
113
113
|
readonly NAVIGATION: "/api/admin/navigation";
|
|
114
114
|
readonly NAVIGATION_BY_ID: (id: string) => string;
|
|
115
115
|
readonly ADMIN_SITE: "/api/admin/site";
|
|
116
|
+
readonly SUBLISTING_CATEGORIES: "/api/admin/sublisting-categories";
|
|
117
|
+
readonly SUBLISTING_CATEGORY_BY_ID: (id: string) => string;
|
|
116
118
|
};
|
|
117
119
|
export declare const CHAT_ENDPOINTS: {
|
|
118
120
|
readonly LIST: "/api/chat";
|
|
@@ -375,6 +377,8 @@ export declare const API_ENDPOINTS: {
|
|
|
375
377
|
readonly NAVIGATION: "/api/admin/navigation";
|
|
376
378
|
readonly NAVIGATION_BY_ID: (id: string) => string;
|
|
377
379
|
readonly ADMIN_SITE: "/api/admin/site";
|
|
380
|
+
readonly SUBLISTING_CATEGORIES: "/api/admin/sublisting-categories";
|
|
381
|
+
readonly SUBLISTING_CATEGORY_BY_ID: (id: string) => string;
|
|
378
382
|
};
|
|
379
383
|
readonly CHAT: {
|
|
380
384
|
readonly LIST: "/api/chat";
|
|
@@ -639,6 +643,8 @@ export declare const API_ROUTES: {
|
|
|
639
643
|
readonly NAVIGATION: "/api/admin/navigation";
|
|
640
644
|
readonly NAVIGATION_BY_ID: (id: string) => string;
|
|
641
645
|
readonly ADMIN_SITE: "/api/admin/site";
|
|
646
|
+
readonly SUBLISTING_CATEGORIES: "/api/admin/sublisting-categories";
|
|
647
|
+
readonly SUBLISTING_CATEGORY_BY_ID: (id: string) => string;
|
|
642
648
|
};
|
|
643
649
|
readonly CHAT: {
|
|
644
650
|
readonly LIST: "/api/chat";
|
|
@@ -131,6 +131,8 @@ export const ADMIN_ENDPOINTS = {
|
|
|
131
131
|
NAVIGATION: "/api/admin/navigation",
|
|
132
132
|
NAVIGATION_BY_ID: (id) => `/api/admin/navigation/${id}`,
|
|
133
133
|
ADMIN_SITE: "/api/admin/site",
|
|
134
|
+
SUBLISTING_CATEGORIES: "/api/admin/sublisting-categories",
|
|
135
|
+
SUBLISTING_CATEGORY_BY_ID: (id) => `/api/admin/sublisting-categories/${id}`,
|
|
134
136
|
};
|
|
135
137
|
// ---------------------------------------------------------------------------
|
|
136
138
|
// Chat
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AdminSublistingCategoriesView(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
|
+
import { ListingToolbar, Pagination } from "../../../ui";
|
|
6
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
8
|
+
import { DataTable } from "./DataTable";
|
|
9
|
+
const PAGE_SIZE = 25;
|
|
10
|
+
const DEFAULT_SORT = "name";
|
|
11
|
+
const SORT_OPTIONS = [
|
|
12
|
+
{ value: "name", label: "Name A–Z" },
|
|
13
|
+
{ value: "-name", label: "Name Z–A" },
|
|
14
|
+
{ value: "-productCount", label: "Most listings" },
|
|
15
|
+
{ value: "-createdAt", label: "Newest" },
|
|
16
|
+
];
|
|
17
|
+
export function AdminSublistingCategoriesView() {
|
|
18
|
+
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
19
|
+
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
20
|
+
const commitSearch = useCallback(() => {
|
|
21
|
+
table.set("q", searchInput.trim());
|
|
22
|
+
table.setPage(1);
|
|
23
|
+
}, [searchInput, table]);
|
|
24
|
+
const resetAll = useCallback(() => {
|
|
25
|
+
table.setMany({ q: "", sort: "" });
|
|
26
|
+
setSearchInput("");
|
|
27
|
+
}, [table]);
|
|
28
|
+
const hasActiveState = !!table.get("q") || table.get("sort") !== DEFAULT_SORT;
|
|
29
|
+
const { rows, total, isLoading, errorMessage } = useAdminListingData({
|
|
30
|
+
queryKey: ["admin", "sublisting-categories", "listing"],
|
|
31
|
+
endpoint: ADMIN_ENDPOINTS.SUBLISTING_CATEGORIES,
|
|
32
|
+
page: table.getNumber("page", 1),
|
|
33
|
+
pageSize: PAGE_SIZE,
|
|
34
|
+
sorts: table.get("sort") || DEFAULT_SORT,
|
|
35
|
+
q: table.get("q") || undefined,
|
|
36
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
37
|
+
id: toStringValue(item.id, `sc-${index}`),
|
|
38
|
+
primary: toStringValue(item.name, "Untitled"),
|
|
39
|
+
secondary: [
|
|
40
|
+
item.itemCode ? `Code: ${item.itemCode}` : "",
|
|
41
|
+
`${typeof item.productCount === "number" ? item.productCount : 0} listing${item.productCount === 1 ? "" : "s"}`,
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(" · "),
|
|
45
|
+
status: "",
|
|
46
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
47
|
+
})),
|
|
48
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
49
|
+
});
|
|
50
|
+
const currentPage = table.getNumber("page", 1);
|
|
51
|
+
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
52
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { searchValue: searchInput, searchPlaceholder: "Search sub-listing categories\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => {
|
|
53
|
+
table.set("sort", v);
|
|
54
|
+
table.setPage(1);
|
|
55
|
+
}, hideViewToggle: true, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No sub-listing categories found" })] })] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StackedViewShellProps } from "../../../ui";
|
|
2
|
+
export interface AdminSublistingCategoryEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
3
|
+
categoryId?: string;
|
|
4
|
+
onSaved?: (id: string) => void;
|
|
5
|
+
onDeleted?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDeleted, ...rest }: AdminSublistingCategoryEditorViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { Button, Form, Input, StackedViewShell, useToast, } from "../../../ui";
|
|
6
|
+
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
|
+
import { useMediaUpload } from "../../media";
|
|
8
|
+
import { apiClient } from "../../../http";
|
|
9
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
+
export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDeleted, ...rest }) {
|
|
11
|
+
const isEdit = Boolean(categoryId);
|
|
12
|
+
const { showToast } = useToast();
|
|
13
|
+
const [name, setName] = React.useState("");
|
|
14
|
+
const [itemCode, setItemCode] = React.useState("");
|
|
15
|
+
const [description, setDescription] = React.useState("");
|
|
16
|
+
const [coverImage, setCoverImage] = React.useState("");
|
|
17
|
+
const categoryQuery = useQuery({
|
|
18
|
+
queryKey: ["admin", "sublisting-category", categoryId],
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.SUBLISTING_CATEGORY_BY_ID(categoryId));
|
|
21
|
+
return res?.data ?? res;
|
|
22
|
+
},
|
|
23
|
+
enabled: isEdit,
|
|
24
|
+
});
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
const cat = categoryQuery.data;
|
|
27
|
+
if (!cat)
|
|
28
|
+
return;
|
|
29
|
+
setName(cat.name ?? "");
|
|
30
|
+
setItemCode(cat.itemCode ?? "");
|
|
31
|
+
setDescription(cat.description ?? "");
|
|
32
|
+
setCoverImage(cat.coverImage ?? "");
|
|
33
|
+
}, [categoryQuery.data]);
|
|
34
|
+
const saveMutation = useMutation({
|
|
35
|
+
mutationFn: async () => {
|
|
36
|
+
const payload = {
|
|
37
|
+
name,
|
|
38
|
+
itemCode: itemCode || undefined,
|
|
39
|
+
description: description || undefined,
|
|
40
|
+
coverImage: coverImage || undefined,
|
|
41
|
+
};
|
|
42
|
+
if (isEdit) {
|
|
43
|
+
return apiClient.put(ADMIN_ENDPOINTS.SUBLISTING_CATEGORY_BY_ID(categoryId), payload);
|
|
44
|
+
}
|
|
45
|
+
return apiClient.post(ADMIN_ENDPOINTS.SUBLISTING_CATEGORIES, payload);
|
|
46
|
+
},
|
|
47
|
+
onSuccess: (res) => {
|
|
48
|
+
const id = res?.data?.id ?? res?.id ?? categoryId;
|
|
49
|
+
showToast(isEdit ? "Category updated." : "Category created.", "success");
|
|
50
|
+
if (onSaved && id)
|
|
51
|
+
onSaved(String(id));
|
|
52
|
+
},
|
|
53
|
+
onError: (err) => {
|
|
54
|
+
showToast(err?.message ?? "Failed to save category.", "error");
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const deleteMutation = useMutation({
|
|
58
|
+
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.SUBLISTING_CATEGORY_BY_ID(categoryId)),
|
|
59
|
+
onSuccess: () => {
|
|
60
|
+
showToast("Category deleted. All linked listings were unlinked.", "success");
|
|
61
|
+
if (onDeleted)
|
|
62
|
+
onDeleted();
|
|
63
|
+
},
|
|
64
|
+
onError: (err) => {
|
|
65
|
+
showToast(err?.message ?? "Failed to delete category.", "error");
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const { upload } = useMediaUpload();
|
|
69
|
+
const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
|
|
70
|
+
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Sub-listing Category" : "New Sub-listing Category", sections: [
|
|
71
|
+
_jsxs(Form, { onSubmit: (e) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
saveMutation.mutate();
|
|
74
|
+
}, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(Input, { label: "Category name", value: name, onChange: (e) => setName(e.target.value), 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, {
|
|
75
|
+
type: "category-image",
|
|
76
|
+
name: name || "sublisting",
|
|
77
|
+
}), 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: () => {
|
|
78
|
+
if (confirm("Delete this category? All linked listings will be unlinked. This cannot be undone.")) {
|
|
79
|
+
deleteMutation.mutate();
|
|
80
|
+
}
|
|
81
|
+
}, children: "Delete" }))] })] }, "sc-editor-form"),
|
|
82
|
+
] }));
|
|
83
|
+
}
|
|
@@ -28,6 +28,9 @@ export { AdminBrandsView } from "./AdminBrandsView";
|
|
|
28
28
|
export type { AdminBrandsViewProps } from "./AdminBrandsView";
|
|
29
29
|
export { AdminBrandEditorView } from "./AdminBrandEditorView";
|
|
30
30
|
export type { AdminBrandEditorViewProps } from "./AdminBrandEditorView";
|
|
31
|
+
export { AdminSublistingCategoriesView } from "./AdminSublistingCategoriesView";
|
|
32
|
+
export { AdminSublistingCategoryEditorView } from "./AdminSublistingCategoryEditorView";
|
|
33
|
+
export type { AdminSublistingCategoryEditorViewProps } from "./AdminSublistingCategoryEditorView";
|
|
31
34
|
export { AdminBlogView } from "./AdminBlogView";
|
|
32
35
|
export type { AdminBlogViewProps } from "./AdminBlogView";
|
|
33
36
|
export { AdminBlogEditorView } from "./AdminBlogEditorView";
|
|
@@ -14,6 +14,8 @@ export { AdminCategoriesView } from "./AdminCategoriesView";
|
|
|
14
14
|
export { AdminCategoryEditorView } from "./AdminCategoryEditorView";
|
|
15
15
|
export { AdminBrandsView } from "./AdminBrandsView";
|
|
16
16
|
export { AdminBrandEditorView } from "./AdminBrandEditorView";
|
|
17
|
+
export { AdminSublistingCategoriesView } from "./AdminSublistingCategoriesView";
|
|
18
|
+
export { AdminSublistingCategoryEditorView } from "./AdminSublistingCategoryEditorView";
|
|
17
19
|
export { AdminBlogView } from "./AdminBlogView";
|
|
18
20
|
export { AdminBlogEditorView } from "./AdminBlogEditorView";
|
|
19
21
|
export { AdminFaqsView } from "./AdminFaqsView";
|
|
@@ -10,6 +10,7 @@ import { safeDisplayName } from "../../../security";
|
|
|
10
10
|
import { Button, Container, Div, Heading, Input, Main, RichText, Row, Section, Span, Stack, Text, } from "../../../ui";
|
|
11
11
|
import { AuctionDetailView } from "../../products/components/AuctionDetailView";
|
|
12
12
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
13
|
+
import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
|
|
13
14
|
import { BuyBar } from "../../products/components/BuyBar";
|
|
14
15
|
import { RelatedProducts } from "../../products/components/RelatedProducts";
|
|
15
16
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
@@ -85,6 +86,9 @@ export async function AuctionDetailPageView({ id, onPlaceBid }) {
|
|
|
85
86
|
const specs = Array.isArray(p.specifications)
|
|
86
87
|
? p.specifications
|
|
87
88
|
: [];
|
|
89
|
+
const customSections = Array.isArray(p.customSections)
|
|
90
|
+
? p.customSections
|
|
91
|
+
: [];
|
|
88
92
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
89
93
|
const relatedDocs = await productRepository
|
|
90
94
|
.findByCategory(String(p.category ?? ""))
|
|
@@ -103,7 +107,11 @@ export async function AuctionDetailPageView({ id, onPlaceBid }) {
|
|
|
103
107
|
codAvailable: "Cash on Delivery",
|
|
104
108
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
105
109
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
106
|
-
} }), (categoryName || category || brand) && (_jsxs(Row, { align: "center", gap: "xs", className: "text-xs text-zinc-400 dark:text-zinc-500 flex-wrap", children: [category ? (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: categoryName || category })) : categoryName ? (_jsx(Span, { children: categoryName })) : null, (category || categoryName) && brand && _jsx(Span, { children: "\u203A" }), brand && (brandSlug ? (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "font-medium text-zinc-600 dark:text-zinc-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: brand })) : (_jsx(Span, { className: "font-medium text-zinc-600 dark:text-zinc-300", children: brand })))] })), features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this item" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Listed by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderBidForm: () => onPlaceBid ? (_jsx(Div, { id: "auction-bid-form", children: _jsx(PlaceBidFormClient, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid }) })) : (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsx(Div, { className: "lg:hidden", children: _jsx(PlaceBidFormClient, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid }) })) : !isEnded ? (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 lg:hidden", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined
|
|
110
|
+
} }), (categoryName || category || brand) && (_jsxs(Row, { align: "center", gap: "xs", className: "text-xs text-zinc-400 dark:text-zinc-500 flex-wrap", children: [category ? (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: categoryName || category })) : categoryName ? (_jsx(Span, { children: categoryName })) : null, (category || categoryName) && brand && _jsx(Span, { children: "\u203A" }), brand && (brandSlug ? (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "font-medium text-zinc-600 dark:text-zinc-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: brand })) : (_jsx(Span, { className: "font-medium text-zinc-600 dark:text-zinc-300", children: brand })))] })), features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this item" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Listed by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderBidForm: () => onPlaceBid ? (_jsx(Div, { id: "auction-bid-form", children: _jsx(PlaceBidFormClient, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid }) })) : (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsx(Div, { className: "lg:hidden", children: _jsx(PlaceBidFormClient, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid }) })) : !isEnded ? (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 lg:hidden", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
111
|
+
id: s.id,
|
|
112
|
+
label: s.title,
|
|
113
|
+
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
114
|
+
})) })), renderBidHistory: () => {
|
|
107
115
|
const bids = (bidsResult?.items ?? []).map((b) => ({
|
|
108
116
|
id: String(b.id ?? ""),
|
|
109
117
|
bidderId: String(b.userId ?? b.bidderId ?? ""),
|
|
@@ -69,9 +69,9 @@ export declare const auctionItemSchema: z.ZodObject<{
|
|
|
69
69
|
sellerId: string;
|
|
70
70
|
featured: boolean;
|
|
71
71
|
isAuction: true;
|
|
72
|
+
auctionEndDate: string;
|
|
72
73
|
startingBid: number;
|
|
73
74
|
bidCount: number;
|
|
74
|
-
auctionEndDate: string;
|
|
75
75
|
media?: {
|
|
76
76
|
url: string;
|
|
77
77
|
type: "video" | "image" | "file";
|
|
@@ -98,9 +98,9 @@ export declare const auctionItemSchema: z.ZodObject<{
|
|
|
98
98
|
sellerId: string;
|
|
99
99
|
featured: boolean;
|
|
100
100
|
isAuction: true;
|
|
101
|
+
auctionEndDate: string;
|
|
101
102
|
startingBid: number;
|
|
102
103
|
bidCount: number;
|
|
103
|
-
auctionEndDate: string;
|
|
104
104
|
media?: {
|
|
105
105
|
url: string;
|
|
106
106
|
type: "video" | "image" | "file";
|
|
@@ -10,6 +10,7 @@ import { Button, Container, Div, Heading, Main, RichText, Row, Section, Span, St
|
|
|
10
10
|
import { PreOrderDetailView } from "../../products/components/PreOrderDetailView";
|
|
11
11
|
import { BuyBar } from "../../products/components/BuyBar";
|
|
12
12
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell";
|
|
13
|
+
import { CustomSectionTabContent } from "../../products/components/CustomSectionTabContent";
|
|
13
14
|
import { PreOrderActionsClient } from "./PreOrderActionsClient";
|
|
14
15
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient";
|
|
15
16
|
import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges";
|
|
@@ -90,6 +91,9 @@ export async function PreOrderDetailPageView({ id, onReserveNow }) {
|
|
|
90
91
|
const specs = Array.isArray(p.specifications)
|
|
91
92
|
? p.specifications
|
|
92
93
|
: [];
|
|
94
|
+
const customSections = Array.isArray(p.customSections)
|
|
95
|
+
? p.customSections
|
|
96
|
+
: [];
|
|
93
97
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
94
98
|
return (_jsx(Main, { children: _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "hover:text-primary-600 transition-colors", children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 transition-colors", children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-1.5 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-indigo-100 dark:bg-indigo-900/30 px-2.5 py-0.5 text-xs font-semibold text-indigo-700 dark:text-indigo-300", children: "Pre-Order" }), productionStatus && (_jsx(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-600 dark:text-zinc-300", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), price !== null && (_jsx(Span, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) })), deliveryDate && (_jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-600 dark:text-zinc-400", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { className: "font-medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, labels: {
|
|
95
99
|
featured: "Featured",
|
|
@@ -105,7 +109,11 @@ export async function PreOrderDetailPageView({ id, onReserveNow }) {
|
|
|
105
109
|
codAvailable: "Cash on Delivery",
|
|
106
110
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
107
111
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
108
|
-
} }), (categoryName || category || brand) && (_jsxs(Row, { align: "center", gap: "xs", className: "text-xs text-zinc-400 dark:text-zinc-500 flex-wrap", children: [category ? (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: categoryName || category })) : (categoryName ? _jsx(Span, { children: categoryName }) : null), brand && (categoryName || category) && (_jsx(Span, { className: "text-zinc-300 dark:text-zinc-600", children: "\u00B7" })), brand && (brandSlug ? (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: brand })) : (_jsx(Span, { children: brand })))] })), features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined
|
|
112
|
+
} }), (categoryName || category || brand) && (_jsxs(Row, { align: "center", gap: "xs", className: "text-xs text-zinc-400 dark:text-zinc-500 flex-wrap", children: [category ? (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: categoryName || category })) : (categoryName ? _jsx(Span, { children: categoryName }) : null), brand && (categoryName || category) && (_jsx(Span, { className: "text-zinc-300 dark:text-zinc-600", children: "\u00B7" })), brand && (brandSlug ? (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: brand })) : (_jsx(Span, { children: brand })))] })), features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
113
|
+
id: s.id,
|
|
114
|
+
label: s.title,
|
|
115
|
+
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
116
|
+
})) })), renderBuyBar: () => (_jsxs(Div, { id: "pre-order-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [reserveTarget > 0 && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { className: "text-xs text-zinc-500", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { className: "text-xs font-semibold text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: "h-2 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700", children: _jsx(Div, { className: "h-full rounded-full bg-primary transition-all", style: { width: `${progressPct}%` } }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: String(product.id), price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "sm", className: "justify-center text-center", children: [
|
|
109
117
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
110
118
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
111
119
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CustomField } from "../schemas/firestore";
|
|
2
|
+
export interface CustomFieldsEditorProps {
|
|
3
|
+
fields: CustomField[];
|
|
4
|
+
onChange: (fields: CustomField[]) => void;
|
|
5
|
+
/** If true, adds `unit` input beside value. Default false. */
|
|
6
|
+
showUnit?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function CustomFieldsEditor({ fields, onChange, showUnit, }: CustomFieldsEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Div, Input, Row, Select, Stack, Text } from "../../../ui";
|
|
4
|
+
import { MAX_CUSTOM_FIELDS } from "../schemas/firestore";
|
|
5
|
+
const TYPE_OPTIONS = [
|
|
6
|
+
{ value: "text", label: "Text" },
|
|
7
|
+
{ value: "number", label: "Number" },
|
|
8
|
+
{ value: "boolean", label: "Yes / No" },
|
|
9
|
+
{ value: "url", label: "URL" },
|
|
10
|
+
];
|
|
11
|
+
const BOOL_OPTIONS = [
|
|
12
|
+
{ value: "", label: "— Select —" },
|
|
13
|
+
{ value: "true", label: "Yes" },
|
|
14
|
+
{ value: "false", label: "No" },
|
|
15
|
+
];
|
|
16
|
+
function emptyField() {
|
|
17
|
+
return { key: "", type: "text", value: "" };
|
|
18
|
+
}
|
|
19
|
+
export function CustomFieldsEditor({ fields, onChange, showUnit = false, }) {
|
|
20
|
+
function update(index, patch) {
|
|
21
|
+
const next = fields.map((f, i) => (i === index ? { ...f, ...patch } : f));
|
|
22
|
+
onChange(next);
|
|
23
|
+
}
|
|
24
|
+
function remove(index) {
|
|
25
|
+
onChange(fields.filter((_, i) => i !== index));
|
|
26
|
+
}
|
|
27
|
+
function add() {
|
|
28
|
+
if (fields.length >= MAX_CUSTOM_FIELDS)
|
|
29
|
+
return;
|
|
30
|
+
onChange([...fields, emptyField()]);
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs(Stack, { gap: "xs", children: [fields.map((field, i) => (_jsxs(Div, { className: "grid grid-cols-[1fr_140px_1fr_auto] gap-2 items-start", children: [_jsx(Input, { value: field.key, onChange: (e) => update(i, { key: e.target.value }), placeholder: "Field name", "aria-label": `Custom field ${i + 1} name` }), _jsx(Select, { options: TYPE_OPTIONS, value: field.type, onChange: (e) => update(i, { type: e.target.value, value: "" }), "aria-label": `Custom field ${i + 1} type` }), field.type === "boolean" ? (_jsx(Select, { options: BOOL_OPTIONS, value: field.value, onChange: (e) => update(i, { value: e.target.value }), "aria-label": `Custom field ${i + 1} value` })) : (_jsxs(Div, { className: showUnit ? "flex gap-1" : "", children: [_jsx(Input, { type: field.type === "number" ? "number" : "text", value: field.value, onChange: (e) => update(i, { value: e.target.value }), placeholder: field.type === "url" ? "https://" : "Value", "aria-label": `Custom field ${i + 1} value`, className: "flex-1" }), showUnit && (_jsx(Input, { value: field.unit ?? "", onChange: (e) => update(i, { unit: e.target.value || undefined }), placeholder: "Unit", "aria-label": `Custom field ${i + 1} unit`, className: "w-20 flex-shrink-0" }))] })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(i), "aria-label": `Remove field ${i + 1}`, className: "text-zinc-400 hover:text-red-500 dark:hover:text-red-400 px-2", children: "\u2715" })] }, i))), _jsxs(Row, { align: "center", justify: "between", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: add, disabled: fields.length >= MAX_CUSTOM_FIELDS, className: "text-primary-600 dark:text-primary-400", children: "+ Add field" }), fields.length > 0 && (_jsxs(Text, { className: "text-xs text-zinc-400", children: [fields.length, " / ", MAX_CUSTOM_FIELDS] }))] })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Div, RichText, Text } from "../../../ui";
|
|
3
|
+
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
4
|
+
function renderFieldValue(f) {
|
|
5
|
+
if (f.type === "boolean")
|
|
6
|
+
return f.value === "true" ? "Yes" : "No";
|
|
7
|
+
return f.value + (f.unit ? ` ${f.unit}` : "");
|
|
8
|
+
}
|
|
9
|
+
export function CustomSectionTabContent({ section }) {
|
|
10
|
+
const html = section.text ? normalizeRichTextHtml(section.text) : null;
|
|
11
|
+
const fields = section.fields?.filter((f) => f.key && f.value) ?? [];
|
|
12
|
+
return (_jsxs(Div, { className: "space-y-4", children: [html && (_jsx(RichText, { html: html, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })), fields.length > 0 && (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: fields.map((f, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300 capitalize", children: f.key }), _jsx("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: f.type === "url" ? (_jsx("a", { href: f.value, target: "_blank", rel: "noopener noreferrer", className: "text-primary-600 hover:underline dark:text-primary-400 break-all", children: f.value })) : (renderFieldValue(f)) })] }, i))) })), !html && fields.length === 0 && (_jsx(Text, { className: "text-sm text-zinc-400", children: "No content in this section." }))] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CustomSection } from "../schemas/firestore";
|
|
2
|
+
export interface CustomSectionsEditorProps {
|
|
3
|
+
sections: CustomSection[];
|
|
4
|
+
onChange: (sections: CustomSection[]) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function CustomSectionsEditor({ sections, onChange, }: CustomSectionsEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { Button, Div, Heading, Input, Stack, Text } from "../../../ui";
|
|
5
|
+
import { MAX_CUSTOM_SECTIONS } from "../schemas/firestore";
|
|
6
|
+
import { CustomFieldsEditor } from "./CustomFieldsEditor";
|
|
7
|
+
function generateSectionId() {
|
|
8
|
+
return `cs_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
9
|
+
}
|
|
10
|
+
function emptySection() {
|
|
11
|
+
return { id: generateSectionId(), title: "", fields: [] };
|
|
12
|
+
}
|
|
13
|
+
export function CustomSectionsEditor({ sections, onChange, }) {
|
|
14
|
+
const baseId = useId();
|
|
15
|
+
function update(index, patch) {
|
|
16
|
+
onChange(sections.map((s, i) => (i === index ? { ...s, ...patch } : s)));
|
|
17
|
+
}
|
|
18
|
+
function remove(index) {
|
|
19
|
+
onChange(sections.filter((_, i) => i !== index));
|
|
20
|
+
}
|
|
21
|
+
function add() {
|
|
22
|
+
if (sections.length >= MAX_CUSTOM_SECTIONS)
|
|
23
|
+
return;
|
|
24
|
+
onChange([...sections, emptySection()]);
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(Stack, { gap: "md", children: [sections.map((section, i) => (_jsxs(Div, { className: "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/40 p-4 space-y-3", children: [_jsxs(Div, { className: "flex items-center justify-between gap-2", children: [_jsxs(Heading, { level: 4, className: "text-sm font-semibold text-zinc-700 dark:text-zinc-200", children: ["Section ", i + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(i), className: "text-zinc-400 hover:text-red-500 dark:hover:text-red-400 text-xs", children: "Remove" })] }), _jsxs(Div, { children: [_jsxs("label", { htmlFor: `${baseId}-title-${i}`, className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Section title ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${baseId}-title-${i}`, value: section.title, onChange: (e) => update(i, { title: e.target.value }), placeholder: 'e.g. "Box Contents", "Compatibility", "Grading Details"', maxLength: 80 })] }), _jsxs(Div, { children: [_jsxs("label", { htmlFor: `${baseId}-text-${i}`, className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Body text", " ", _jsx("span", { className: "text-zinc-400 font-normal", children: "(optional)" })] }), _jsx("textarea", { id: `${baseId}-text-${i}`, value: section.text ?? "", onChange: (e) => update(i, { text: e.target.value || undefined }), placeholder: "Additional details for this section\u2026", rows: 3, maxLength: 2000, className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-800 dark:text-zinc-100 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500 resize-y" })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Fields", " ", _jsx("span", { className: "text-zinc-400 font-normal", children: "(optional)" })] }), _jsx(CustomFieldsEditor, { fields: section.fields ?? [], onChange: (fields) => update(i, { fields: fields.length > 0 ? fields : undefined }), showUnit: true })] })] }, section.id))), _jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: add, disabled: sections.length >= MAX_CUSTOM_SECTIONS, className: "text-primary-600 dark:text-primary-400", children: "+ Add section" }), _jsxs(Text, { className: "text-xs text-zinc-400", children: [sections.length, " / ", MAX_CUSTOM_SECTIONS, " sections"] })] })] }));
|
|
27
|
+
}
|
|
@@ -15,6 +15,7 @@ import { ProductFeatureBadges } from "./ProductFeatureBadges";
|
|
|
15
15
|
import { RelatedProductsCarousel } from "./RelatedProductsCarousel";
|
|
16
16
|
import { BuyBar } from "./BuyBar";
|
|
17
17
|
import { ShareButton } from "./ShareButton";
|
|
18
|
+
import { CustomSectionTabContent } from "./CustomSectionTabContent";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Helpers
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -130,6 +131,9 @@ export async function ProductDetailPageView({ slug, renderOfferAction, }) {
|
|
|
130
131
|
const specs = Array.isArray(p.specifications)
|
|
131
132
|
? p.specifications
|
|
132
133
|
: [];
|
|
134
|
+
const customSections = Array.isArray(p.customSections)
|
|
135
|
+
? p.customSections
|
|
136
|
+
: [];
|
|
133
137
|
const allowOffers = p.allowOffers === true;
|
|
134
138
|
const productType = typeof p.type === "string" ? p.type : "simple";
|
|
135
139
|
const minOfferPercent = typeof p.minOfferPercent === "number" ? p.minOfferPercent : 70;
|
|
@@ -206,5 +210,9 @@ export async function ProductDetailPageView({ slug, renderOfferAction, }) {
|
|
|
206
210
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
207
211
|
{ icon: "✓", label: "Verified\nSeller" },
|
|
208
212
|
{ icon: "⭐", label: "Quality\nGuarantee" },
|
|
209
|
-
].map(({ icon, label }) => (_jsxs(Div, { className: "flex flex-col items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400 min-w-[60px]", children: [_jsx(Span, { className: "text-base", children: icon }), _jsx(Span, { className: "whitespace-pre-line leading-tight", children: label })] }, label))) }) })] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." })
|
|
213
|
+
].map(({ icon, label }) => (_jsxs(Div, { className: "flex flex-col items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400 min-w-[60px]", children: [_jsx(Span, { className: "text-base", children: icon }), _jsx(Span, { className: "whitespace-pre-line leading-tight", children: label })] }, label))) }) })] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx("ul", { className: "space-y-2", children: ingredients.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 rounded-full bg-primary-400" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx("ol", { className: "space-y-3", children: howToUse.map((step, i) => (_jsxs("li", { className: "flex items-start gap-3 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30 text-xs font-bold text-primary-700 dark:text-primary-300", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
214
|
+
id: s.id,
|
|
215
|
+
label: s.title,
|
|
216
|
+
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
217
|
+
})) })), renderRelated: () => relatedItems.length > 0 ? (_jsx(RelatedProductsCarousel, { items: relatedItems })) : null }), _jsxs(BuyBar, { children: [formattedPrice && (_jsx(Span, { className: "mr-auto text-sm font-bold text-zinc-900 dark:text-zinc-50", children: formattedPrice })), _jsx(Button, { variant: "secondary", size: "sm", className: "shrink-0", disabled: !inStock, children: "Add to Cart" }), _jsx(Button, { variant: "primary", size: "sm", className: "flex-1", disabled: !inStock, children: inStock ? "Buy Now" : "Out of Stock" })] })] }) }));
|
|
210
218
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef } from "react";
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
|
-
import { Alert, Checkbox, FormField, FormGroup, Heading, RichTextEditor, Stack, Text, } from "../../../ui";
|
|
5
|
+
import { Alert, Checkbox, Div, FormField, FormGroup, Heading, RichTextEditor, Stack, Text, } from "../../../ui";
|
|
6
|
+
import { CustomSectionsEditor } from "./CustomSectionsEditor";
|
|
7
|
+
import { SublistingCategorySelect } from "./SublistingCategorySelect";
|
|
6
8
|
import { ImageUpload, MediaUploadField, MediaUploadList, } from "../../media";
|
|
7
9
|
import { useMediaUpload } from "../../media";
|
|
8
10
|
import { resolveDate } from "../../../utils/date.formatter";
|
|
@@ -135,5 +137,5 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
135
137
|
value: product.pickupAddressId || "",
|
|
136
138
|
onChange: (value) => update({ pickupAddressId: value }),
|
|
137
139
|
disabled: isReadonly,
|
|
138
|
-
}) })) : (_jsx(FormField, { name: "pickupAddressId", label: t("formPickupAddress"), type: "text", value: product.pickupAddressId || "", onChange: (value) => update({ pickupAddressId: value }), disabled: isReadonly })), _jsx(FormField, { name: "shippingInfo", label: t("formShipping"), type: "textarea", value: product.shippingInfo || "", onChange: (value) => update({ shippingInfo: value }), disabled: isReadonly, placeholder: "Shipping information..." }), _jsx(FormField, { name: "returnPolicy", label: t("formReturnPolicy"), type: "textarea", value: product.returnPolicy || "", onChange: (value) => update({ returnPolicy: value }), disabled: isReadonly, placeholder: "Return policy details..." }), product.storeName && (_jsx(FormField, { name: "storeName", label: t("formSeller"), type: "text", value: product.storeName, onChange: () => { }, disabled: true }))] }));
|
|
140
|
+
}) })) : (_jsx(FormField, { name: "pickupAddressId", label: t("formPickupAddress"), type: "text", value: product.pickupAddressId || "", onChange: (value) => update({ pickupAddressId: value }), disabled: isReadonly })), _jsx(SublistingCategorySelect, { value: product.sublistingCategoryId ?? "", onChange: (id) => update({ sublistingCategoryId: id || undefined }), disabled: isReadonly }), _jsxs(Div, { children: [_jsx(Heading, { level: 3, className: "mb-1 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Custom Sections" }), _jsx(Text, { className: "mb-3 text-xs text-zinc-500 dark:text-zinc-400", children: "Add up to 3 custom tabs to your product page \u2014 e.g. \"Box Contents\", \"Compatibility\", \"Grading Details\"." }), _jsx(CustomSectionsEditor, { sections: product.customSections ?? [], onChange: (sections) => update({ customSections: sections }) })] }), _jsx(FormField, { name: "shippingInfo", label: t("formShipping"), type: "textarea", value: product.shippingInfo || "", onChange: (value) => update({ shippingInfo: value }), disabled: isReadonly, placeholder: "Shipping information..." }), _jsx(FormField, { name: "returnPolicy", label: t("formReturnPolicy"), type: "textarea", value: product.returnPolicy || "", onChange: (value) => update({ returnPolicy: value }), disabled: isReadonly, placeholder: "Return policy details..." }), product.storeName && (_jsx(FormField, { name: "storeName", label: t("formSeller"), type: "text", value: product.storeName, onChange: () => { }, disabled: true }))] }));
|
|
139
141
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export interface CustomTabDef {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
content: React.ReactNode;
|
|
6
|
+
}
|
|
2
7
|
export interface ProductTabsShellProps {
|
|
3
8
|
descriptionContent?: React.ReactNode;
|
|
4
9
|
specsContent?: React.ReactNode;
|
|
5
10
|
ingredientsContent?: React.ReactNode;
|
|
6
11
|
howToUseContent?: React.ReactNode;
|
|
7
12
|
reviewsContent?: React.ReactNode;
|
|
13
|
+
/** Additional tabs from custom sections — rendered after standard tabs */
|
|
14
|
+
customTabs?: CustomTabDef[];
|
|
8
15
|
className?: string;
|
|
9
16
|
}
|
|
10
|
-
export declare function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
|
+
export declare function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
const
|
|
4
|
+
const STATIC_TABS = [
|
|
5
5
|
{ id: "description", label: "Description" },
|
|
6
6
|
{ id: "specs", label: "Specifications" },
|
|
7
7
|
{ id: "ingredients", label: "Ingredients" },
|
|
8
8
|
{ id: "howToUse", label: "How to Use" },
|
|
9
9
|
{ id: "reviews", label: "Reviews" },
|
|
10
10
|
];
|
|
11
|
-
export function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, className = "", }) {
|
|
12
|
-
const
|
|
11
|
+
export function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs = [], className = "", }) {
|
|
12
|
+
const staticContentMap = {
|
|
13
13
|
description: descriptionContent,
|
|
14
14
|
specs: specsContent,
|
|
15
15
|
ingredients: ingredientsContent,
|
|
16
16
|
howToUse: howToUseContent,
|
|
17
17
|
reviews: reviewsContent,
|
|
18
18
|
};
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
const visibleStatic = STATIC_TABS.filter((t) => staticContentMap[t.id] != null);
|
|
20
|
+
const visibleCustom = customTabs.filter((t) => t.content != null);
|
|
21
|
+
const allTabs = [
|
|
22
|
+
...visibleStatic.map((t) => ({
|
|
23
|
+
id: t.id,
|
|
24
|
+
label: t.label,
|
|
25
|
+
content: staticContentMap[t.id],
|
|
26
|
+
})),
|
|
27
|
+
...visibleCustom,
|
|
28
|
+
];
|
|
29
|
+
const [activeId, setActiveId] = useState(allTabs[0]?.id ?? "");
|
|
30
|
+
if (allTabs.length === 0)
|
|
22
31
|
return null;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
const activeContent = allTabs.find((t) => t.id === activeId)?.content;
|
|
33
|
+
return (_jsxs("div", { className: `mt-8 ${className}`, children: [_jsx("div", { className: "mb-6 flex gap-1 overflow-x-auto border-b border-zinc-200 dark:border-zinc-700 pb-px", children: allTabs.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveId(t.id), className: `flex-shrink-0 -mb-px pb-3 px-4 text-sm font-medium border-b-2 transition-colors ${activeId === t.id
|
|
34
|
+
? "border-[var(--appkit-color-primary,#6366f1)] text-[var(--appkit-color-primary,#6366f1)]"
|
|
35
|
+
: "border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"}`, children: t.label }, t.id))) }), _jsx("div", { children: activeContent })] }));
|
|
26
36
|
}
|