@mohasinac/appkit 2.7.11 → 2.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/jobs/core/assignSpinPrize.js +8 -7
- package/dist/_internal/server/jobs/core/bundleStockSync.js +11 -6
- package/dist/_internal/server/jobs/core/countersReconcile.js +3 -2
- package/dist/_internal/server/jobs/core/onProductStockChange.js +12 -7
- package/dist/_internal/server/jobs/core/onProductWrite.js +3 -2
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +7 -5
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +11 -6
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +13 -7
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +9 -4
- package/dist/_internal/server/jobs/core/triggerEventRaffle.js +3 -2
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
- package/dist/_internal/shared/features/events/schema.d.ts +4 -4
- package/dist/_internal/shared/features/products/schema.d.ts +20 -20
- package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/constants/field-names.d.ts +813 -0
- package/dist/constants/field-names.js +918 -0
- package/dist/constants/sort.d.ts +7 -0
- package/dist/constants/sort.js +5 -0
- package/dist/constants/table-keys.d.ts +48 -0
- package/dist/constants/table-keys.js +46 -0
- package/dist/features/account/components/AddressesIndexListing.js +9 -6
- package/dist/features/admin/components/AdminAllEventEntriesView.js +4 -1
- package/dist/features/admin/components/AdminBidsView.js +4 -1
- package/dist/features/admin/components/AdminBlogView.js +9 -2
- package/dist/features/admin/components/AdminBrandsView.js +9 -2
- package/dist/features/admin/components/AdminCarouselView.js +4 -1
- package/dist/features/admin/components/AdminCartsView.js +4 -1
- package/dist/features/admin/components/AdminCategoriesView.js +9 -2
- package/dist/features/admin/components/AdminContactView.js +4 -1
- package/dist/features/admin/components/AdminCouponsView.js +4 -1
- package/dist/features/admin/components/AdminFaqsView.js +5 -1
- package/dist/features/admin/components/AdminFeaturesView.js +5 -2
- package/dist/features/admin/components/AdminHistoryView.js +5 -1
- package/dist/features/admin/components/AdminNewsletterView.js +4 -1
- package/dist/features/admin/components/AdminNotificationsView.js +4 -1
- package/dist/features/admin/components/AdminOrdersView.js +14 -12
- package/dist/features/admin/components/AdminPayoutsView.js +4 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -1
- package/dist/features/admin/components/AdminProductsView.js +14 -3
- package/dist/features/admin/components/AdminReturnRequestsView.js +4 -1
- package/dist/features/admin/components/AdminReviewsView.js +4 -1
- package/dist/features/admin/components/AdminScammersView.js +4 -1
- package/dist/features/admin/components/AdminSessionsView.js +4 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +5 -1
- package/dist/features/admin/components/AdminStoresView.js +10 -3
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +4 -1
- package/dist/features/admin/components/AdminSupportTicketsView.js +4 -1
- package/dist/features/admin/components/AdminTeamView.js +4 -1
- package/dist/features/admin/components/AdminUsersView.js +9 -12
- package/dist/features/admin/components/AdminViewCards.d.ts +12 -0
- package/dist/features/admin/components/AdminViewCards.js +53 -0
- package/dist/features/admin/components/AdminWishlistsView.js +4 -1
- package/dist/features/auctions/repository/bid.repository.js +1 -6
- package/dist/features/auctions/schemas/index.d.ts +4 -4
- package/dist/features/blog/actions/blog-actions.d.ts +16 -16
- package/dist/features/blog/components/BlogFilters.d.ts +28 -28
- package/dist/features/blog/components/BlogFilters.js +18 -15
- package/dist/features/blog/components/BlogIndexListing.js +20 -16
- package/dist/features/blog/schemas/index.d.ts +10 -10
- package/dist/features/categories/components/CategoriesIndexListing.js +25 -21
- package/dist/features/categories/schemas/index.d.ts +2 -2
- package/dist/features/events/components/AdminEventsView.js +5 -1
- package/dist/features/events/components/EventFilters.d.ts +24 -24
- package/dist/features/events/components/EventFilters.js +16 -13
- package/dist/features/events/components/EventStatusBadge.js +1 -0
- package/dist/features/events/components/EventsIndexListing.js +31 -26
- package/dist/features/events/components/EventsListPageView.js +13 -10
- package/dist/features/events/schemas/firestore.d.ts +1 -0
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/types/index.d.ts +1 -1
- package/dist/features/faq/actions/faq-actions.d.ts +10 -10
- package/dist/features/faq/schemas/index.d.ts +4 -4
- package/dist/features/orders/components/OrderFilters.d.ts +20 -20
- package/dist/features/orders/components/OrderFilters.js +33 -30
- package/dist/features/orders/repository/orders.repository.js +1 -15
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +35 -31
- package/dist/features/pre-orders/schemas/index.d.ts +3 -3
- package/dist/features/products/components/AuctionsIndexListing.js +34 -30
- package/dist/features/products/components/BidHistory.d.ts +0 -1
- package/dist/features/products/components/BidHistory.js +1 -1
- package/dist/features/products/components/PrizeDrawsIndexListing.js +44 -40
- package/dist/features/products/components/ProductDetailPageView.js +1 -1
- package/dist/features/products/components/ProductFilters.d.ts +20 -20
- package/dist/features/products/components/ProductFilters.js +62 -59
- package/dist/features/products/components/ProductsIndexListing.js +28 -24
- package/dist/features/products/components/ProductsIndexPageView.js +18 -10
- package/dist/features/products/hooks/useProducts.js +2 -0
- package/dist/features/products/repository/products.repository.js +2 -22
- package/dist/features/products/schemas/index.d.ts +28 -28
- package/dist/features/products/schemas/product-features.validators.d.ts +12 -12
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +21 -17
- package/dist/features/promotions/schemas/index.d.ts +4 -4
- package/dist/features/reviews/components/ReviewDetailShell.js +6 -5
- package/dist/features/reviews/components/ReviewFilters.d.ts +16 -16
- package/dist/features/reviews/components/ReviewFilters.js +10 -7
- package/dist/features/reviews/components/ReviewsIndexListing.js +21 -17
- package/dist/features/reviews/components/ReviewsList.d.ts +5 -2
- package/dist/features/reviews/components/ReviewsList.js +11 -5
- package/dist/features/reviews/schemas/firestore.d.ts +1 -0
- package/dist/features/reviews/schemas/index.d.ts +8 -8
- package/dist/features/reviews/types/index.d.ts +1 -0
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/features/seller/components/SellerAuctionsView.js +5 -1
- package/dist/features/seller/components/SellerBidsView.js +4 -1
- package/dist/features/seller/components/SellerCouponsView.js +4 -1
- package/dist/features/seller/components/SellerOffersView.js +5 -1
- package/dist/features/seller/components/SellerOrdersView.js +4 -1
- package/dist/features/seller/components/SellerPayoutsView.js +5 -1
- package/dist/features/seller/components/SellerPreOrdersView.js +4 -1
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -1
- package/dist/features/seller/components/SellerProductsView.js +4 -1
- package/dist/features/seller/schemas/index.d.ts +10 -10
- package/dist/features/stores/api/route.js +10 -1
- package/dist/features/stores/components/StoreReviewsListing.js +1 -1
- package/dist/features/stores/components/StoresIndexListing.js +24 -20
- package/dist/features/stores/schemas/firestore.d.ts +1 -0
- package/dist/features/stores/schemas/index.d.ts +4 -4
- package/dist/index.d.ts +8 -1
- package/dist/index.js +12 -1
- package/dist/react/hooks/useUrlTable.js +2 -1
- package/dist/seed/bids-seed-data.js +45 -44
- package/dist/seed/categories-seed-data.js +41 -40
- package/dist/seed/coupons-seed-data.js +41 -40
- package/dist/seed/products-auctions-seed-data.js +54 -53
- package/dist/seed/products-preorders-seed-data.js +25 -24
- package/dist/seed/products-prize-draws-seed-data.js +24 -23
- package/dist/seed/products-standard-seed-data.js +294 -293
- package/dist/seed/reviews-seed-data.js +61 -60
- package/dist/seed/scammers-seed-data.js +3 -2
- package/dist/seed/stores-seed-data.js +9 -8
- package/dist/seed/support-tickets-seed-data.js +13 -12
- package/dist/seed/users-seed-data.js +28 -27
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/sieve-builder.d.ts +41 -0
- package/dist/utils/sieve-builder.js +66 -0
- package/package.json +1 -1
|
@@ -170,6 +170,8 @@ export interface ProductListParams {
|
|
|
170
170
|
dateTo?: string;
|
|
171
171
|
/** Pre-order production status */
|
|
172
172
|
preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship";
|
|
173
|
+
/** Prize-draw reveal status */
|
|
174
|
+
prizeRevealStatus?: "pending" | "open" | "closed";
|
|
173
175
|
/** Filter by brand name */
|
|
174
176
|
brand?: string;
|
|
175
177
|
/** Shipping / free shipping */
|
|
@@ -6,12 +6,16 @@ import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
|
6
6
|
import { Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
|
|
7
7
|
import { usePromotions } from "../hooks/usePromotions";
|
|
8
8
|
import { CouponCard } from "./CouponCard";
|
|
9
|
+
import { TABLE_KEYS } from "../../../constants/table-keys";
|
|
10
|
+
import { COUPON_FIELDS } from "../../../constants/field-names";
|
|
11
|
+
import { sortBy } from "../../../constants/sort";
|
|
12
|
+
const DEFAULT_SORT = sortBy(COUPON_FIELDS.CREATED_AT);
|
|
9
13
|
const COUPON_SORT_OPTIONS = [
|
|
10
|
-
{ value: "
|
|
11
|
-
{ value:
|
|
14
|
+
{ value: sortBy(COUPON_FIELDS.NAME, "ASC"), label: "Name A–Z" },
|
|
15
|
+
{ value: sortBy(COUPON_FIELDS.NAME), label: "Name Z–A" },
|
|
12
16
|
{ value: "-validity.endDate", label: "Expiring Soon" },
|
|
13
|
-
{ value:
|
|
14
|
-
{ value: "
|
|
17
|
+
{ value: sortBy(COUPON_FIELDS.CREATED_AT), label: "Newest First" },
|
|
18
|
+
{ value: sortBy(COUPON_FIELDS.CREATED_AT, "ASC"), label: "Oldest First" },
|
|
15
19
|
];
|
|
16
20
|
const COUPON_TYPES = [
|
|
17
21
|
{ value: "percentage", label: "% Off" },
|
|
@@ -20,8 +24,8 @@ const COUPON_TYPES = [
|
|
|
20
24
|
{ value: "buy_x_get_y", label: "Buy X Get Y" },
|
|
21
25
|
];
|
|
22
26
|
export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
|
|
23
|
-
const table = useUrlTable({ defaults: { pageSize: "12", sort:
|
|
24
|
-
const [searchInput, setSearchInput] = useState(table.get(
|
|
27
|
+
const table = useUrlTable({ defaults: { pageSize: "12", sort: DEFAULT_SORT } });
|
|
28
|
+
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
25
29
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
26
30
|
// Build Sieve filter string
|
|
27
31
|
const buildFilters = () => {
|
|
@@ -29,10 +33,10 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
|
|
|
29
33
|
const typeFilter = table.get("type");
|
|
30
34
|
if (typeFilter)
|
|
31
35
|
parts.push(`type==${typeFilter}`);
|
|
32
|
-
const dateFrom = table.get(
|
|
36
|
+
const dateFrom = table.get(TABLE_KEYS.DATE_FROM);
|
|
33
37
|
if (dateFrom)
|
|
34
38
|
parts.push(`validity.startDate>=${dateFrom}`);
|
|
35
|
-
const dateTo = table.get(
|
|
39
|
+
const dateTo = table.get(TABLE_KEYS.DATE_TO);
|
|
36
40
|
if (dateTo)
|
|
37
41
|
parts.push(`validity.endDate<=${dateTo}`);
|
|
38
42
|
if (storeId)
|
|
@@ -40,37 +44,37 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
|
|
|
40
44
|
return parts.join(",");
|
|
41
45
|
};
|
|
42
46
|
const { promotions: coupons, total, totalPages, isLoading } = usePromotions({
|
|
43
|
-
page: table.getNumber(
|
|
44
|
-
pageSize: table.getNumber(
|
|
45
|
-
sort: table.get(
|
|
47
|
+
page: table.getNumber(TABLE_KEYS.PAGE, 1),
|
|
48
|
+
pageSize: table.getNumber(TABLE_KEYS.PAGE_SIZE, 12),
|
|
49
|
+
sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
|
|
46
50
|
filters: buildFilters(),
|
|
47
51
|
});
|
|
48
52
|
// Use initial data on first load if available and no search/filter active
|
|
49
53
|
const displayCoupons = !isLoading && coupons.length > 0
|
|
50
54
|
? coupons
|
|
51
|
-
: !isLoading && initialCoupons && !table.get(
|
|
55
|
+
: !isLoading && initialCoupons && !table.get(TABLE_KEYS.QUERY) && !table.get("type")
|
|
52
56
|
? initialCoupons
|
|
53
57
|
: coupons;
|
|
54
58
|
const commitSearch = useCallback(() => {
|
|
55
|
-
table.set(
|
|
59
|
+
table.set(TABLE_KEYS.QUERY, searchInput.trim());
|
|
56
60
|
}, [searchInput, table]);
|
|
57
61
|
const handleKeyDown = (e) => {
|
|
58
62
|
if (e.key === "Enter")
|
|
59
63
|
commitSearch();
|
|
60
64
|
};
|
|
61
65
|
const activeType = table.get("type");
|
|
62
|
-
const hasActiveFilters = !!activeType || !!table.get(
|
|
66
|
+
const hasActiveFilters = !!activeType || !!table.get(TABLE_KEYS.DATE_FROM) || !!table.get(TABLE_KEYS.DATE_TO);
|
|
63
67
|
const clearFilters = () => {
|
|
64
|
-
table.setMany({ type: "",
|
|
68
|
+
table.setMany({ type: "", [TABLE_KEYS.DATE_FROM]: "", [TABLE_KEYS.DATE_TO]: "" });
|
|
65
69
|
};
|
|
66
70
|
return (_jsxs("div", { className: "min-h-[40vh]", children: [_jsxs("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: [_jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: `flex shrink-0 items-center gap-2 rounded-lg border px-3.5 py-2 text-sm font-medium transition-colors ${hasActiveFilters
|
|
67
71
|
? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
|
68
|
-
: "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs("span", { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), searchInput && (_jsx("button", { type: "button", onClick: () => { setSearchInput(""); table.set(
|
|
72
|
+
: "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsxs("span", { className: "hidden sm:inline", children: ["Filters", hasActiveFilters ? " •" : ""] })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search by name or description\u2026", className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), searchInput && (_jsx("button", { type: "button", onClick: () => { setSearchInput(""); table.set(TABLE_KEYS.QUERY, ""); }, className: "p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300", "aria-label": "Clear search", children: _jsx(X, { className: "h-3.5 w-3.5" }) })), _jsx("button", { type: "button", onClick: commitSearch, className: "flex shrink-0 items-center justify-center px-3 py-2 text-zinc-400 hover:text-primary dark:hover:text-primary-400 transition-colors", "aria-label": "Search", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, onChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, options: COUPON_SORT_OPTIONS })] })] }), hasActiveFilters && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 mt-2", children: [activeType && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: [COUPON_TYPES.find((t) => t.value === activeType)?.label ?? activeType, _jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, "aria-label": "Remove type filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_FROM) && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["From: ", table.get(TABLE_KEYS.DATE_FROM), _jsx("button", { type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_FROM, ""); }, "aria-label": "Remove from-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get(TABLE_KEYS.DATE_TO) && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-medium px-2.5 py-1", children: ["To: ", table.get(TABLE_KEYS.DATE_TO), _jsx("button", { type: "button", onClick: () => { table.set(TABLE_KEYS.DATE_TO, ""); }, "aria-label": "Remove to-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 underline", children: "Clear all" })] }))] }), _jsxs("div", { className: "py-6 px-4", children: [isLoading ? (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border-2 border-zinc-100 dark:border-slate-700 p-4 animate-pulse space-y-3", children: [_jsx("div", { className: "h-6 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" })] }, i))) })) : displayCoupons.length === 0 ? (_jsx("div", { className: "py-16 text-center", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500", children: "No coupons match your search." }) })) : (_jsx("div", { className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3", children: displayCoupons.map((coupon) => (_jsx(CouponCard, { coupon: coupon, labels: {
|
|
69
73
|
copy: "Copy",
|
|
70
74
|
copied: "Copied!",
|
|
71
75
|
expires: "Expires",
|
|
72
76
|
minOrder: "Min. order",
|
|
73
77
|
off: "OFF",
|
|
74
78
|
freeShipping: "Free Shipping",
|
|
75
|
-
} }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-6", children: [_jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: table.get("type") === t.value, onChange: () => { table.set("type", t.value); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: table.get(
|
|
79
|
+
} }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-6", children: [_jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: table.get("type") === t.value, onChange: () => { table.set("type", t.value); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs("div", { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: table.get(TABLE_KEYS.DATE_FROM) || "", onChange: (e) => { table.set(TABLE_KEYS.DATE_FROM, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "To date" }), _jsx("input", { type: "date", value: table.get(TABLE_KEYS.DATE_TO) || "", onChange: (e) => { table.set(TABLE_KEYS.DATE_TO, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] })] })] })] }), _jsxs("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5 flex gap-2", children: [_jsx("button", { type: "button", onClick: clearFilters, className: "flex-1 rounded-lg border border-zinc-300 dark:border-slate-600 py-2.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: "Clear all" }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), className: "flex-1 rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply" })] })] })] }))] }));
|
|
76
80
|
}
|
|
@@ -49,10 +49,11 @@ export declare const couponItemSchema: z.ZodObject<{
|
|
|
49
49
|
isActive: boolean;
|
|
50
50
|
isPublic: boolean;
|
|
51
51
|
description: string;
|
|
52
|
-
discountValue: number;
|
|
53
52
|
usageCount: number;
|
|
53
|
+
discountValue: number;
|
|
54
54
|
storeId?: string | undefined;
|
|
55
55
|
expiresAt?: string | undefined;
|
|
56
|
+
startsAt?: string | undefined;
|
|
56
57
|
minOrderAmount?: number | undefined;
|
|
57
58
|
maxDiscountAmount?: number | undefined;
|
|
58
59
|
maxUsageCount?: number | undefined;
|
|
@@ -61,7 +62,6 @@ export declare const couponItemSchema: z.ZodObject<{
|
|
|
61
62
|
getQuantity?: number | undefined;
|
|
62
63
|
applicableProductIds?: string[] | undefined;
|
|
63
64
|
applicableCategoryIds?: string[] | undefined;
|
|
64
|
-
startsAt?: string | undefined;
|
|
65
65
|
}, {
|
|
66
66
|
id: string;
|
|
67
67
|
createdAt: string;
|
|
@@ -73,10 +73,11 @@ export declare const couponItemSchema: z.ZodObject<{
|
|
|
73
73
|
isActive: boolean;
|
|
74
74
|
isPublic: boolean;
|
|
75
75
|
description: string;
|
|
76
|
-
discountValue: number;
|
|
77
76
|
usageCount: number;
|
|
77
|
+
discountValue: number;
|
|
78
78
|
storeId?: string | undefined;
|
|
79
79
|
expiresAt?: string | undefined;
|
|
80
|
+
startsAt?: string | undefined;
|
|
80
81
|
minOrderAmount?: number | undefined;
|
|
81
82
|
maxDiscountAmount?: number | undefined;
|
|
82
83
|
maxUsageCount?: number | undefined;
|
|
@@ -85,7 +86,6 @@ export declare const couponItemSchema: z.ZodObject<{
|
|
|
85
86
|
getQuantity?: number | undefined;
|
|
86
87
|
applicableProductIds?: string[] | undefined;
|
|
87
88
|
applicableCategoryIds?: string[] | undefined;
|
|
88
|
-
startsAt?: string | undefined;
|
|
89
89
|
}>;
|
|
90
90
|
export declare const promotionsListParamsSchema: z.ZodObject<{
|
|
91
91
|
scope: z.ZodOptional<z.ZodEnum<["admin", "seller"]>>;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { StarRating } from "../../../ui";
|
|
6
|
-
import { RichText } from "../../../ui";
|
|
5
|
+
import { RichText, Span, StarRating } from "../../../ui";
|
|
7
6
|
import { maskName } from "../../../security";
|
|
8
7
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
9
8
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
@@ -70,11 +69,13 @@ export function ReviewDetailShell({ review, storeHref }) {
|
|
|
70
69
|
? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
|
|
71
70
|
: null;
|
|
72
71
|
const sellerHref = storeHref ?? null;
|
|
73
|
-
const reviewerHref =
|
|
72
|
+
const reviewerHref = !review.isAnonymous && review.userId
|
|
73
|
+
? String(ROUTES.PUBLIC.PROFILE(review.userId))
|
|
74
|
+
: null;
|
|
74
75
|
const currentImage = lightboxIdx !== null ? images[lightboxIdx] : null;
|
|
75
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-neutral-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 pb-8 pt-10", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx(StarRating, { value: review.rating, size: "lg", readOnly: true }), _jsxs("span", { className: "text-2xl font-bold text-neutral-900 dark:text-white", children: [review.rating, ".0"] }), review.verified && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "\u2713 Verified Purchase" })), review.featured && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", children: "\u2605 Featured" }))] }), review.title && (_jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-4 leading-snug", children: review.title })), _jsxs("div", { className: "flex items-center gap-3", children: [review.userAvatar ? (_jsx("div", { role: "img", "aria-label": displayName, className: "h-11 w-11 flex-shrink-0 rounded-full bg-center bg-cover ring-2 ring-white dark:ring-zinc-800", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-primary/10 text-base font-bold text-primary ring-2 ring-white dark:ring-zinc-800", children: initials })), _jsxs("div", { className: "min-w-0", children: [_jsx(Link, { href: reviewerHref, className: "text-sm font-semibold text-neutral-900 dark:text-white hover:text-primary transition-colors", children: displayName }), date && (_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mt-0.5", children: date }))] })] })] }) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-8 space-y-8", children: [review.comment && (_jsx("section", { children: _jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:font-semibold prose-img:rounded-lg prose-a:text-primary", className: "text-neutral-700 dark:text-zinc-300" }) })), images.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: ["Photos (", images.length, ")"] }), _jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: images.map((img, i) => (_jsxs("button", { type: "button", onClick: () => setLightboxIdx(i), "aria-label": `View photo ${i + 1}`, className: "group relative aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-neutral-100 dark:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: [_jsx("div", { className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30", children: _jsx("span", { className: "text-white text-xl", children: "\uD83D\uDD0D" }) })] }, i))) })] })), review.video && (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: "Video" }), _jsx("div", { className: "overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-black aspect-video", children: _jsx("video", { src: review.video.url, poster: review.video.thumbnailUrl, controls: true, className: "h-full w-full", preload: "metadata" }) })] })), _jsxs("section", { className: "flex items-center gap-4 py-4 border-t border-neutral-100 dark:border-zinc-800", children: [_jsx("div", { className: "text-sm text-neutral-500 dark:text-zinc-400", children: helpfulCount > 0 && (_jsxs("span", { children: [_jsx("strong", { className: "text-neutral-900 dark:text-white", children: helpfulCount }), " ", helpfulCount === 1 ? "person" : "people", " found this helpful"] })) }), _jsxs("button", { type: "button", onClick: handleVote, disabled: voted || voting, className: `ml-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors ${voted
|
|
76
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-neutral-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 pb-8 pt-10", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx(StarRating, { value: review.rating, size: "lg", readOnly: true }), _jsxs("span", { className: "text-2xl font-bold text-neutral-900 dark:text-white", children: [review.rating, ".0"] }), review.verified && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "\u2713 Verified Purchase" })), review.featured && (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", children: "\u2605 Featured" }))] }), review.title && (_jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-4 leading-snug", children: review.title })), _jsxs("div", { className: "flex items-center gap-3", children: [review.userAvatar ? (_jsx("div", { role: "img", "aria-label": displayName, className: "h-11 w-11 flex-shrink-0 rounded-full bg-center bg-cover ring-2 ring-white dark:ring-zinc-800", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-primary/10 text-base font-bold text-primary ring-2 ring-white dark:ring-zinc-800", children: initials })), _jsxs("div", { className: "min-w-0", children: [reviewerHref ? (_jsx(Link, { href: reviewerHref, className: "text-sm font-semibold text-neutral-900 dark:text-white hover:text-primary transition-colors", children: displayName })) : (_jsx("span", { className: "text-sm font-semibold text-neutral-900 dark:text-white", children: review.isAnonymous ? "Anonymous" : displayName })), date && (_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mt-0.5", children: date }))] })] })] }) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-8 space-y-8", children: [review.comment && (_jsx("section", { children: _jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:font-semibold prose-img:rounded-lg prose-a:text-primary", className: "text-neutral-700 dark:text-zinc-300" }) })), images.length > 0 && (_jsxs("section", { children: [_jsxs("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: ["Photos (", images.length, ")"] }), _jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: images.map((img, i) => (_jsxs("button", { type: "button", onClick: () => setLightboxIdx(i), "aria-label": `View photo ${i + 1}`, className: "group relative aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-neutral-100 dark:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: [_jsx("div", { className: "h-full w-full bg-center bg-cover transition-transform duration-300 group-hover:scale-105", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30", children: _jsx("span", { className: "text-white text-xl", children: "\uD83D\uDD0D" }) })] }, i))) })] })), review.video && (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-neutral-400 dark:text-zinc-500 mb-3", children: "Video" }), _jsx("div", { className: "overflow-hidden rounded-xl border border-neutral-200 dark:border-zinc-700 bg-black aspect-video", children: _jsx("video", { src: review.video.url, poster: review.video.thumbnailUrl, controls: true, className: "h-full w-full", preload: "metadata" }) })] })), _jsxs("section", { className: "flex items-center gap-4 py-4 border-t border-neutral-100 dark:border-zinc-800", children: [_jsx("div", { className: "text-sm text-neutral-500 dark:text-zinc-400", children: helpfulCount > 0 && (_jsxs("span", { children: [_jsx("strong", { className: "text-neutral-900 dark:text-white", children: helpfulCount }), " ", helpfulCount === 1 ? "person" : "people", " found this helpful"] })) }), _jsxs("button", { type: "button", onClick: handleVote, disabled: voted || voting, className: `ml-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors ${voted
|
|
76
77
|
? "border-emerald-200 bg-emerald-50 text-emerald-600 dark:border-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-400 cursor-default"
|
|
77
|
-
: "border-neutral-300 dark:border-zinc-600 text-neutral-700 dark:text-zinc-200 hover:border-primary hover:text-primary dark:hover:border-primary dark:hover:text-primary disabled:opacity-50"}`, children: [_jsx("span", { "aria-hidden": "true", children: voted ? "✓" : "👍" }), voted ? "Marked helpful" : voting ? "Saving…" : "Helpful?"] })] }), _jsxs("section", { className: "grid gap-3 sm:grid-cols-3", children: [productHref && (_jsxs(Link, { href: productHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30 text-xl", children: "\uD83D\uDCE6" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Product" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: review.productTitle ?? "View Product" })] })] })), sellerHref && (_jsxs(Link, { href: sellerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/30 text-xl", children: "\uD83C\uDFEA" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Seller" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: "View Seller" })] })] })), _jsxs(Link, { href: reviewerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: displayName })] })] })] })] }), lightboxIdx !== null && currentImage && (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/95", onClick: closeLightbox, role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", children: [_jsx("button", { type: "button", onClick: closeLightbox, "aria-label": "Close lightbox", className: "absolute top-4 right-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 transition-colors text-xl", children: "\u00D7" }), _jsxs("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 text-white/70 text-sm", children: [lightboxIdx + 1, " / ", images.length] }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); prevImage(); }, "aria-label": "Previous image", className: "absolute left-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u2039" })), _jsx("div", { className: "max-h-[85vh] max-w-[85vw] flex items-center justify-center", onClick: (e) => e.stopPropagation(), children: _jsx("img", { src: currentImage.url, alt: `Review photo ${lightboxIdx + 1}`, className: "max-h-[85vh] max-w-[85vw] rounded-lg object-contain shadow-2xl" }) }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); nextImage(); }, "aria-label": "Next image", className: "absolute right-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u203A" })), images.length > 1 && (_jsx("div", { className: "absolute bottom-4 left-0 right-0 flex justify-center gap-2 px-4", children: images.map((img, i) => (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); setLightboxIdx(i); }, "aria-label": `Go to image ${i + 1}`, className: `h-12 w-12 flex-shrink-0 rounded-md bg-center bg-cover border-2 transition-all ${i === lightboxIdx
|
|
78
|
+
: "border-neutral-300 dark:border-zinc-600 text-neutral-700 dark:text-zinc-200 hover:border-primary hover:text-primary dark:hover:border-primary dark:hover:text-primary disabled:opacity-50"}`, children: [_jsx("span", { "aria-hidden": "true", children: voted ? "✓" : "👍" }), voted ? "Marked helpful" : voting ? "Saving…" : "Helpful?"] })] }), _jsxs("section", { className: "grid gap-3 sm:grid-cols-3", children: [productHref && (_jsxs(Link, { href: productHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30 text-xl", children: "\uD83D\uDCE6" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Product" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: review.productTitle ?? "View Product" })] })] })), sellerHref && (_jsxs(Link, { href: sellerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/30 text-xl", children: "\uD83C\uDFEA" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Seller" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: "View Seller" })] })] })), reviewerHref ? (_jsxs(Link, { href: reviewerHref, className: "group flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4 hover:border-primary hover:shadow-sm transition-all", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx("p", { className: "text-sm font-medium text-neutral-900 dark:text-white truncate group-hover:text-primary transition-colors", children: displayName })] })] })) : (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-4", children: [_jsx("span", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30 text-xl", children: "\uD83D\uDC64" }), _jsxs("div", { className: "min-w-0", children: [_jsx(Span, { className: "block text-xs text-neutral-400 dark:text-zinc-500 mb-0.5", children: "Reviewer" }), _jsx(Span, { className: "block text-sm font-medium text-neutral-900 dark:text-white truncate", children: "Anonymous" })] })] }))] })] }), lightboxIdx !== null && currentImage && (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/95", onClick: closeLightbox, role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", children: [_jsx("button", { type: "button", onClick: closeLightbox, "aria-label": "Close lightbox", className: "absolute top-4 right-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 transition-colors text-xl", children: "\u00D7" }), _jsxs("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 text-white/70 text-sm", children: [lightboxIdx + 1, " / ", images.length] }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); prevImage(); }, "aria-label": "Previous image", className: "absolute left-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u2039" })), _jsx("div", { className: "max-h-[85vh] max-w-[85vw] flex items-center justify-center", onClick: (e) => e.stopPropagation(), children: _jsx("img", { src: currentImage.url, alt: `Review photo ${lightboxIdx + 1}`, className: "max-h-[85vh] max-w-[85vw] rounded-lg object-contain shadow-2xl" }) }), images.length > 1 && (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); nextImage(); }, "aria-label": "Next image", className: "absolute right-4 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/25 transition-colors text-2xl", children: "\u203A" })), images.length > 1 && (_jsx("div", { className: "absolute bottom-4 left-0 right-0 flex justify-center gap-2 px-4", children: images.map((img, i) => (_jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); setLightboxIdx(i); }, "aria-label": `Go to image ${i + 1}`, className: `h-12 w-12 flex-shrink-0 rounded-md bg-center bg-cover border-2 transition-all ${i === lightboxIdx
|
|
78
79
|
? "border-white scale-110"
|
|
79
80
|
: "border-transparent opacity-60 hover:opacity-100"}`, style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) }))] }))] }));
|
|
80
81
|
}
|
|
@@ -7,55 +7,55 @@ export declare const REVIEW_FILTER_KEYS: {
|
|
|
7
7
|
readonly public: readonly ["rating", "brand", "dateFrom", "dateTo"];
|
|
8
8
|
};
|
|
9
9
|
export declare const REVIEW_ADMIN_SORT_OPTIONS: readonly [{
|
|
10
|
-
readonly value:
|
|
10
|
+
readonly value: string;
|
|
11
11
|
readonly key: "sortNewest";
|
|
12
12
|
}, {
|
|
13
|
-
readonly value:
|
|
13
|
+
readonly value: string;
|
|
14
14
|
readonly key: "sortOldest";
|
|
15
15
|
}, {
|
|
16
|
-
readonly value:
|
|
16
|
+
readonly value: string;
|
|
17
17
|
readonly key: "sortHighestRated";
|
|
18
18
|
}, {
|
|
19
|
-
readonly value:
|
|
19
|
+
readonly value: string;
|
|
20
20
|
readonly key: "sortLowestRated";
|
|
21
21
|
}];
|
|
22
22
|
export declare const REVIEW_SELLER_SORT_OPTIONS: readonly [{
|
|
23
|
-
readonly value:
|
|
23
|
+
readonly value: string;
|
|
24
24
|
readonly key: "sortNewest";
|
|
25
25
|
}, {
|
|
26
|
-
readonly value:
|
|
26
|
+
readonly value: string;
|
|
27
27
|
readonly key: "sortOldest";
|
|
28
28
|
}, {
|
|
29
|
-
readonly value:
|
|
29
|
+
readonly value: string;
|
|
30
30
|
readonly key: "sortHighestRated";
|
|
31
31
|
}, {
|
|
32
|
-
readonly value:
|
|
32
|
+
readonly value: string;
|
|
33
33
|
readonly key: "sortLowestRated";
|
|
34
34
|
}];
|
|
35
35
|
export declare const REVIEW_PUBLIC_SORT_OPTIONS: readonly [{
|
|
36
|
-
readonly value:
|
|
36
|
+
readonly value: string;
|
|
37
37
|
readonly key: "sortNewest";
|
|
38
38
|
}, {
|
|
39
|
-
readonly value:
|
|
39
|
+
readonly value: string;
|
|
40
40
|
readonly key: "sortOldest";
|
|
41
41
|
}, {
|
|
42
|
-
readonly value:
|
|
42
|
+
readonly value: string;
|
|
43
43
|
readonly key: "sortHighestRated";
|
|
44
44
|
}, {
|
|
45
|
-
readonly value:
|
|
45
|
+
readonly value: string;
|
|
46
46
|
readonly key: "sortLowestRated";
|
|
47
47
|
}];
|
|
48
48
|
export declare const REVIEW_SORT_OPTIONS: readonly [{
|
|
49
|
-
readonly value:
|
|
49
|
+
readonly value: string;
|
|
50
50
|
readonly key: "sortNewest";
|
|
51
51
|
}, {
|
|
52
|
-
readonly value:
|
|
52
|
+
readonly value: string;
|
|
53
53
|
readonly key: "sortOldest";
|
|
54
54
|
}, {
|
|
55
|
-
readonly value:
|
|
55
|
+
readonly value: string;
|
|
56
56
|
readonly key: "sortHighestRated";
|
|
57
57
|
}, {
|
|
58
|
-
readonly value:
|
|
58
|
+
readonly value: string;
|
|
59
59
|
readonly key: "sortLowestRated";
|
|
60
60
|
}];
|
|
61
61
|
export declare function getReviewFilterKeys(variant: ReviewFilterVariant): readonly string[];
|
|
@@ -5,16 +5,19 @@ import { FilterFacetSection } from "../../filters/FilterFacetSection";
|
|
|
5
5
|
import { RangeFilter } from "../../filters/RangeFilter";
|
|
6
6
|
import { SwitchFilter } from "../../filters/SwitchFilter";
|
|
7
7
|
import { Div } from "../../../ui";
|
|
8
|
+
import { TABLE_KEYS } from "../../../constants/table-keys";
|
|
9
|
+
import { REVIEW_FIELDS } from "../../../constants/field-names";
|
|
10
|
+
import { sortBy } from "../../../constants/sort";
|
|
8
11
|
export const REVIEW_FILTER_KEYS = {
|
|
9
|
-
admin: [
|
|
10
|
-
seller: [
|
|
11
|
-
public: [
|
|
12
|
+
admin: [TABLE_KEYS.STATUS, TABLE_KEYS.RATING, TABLE_KEYS.BRAND, "verified", TABLE_KEYS.FEATURED, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
|
|
13
|
+
seller: [TABLE_KEYS.STATUS, TABLE_KEYS.RATING, TABLE_KEYS.BRAND, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
|
|
14
|
+
public: [TABLE_KEYS.RATING, TABLE_KEYS.BRAND, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO],
|
|
12
15
|
};
|
|
13
16
|
export const REVIEW_ADMIN_SORT_OPTIONS = [
|
|
14
|
-
{ value:
|
|
15
|
-
{ value: "
|
|
16
|
-
{ value:
|
|
17
|
-
{ value: "
|
|
17
|
+
{ value: sortBy(REVIEW_FIELDS.CREATED_AT), key: "sortNewest" },
|
|
18
|
+
{ value: sortBy(REVIEW_FIELDS.CREATED_AT, "ASC"), key: "sortOldest" },
|
|
19
|
+
{ value: sortBy(REVIEW_FIELDS.RATING), key: "sortHighestRated" },
|
|
20
|
+
{ value: sortBy(REVIEW_FIELDS.RATING, "ASC"), key: "sortLowestRated" },
|
|
18
21
|
];
|
|
19
22
|
export const REVIEW_SELLER_SORT_OPTIONS = REVIEW_ADMIN_SORT_OPTIONS;
|
|
20
23
|
export const REVIEW_PUBLIC_SORT_OPTIONS = REVIEW_ADMIN_SORT_OPTIONS;
|
|
@@ -7,21 +7,25 @@ import { Pagination, ListingToolbar } from "../../../ui";
|
|
|
7
7
|
import { ReviewCard } from "./ReviewsList";
|
|
8
8
|
import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
|
|
9
9
|
import { useReviews } from "../hooks/useReviews";
|
|
10
|
+
import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
|
|
11
|
+
import { sortBy } from "../../../constants/sort";
|
|
12
|
+
import { REVIEW_FIELDS } from "../../../constants/field-names";
|
|
10
13
|
const PAGE_SIZE = 12;
|
|
11
|
-
const
|
|
14
|
+
const DEFAULT_SORT = sortBy(REVIEW_FIELDS.CREATED_AT);
|
|
15
|
+
const FILTER_KEYS = ["rating", TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO, "minVotes", "maxVotes"];
|
|
12
16
|
export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
13
|
-
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort:
|
|
14
|
-
const [searchInput, setSearchInput] = useState(table.get(
|
|
17
|
+
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
18
|
+
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
15
19
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
16
|
-
const [view, setView] = useState(table.get(
|
|
20
|
+
const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
|
|
17
21
|
const handleViewToggle = (next) => {
|
|
18
|
-
if (next ===
|
|
22
|
+
if (next === VIEW_MODE.TABLE)
|
|
19
23
|
return;
|
|
20
24
|
setView(next);
|
|
21
|
-
table.set(
|
|
25
|
+
table.set(TABLE_KEYS.VIEW, next);
|
|
22
26
|
};
|
|
23
|
-
const sort = table.get(
|
|
24
|
-
const currentPage = table.getNumber(
|
|
27
|
+
const sort = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
|
|
28
|
+
const currentPage = table.getNumber(TABLE_KEYS.PAGE, 1);
|
|
25
29
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
26
30
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
27
31
|
const pendingTable = useMemo(() => ({
|
|
@@ -64,24 +68,24 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
|
64
68
|
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
|
|
65
69
|
}, []);
|
|
66
70
|
const resetAll = useCallback(() => {
|
|
67
|
-
const updates = {
|
|
71
|
+
const updates = { [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" };
|
|
68
72
|
for (const k of FILTER_KEYS)
|
|
69
73
|
updates[k] = "";
|
|
70
74
|
table.setMany(updates);
|
|
71
75
|
setSearchInput("");
|
|
72
76
|
}, [table]);
|
|
73
77
|
const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
|
|
74
|
-
const hasActiveState = !!table.get(
|
|
75
|
-
table.get(
|
|
78
|
+
const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
|
|
79
|
+
table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
|
|
76
80
|
activeFilterCount > 0;
|
|
77
81
|
const commitSearch = useCallback(() => {
|
|
78
|
-
table.set(
|
|
82
|
+
table.set(TABLE_KEYS.QUERY, searchInput.trim());
|
|
79
83
|
}, [searchInput, table]);
|
|
80
84
|
const { reviews, total, totalPages, isLoading } = useReviews({
|
|
81
|
-
q: table.get(
|
|
82
|
-
rating: table.get(
|
|
83
|
-
dateFrom: table.get(
|
|
84
|
-
dateTo: table.get(
|
|
85
|
+
q: table.get(TABLE_KEYS.QUERY) || undefined,
|
|
86
|
+
rating: table.get(TABLE_KEYS.RATING) || undefined,
|
|
87
|
+
dateFrom: table.get(TABLE_KEYS.DATE_FROM) || undefined,
|
|
88
|
+
dateTo: table.get(TABLE_KEYS.DATE_TO) || undefined,
|
|
85
89
|
minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
|
|
86
90
|
maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
|
|
87
91
|
sort,
|
|
@@ -92,5 +96,5 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
|
92
96
|
value: opt.value,
|
|
93
97
|
label: opt.key,
|
|
94
98
|
}));
|
|
95
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(
|
|
99
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, 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) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
96
100
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { Review } from "../types";
|
|
2
|
+
export type ReviewCardContext = "listing" | "store" | "general";
|
|
2
3
|
export interface ReviewCardProps {
|
|
3
4
|
review: Review;
|
|
5
|
+
context?: ReviewCardContext;
|
|
4
6
|
className?: string;
|
|
5
7
|
}
|
|
6
|
-
export declare function ReviewCard({ review, className }: ReviewCardProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function ReviewCard({ review, context, className }: ReviewCardProps): import("react/jsx-runtime").JSX.Element;
|
|
7
9
|
export interface ReviewsListProps {
|
|
8
10
|
reviews: Review[];
|
|
11
|
+
context?: ReviewCardContext;
|
|
9
12
|
isLoading?: boolean;
|
|
10
13
|
totalPages?: number;
|
|
11
14
|
currentPage?: number;
|
|
12
15
|
onPageChange?: (page: number) => void;
|
|
13
16
|
emptyLabel?: string;
|
|
14
17
|
}
|
|
15
|
-
export declare function ReviewsList({ reviews, isLoading, totalPages, currentPage, onPageChange, emptyLabel, }: ReviewsListProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function ReviewsList({ reviews, context, isLoading, totalPages, currentPage, onPageChange, emptyLabel, }: ReviewsListProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import { maskName } from "../../../security";
|
|
|
7
7
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
8
8
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
9
9
|
import { ROUTES } from "../../../next";
|
|
10
|
-
export function ReviewCard({ review, className = "" }) {
|
|
10
|
+
export function ReviewCard({ review, context = "general", className = "" }) {
|
|
11
11
|
const date = review.createdAt
|
|
12
12
|
? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
|
|
13
13
|
year: "numeric",
|
|
@@ -21,15 +21,21 @@ export function ReviewCard({ review, className = "" }) {
|
|
|
21
21
|
const productHref = review.productId
|
|
22
22
|
? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
|
|
23
23
|
: null;
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const profileHref = !review.isAnonymous && review.userId
|
|
25
|
+
? String(ROUTES.PUBLIC.PROFILE(review.userId))
|
|
26
|
+
: null;
|
|
27
|
+
const showStoreLink = context !== "store" && !!(review.storeSlug && review.storeName);
|
|
28
|
+
const showProductLink = context !== "listing" && !!productHref;
|
|
29
|
+
const showProfileLink = !!profileHref;
|
|
30
|
+
const hasFooter = showStoreLink || showProductLink || showProfileLink;
|
|
31
|
+
return (_jsxs(Div, { className: `group flex flex-col h-full rounded-xl border border-neutral-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900 transition-shadow hover:shadow-md ${className}`, children: [_jsxs(Link, { href: reviewHref, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { className: "flex items-start gap-3", children: [review.userAvatar ? (_jsx(Div, { role: "img", "aria-label": displayName, className: "h-9 w-9 flex-shrink-0 rounded-full bg-center bg-cover", style: { backgroundImage: `url(${review.userAvatar})` } })) : (_jsx(Div, { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-neutral-200 text-sm font-medium text-neutral-600 dark:bg-zinc-700 dark:text-zinc-300", children: initials })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Span, { className: "font-medium text-neutral-900 dark:text-white", children: displayName }), review.verified && (_jsx(Span, { className: "rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", children: "Verified" })), date && (_jsx(Span, { className: "text-xs text-neutral-400 dark:text-zinc-500", children: date }))] }), _jsx(Div, { className: "mt-1", children: _jsx(StarRating, { value: review.rating, size: "sm", readOnly: true }) })] })] }), review.title && (_jsx(Heading, { level: 4, className: "mt-3 font-semibold text-neutral-900 dark:text-white", children: review.title })), review.comment && (_jsx(RichText, { html: normalizeRichTextHtml(review.comment), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-2 text-sm leading-relaxed text-neutral-600 dark:text-zinc-400" })), review.images && review.images.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mt-3", children: review.images.map((img, i) => (_jsx(Div, { role: "img", "aria-label": `Review image ${i + 1}`, className: "h-16 w-16 rounded-lg bg-center bg-cover border border-neutral-100 dark:border-zinc-700", style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) })), (review.helpfulCount ?? 0) > 0 && (_jsxs(Text, { className: "mt-3 text-xs text-neutral-400 dark:text-zinc-500", children: [review.helpfulCount, " found this helpful"] }))] }), hasFooter && (_jsxs(Div, { className: "mt-3 pt-3 border-t border-neutral-100 dark:border-zinc-800 flex flex-col gap-1.5", children: [showStoreLink && (_jsxs(Link, { href: String(ROUTES.PUBLIC.STORE_DETAIL(review.storeSlug)), className: "flex items-center gap-1.5 text-xs font-medium text-primary hover:underline", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83C\uDFEA" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.storeName })] })), showProductLink && (_jsxs(Link, { href: productHref, className: "flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 hover:text-neutral-700 dark:hover:text-zinc-200", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: review.productTitle ?? "View Product" }), _jsx("span", { "aria-hidden": "true", className: "ml-auto text-primary group-hover:translate-x-0.5 transition-transform", children: "\u2192" })] })), showProfileLink && (_jsxs(Link, { href: profileHref, className: "flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 hover:text-neutral-700 dark:hover:text-zinc-200", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDC64" }), _jsx("span", { className: THEME_CONSTANTS.utilities.textClamp1, children: displayName })] }))] }))] }));
|
|
26
32
|
}
|
|
27
|
-
export function ReviewsList({ reviews, isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
|
|
33
|
+
export function ReviewsList({ reviews, context = "general", isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
|
|
28
34
|
if (isLoading) {
|
|
29
35
|
return (_jsx(Stack, { gap: "md", children: Array.from({ length: 4 }).map((_, i) => (_jsxs(Div, { className: "rounded-xl border border-neutral-200 dark:border-zinc-700 p-5", children: [_jsxs(Div, { className: "flex gap-3", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-full" }), _jsxs(Stack, { gap: "sm", className: "flex-1", children: [_jsx(Skeleton, { className: "h-4 w-24 rounded" }), _jsx(Skeleton, { className: "h-3 w-16 rounded" })] })] }), _jsxs(Stack, { gap: "xs", className: "mt-3", children: [_jsx(Skeleton, { className: "h-3 w-full rounded" }), _jsx(Skeleton, { className: "h-3 w-3/4 rounded" })] })] }, i))) }));
|
|
30
36
|
}
|
|
31
37
|
if (reviews.length === 0) {
|
|
32
38
|
return (_jsx(Text, { className: "py-12 text-center text-sm text-neutral-500 dark:text-zinc-500", children: emptyLabel }));
|
|
33
39
|
}
|
|
34
|
-
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Stack, { gap: "md", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) }), totalPages > 1 && onPageChange && (_jsx(Div, { className: "flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
|
|
40
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Stack, { gap: "md", children: reviews.map((review) => (_jsx(ReviewCard, { review: review, context: context }, review.id))) }), totalPages > 1 && onPageChange && (_jsx(Div, { className: "flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) }))] }));
|
|
35
41
|
}
|
|
@@ -109,18 +109,18 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
109
109
|
duration?: number | undefined;
|
|
110
110
|
} | undefined;
|
|
111
111
|
productTitle?: string | undefined;
|
|
112
|
+
featured?: boolean | undefined;
|
|
113
|
+
storeName?: string | undefined;
|
|
112
114
|
images?: {
|
|
113
115
|
url: string;
|
|
114
116
|
thumbnailUrl?: string | undefined;
|
|
115
117
|
}[] | undefined;
|
|
116
|
-
storeName?: string | undefined;
|
|
117
|
-
featured?: boolean | undefined;
|
|
118
118
|
storeSlug?: string | undefined;
|
|
119
|
-
verified?: boolean | undefined;
|
|
120
119
|
userAvatar?: string | undefined;
|
|
121
120
|
comment?: string | undefined;
|
|
122
121
|
helpfulCount?: number | undefined;
|
|
123
122
|
reportCount?: number | undefined;
|
|
123
|
+
verified?: boolean | undefined;
|
|
124
124
|
}, {
|
|
125
125
|
id: string;
|
|
126
126
|
status: "pending" | "approved" | "rejected";
|
|
@@ -139,18 +139,18 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
139
139
|
duration?: number | undefined;
|
|
140
140
|
} | undefined;
|
|
141
141
|
productTitle?: string | undefined;
|
|
142
|
+
featured?: boolean | undefined;
|
|
143
|
+
storeName?: string | undefined;
|
|
142
144
|
images?: {
|
|
143
145
|
url: string;
|
|
144
146
|
thumbnailUrl?: string | undefined;
|
|
145
147
|
}[] | undefined;
|
|
146
|
-
storeName?: string | undefined;
|
|
147
|
-
featured?: boolean | undefined;
|
|
148
148
|
storeSlug?: string | undefined;
|
|
149
|
-
verified?: boolean | undefined;
|
|
150
149
|
userAvatar?: string | undefined;
|
|
151
150
|
comment?: string | undefined;
|
|
152
151
|
helpfulCount?: number | undefined;
|
|
153
152
|
reportCount?: number | undefined;
|
|
153
|
+
verified?: boolean | undefined;
|
|
154
154
|
}>;
|
|
155
155
|
/** Base schema for list-query parameters. */
|
|
156
156
|
export declare const reviewListParamsSchema: z.ZodObject<{
|
|
@@ -170,9 +170,9 @@ export declare const reviewListParamsSchema: z.ZodObject<{
|
|
|
170
170
|
status?: "pending" | "approved" | "rejected" | undefined;
|
|
171
171
|
page?: number | undefined;
|
|
172
172
|
productId?: string | undefined;
|
|
173
|
+
featured?: boolean | undefined;
|
|
173
174
|
rating?: number | undefined;
|
|
174
175
|
userId?: string | undefined;
|
|
175
|
-
featured?: boolean | undefined;
|
|
176
176
|
}, {
|
|
177
177
|
sort?: string | undefined;
|
|
178
178
|
perPage?: number | undefined;
|
|
@@ -180,7 +180,7 @@ export declare const reviewListParamsSchema: z.ZodObject<{
|
|
|
180
180
|
status?: "pending" | "approved" | "rejected" | undefined;
|
|
181
181
|
page?: number | undefined;
|
|
182
182
|
productId?: string | undefined;
|
|
183
|
+
featured?: boolean | undefined;
|
|
183
184
|
rating?: number | undefined;
|
|
184
185
|
userId?: string | undefined;
|
|
185
|
-
featured?: boolean | undefined;
|
|
186
186
|
}>;
|