@mohasinac/appkit 2.3.1 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +12 -8
- package/dist/client.js +7 -4
- package/dist/constants/api-endpoints.d.ts +4 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/core/contact-submissions.repository.d.ts +32 -0
- package/dist/core/contact-submissions.repository.js +49 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/features/about/components/HowPayoutsWorkView.js +1 -1
- package/dist/features/account/components/AddressFilters.d.ts +5 -0
- package/dist/features/account/components/AddressFilters.js +20 -0
- package/dist/features/account/components/AddressesIndexListing.d.ts +6 -0
- package/dist/features/account/components/AddressesIndexListing.js +51 -0
- package/dist/features/account/components/UserSidebar.d.ts +7 -3
- package/dist/features/account/components/UserSidebar.js +55 -7
- package/dist/features/account/hooks/useAddresses.d.ts +7 -0
- package/dist/features/account/hooks/useAddresses.js +12 -1
- package/dist/features/admin/actions/admin-read-actions.d.ts +12 -0
- package/dist/features/admin/actions/admin-read-actions.js +18 -0
- package/dist/features/admin/components/AdminBlogView.js +26 -2
- package/dist/features/admin/components/AdminCarouselView.js +18 -2
- package/dist/features/admin/components/AdminCategoriesView.js +27 -2
- package/dist/features/admin/components/AdminContactView.d.ts +4 -0
- package/dist/features/admin/components/AdminContactView.js +50 -0
- package/dist/features/admin/components/AdminFaqsView.js +19 -2
- package/dist/features/admin/components/AdminListingScaffold.d.ts +11 -2
- package/dist/features/admin/components/AdminListingScaffold.js +14 -3
- package/dist/features/admin/components/AdminNewsletterView.d.ts +4 -0
- package/dist/features/admin/components/AdminNewsletterView.js +50 -0
- package/dist/features/admin/components/AdminProductsView.js +30 -2
- package/dist/features/admin/components/AdminReviewsView.js +26 -2
- package/dist/features/admin/components/AdminStoresView.js +17 -2
- package/dist/features/admin/components/AdminUsersView.js +27 -2
- package/dist/features/admin/components/DataTable.d.ts +2 -1
- package/dist/features/admin/components/DataTable.js +18 -4
- package/dist/features/admin/components/index.d.ts +6 -0
- package/dist/features/admin/components/index.js +3 -0
- package/dist/features/admin/hooks/useAdminListingData.d.ts +3 -1
- package/dist/features/admin/hooks/useAdminListingData.js +12 -7
- package/dist/features/admin/server.d.ts +3 -0
- package/dist/features/admin/server.js +2 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +93 -47
- package/dist/features/auctions/components/AuctionFilters.d.ts +8 -0
- package/dist/features/auctions/components/AuctionFilters.js +12 -0
- package/dist/features/auctions/components/AuctionsListView.d.ts +6 -1
- package/dist/features/auctions/components/AuctionsListView.js +37 -5
- package/dist/features/auctions/schemas/index.d.ts +4 -4
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +1 -7
- package/dist/features/blog/components/BlogFeaturedCard.js +4 -5
- package/dist/features/blog/components/BlogFilters.js +2 -1
- package/dist/features/blog/components/BlogIndexListing.js +14 -9
- package/dist/features/blog/components/BlogIndexPageView.d.ts +6 -1
- package/dist/features/blog/components/BlogIndexPageView.js +10 -2
- package/dist/features/blog/components/BlogListView.d.ts +2 -1
- package/dist/features/blog/components/BlogListView.js +10 -3
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -1
- package/dist/features/categories/components/CategoriesIndexListing.js +41 -38
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +6 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +41 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +13 -6
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -0
- package/dist/features/categories/components/CategoryDetailTabs.js +17 -0
- package/dist/features/categories/components/CategoryFilters.js +2 -1
- package/dist/features/categories/components/CategoryGrid.d.ts +2 -1
- package/dist/features/categories/components/CategoryGrid.js +8 -6
- package/dist/features/categories/components/CategoryProductsListing.js +22 -11
- package/dist/features/categories/components/index.d.ts +2 -0
- package/dist/features/categories/components/index.js +1 -0
- package/dist/features/categories/hooks/useCategories.d.ts +20 -0
- package/dist/features/categories/hooks/useCategories.js +50 -1
- package/dist/features/categories/hooks/useCategoryTree.d.ts +17 -0
- package/dist/features/categories/hooks/useCategoryTree.js +65 -0
- package/dist/features/events/components/AdminEventEditorView.d.ts +6 -0
- package/dist/features/events/components/AdminEventEditorView.js +203 -0
- package/dist/features/events/components/AdminEventsView.js +28 -2
- package/dist/features/events/components/EventCard.js +4 -2
- package/dist/features/events/components/EventFilters.js +2 -1
- package/dist/features/events/components/EventsIndexListing.js +40 -10
- package/dist/features/events/components/EventsListPageView.d.ts +6 -1
- package/dist/features/events/components/EventsListPageView.js +40 -7
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/hooks/useEvents.js +2 -0
- package/dist/features/events/types/index.d.ts +1 -0
- package/dist/features/homepage/components/BlogArticlesSection.js +1 -1
- package/dist/features/homepage/components/EventsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +1 -1
- package/dist/features/homepage/components/FeaturedProductsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedStoresSection.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +27 -17
- package/dist/features/homepage/components/SectionCarousel.js +4 -4
- package/dist/features/homepage/components/ShopByCategorySection.js +2 -2
- package/dist/features/homepage/schemas/firestore.d.ts +1 -1
- package/dist/features/homepage/schemas/firestore.js +2 -1
- package/dist/features/layout/AppLayoutShell.d.ts +6 -2
- package/dist/features/layout/AppLayoutShell.js +7 -3
- package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +3 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +6 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +80 -12
- package/dist/features/pre-orders/components/PreOrderFilters.d.ts +8 -0
- package/dist/features/pre-orders/components/PreOrderFilters.js +21 -0
- package/dist/features/pre-orders/components/PreOrdersIndexListing.d.ts +2 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +69 -10
- package/dist/features/pre-orders/components/PreOrdersListView.d.ts +6 -1
- package/dist/features/pre-orders/components/PreOrdersListView.js +26 -7
- package/dist/features/pre-orders/components/index.d.ts +2 -0
- package/dist/features/pre-orders/components/index.js +1 -0
- package/dist/features/products/components/AuctionsIndexListing.d.ts +2 -1
- package/dist/features/products/components/AuctionsIndexListing.js +61 -9
- package/dist/features/products/components/InteractiveProductCard.d.ts +2 -4
- package/dist/features/products/components/InteractiveProductCard.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +116 -25
- package/dist/features/products/components/ProductFilters.d.ts +6 -11
- package/dist/features/products/components/ProductFilters.js +5 -3
- package/dist/features/products/components/ProductGrid.d.ts +8 -2
- package/dist/features/products/components/ProductGrid.js +20 -5
- package/dist/features/products/components/ProductTabsShell.d.ts +3 -11
- package/dist/features/products/components/ProductTabsShell.js +14 -14
- package/dist/features/products/components/ProductsIndexListing.js +73 -9
- package/dist/features/products/components/ProductsIndexPageView.d.ts +6 -1
- package/dist/features/products/components/ProductsIndexPageView.js +39 -6
- package/dist/features/products/components/RelatedProductsCarousel.d.ts +7 -0
- package/dist/features/products/components/RelatedProductsCarousel.js +11 -0
- package/dist/features/products/components/index.d.ts +1 -0
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/hooks/useProducts.js +16 -0
- package/dist/features/products/repository/products.repository.d.ts +8 -0
- package/dist/features/products/repository/products.repository.js +2 -0
- package/dist/features/products/schemas/index.d.ts +8 -8
- package/dist/features/products/types/index.d.ts +12 -0
- package/dist/features/promotions/components/CouponsIndexListing.d.ts +9 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +86 -0
- package/dist/features/promotions/components/PromotionsView.d.ts +11 -5
- package/dist/features/promotions/components/PromotionsView.js +6 -1
- package/dist/features/promotions/components/index.d.ts +4 -2
- package/dist/features/promotions/components/index.js +2 -1
- package/dist/features/reviews/components/ReviewDetailPageView.d.ts +4 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +20 -0
- package/dist/features/reviews/components/ReviewDetailShell.d.ts +7 -0
- package/dist/features/reviews/components/ReviewDetailShell.js +80 -0
- package/dist/features/reviews/components/ReviewFilters.d.ts +3 -3
- package/dist/features/reviews/components/ReviewFilters.js +5 -4
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.js +35 -51
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +6 -1
- package/dist/features/reviews/components/ReviewsIndexPageView.js +49 -3
- package/dist/features/reviews/components/ReviewsList.js +9 -1
- package/dist/features/reviews/components/index.d.ts +1 -0
- package/dist/features/reviews/hooks/useReviews.js +15 -1
- package/dist/features/reviews/types/index.d.ts +6 -1
- package/dist/features/seller/components/SellerSidebar.d.ts +8 -4
- package/dist/features/seller/components/SellerSidebar.js +6 -4
- package/dist/features/seller/components/index.d.ts +30 -0
- package/dist/features/seller/components/index.js +17 -0
- package/dist/features/seller/hooks/useSellerStore.d.ts +2 -0
- package/dist/features/seller/hooks/useSellerStore.js +2 -0
- package/dist/features/seller/permission-map.d.ts +4 -2
- package/dist/features/seller/permission-map.js +16 -14
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +1 -1
- package/dist/features/stores/api/[storeSlug]/reviews/route.js +24 -19
- package/dist/features/stores/components/InteractiveStoreCard.d.ts +0 -5
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -9
- package/dist/features/stores/components/StoreAuctionsListing.js +27 -9
- package/dist/features/stores/components/StoreDetailLayoutView.js +2 -0
- package/dist/features/stores/components/StoreFilters.d.ts +5 -0
- package/dist/features/stores/components/StoreFilters.js +20 -0
- package/dist/features/stores/components/StoreHeader.js +2 -2
- package/dist/features/stores/components/StorePreOrdersListing.d.ts +5 -0
- package/dist/features/stores/components/StorePreOrdersListing.js +40 -0
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -0
- package/dist/features/stores/components/StorePreOrdersPageView.js +21 -0
- package/dist/features/stores/components/StoreProductsListing.js +21 -11
- package/dist/features/stores/components/StoreReviewsListing.js +2 -7
- package/dist/features/stores/components/StoresIndexListing.js +42 -8
- package/dist/features/stores/components/StoresIndexPageView.d.ts +6 -1
- package/dist/features/stores/components/StoresIndexPageView.js +9 -2
- package/dist/features/stores/components/index.d.ts +3 -0
- package/dist/features/stores/components/index.js +1 -0
- package/dist/features/stores/hooks/useStores.d.ts +7 -1
- package/dist/features/stores/hooks/useStores.js +16 -3
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/stores/types/index.d.ts +3 -0
- package/dist/features/wishlist/hooks/useGuestWishlist.d.ts +20 -0
- package/dist/features/wishlist/hooks/useGuestWishlist.js +49 -0
- package/dist/features/wishlist/hooks/useWishlistCount.d.ts +7 -0
- package/dist/features/wishlist/hooks/useWishlistCount.js +31 -0
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +56 -0
- package/dist/features/wishlist/hooks/useWishlistWithGuest.js +57 -0
- package/dist/features/wishlist/index.d.ts +3 -0
- package/dist/features/wishlist/index.js +3 -0
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +22 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +70 -0
- package/dist/index.d.ts +50 -1
- package/dist/index.js +63 -1
- package/dist/next/routing/route-map.d.ts +70 -36
- package/dist/next/routing/route-map.js +30 -22
- package/dist/seed/addresses-seed-data.js +62 -261
- package/dist/seed/beyblade-seed-data.d.ts +7 -0
- package/dist/seed/beyblade-seed-data.js +947 -0
- package/dist/seed/bids-seed-data.d.ts +10 -2
- package/dist/seed/bids-seed-data.js +220 -1071
- package/dist/seed/blog-posts-seed-data.d.ts +2 -2
- package/dist/seed/blog-posts-seed-data.js +455 -117
- package/dist/seed/cart-seed-data.d.ts +9 -9
- package/dist/seed/cart-seed-data.js +73 -74
- package/dist/seed/coupons-seed-data.d.ts +3 -4
- package/dist/seed/coupons-seed-data.js +3 -509
- package/dist/seed/events-seed-data.d.ts +2 -2
- package/dist/seed/events-seed-data.js +315 -476
- package/dist/seed/faq-seed-data.d.ts +18 -41
- package/dist/seed/faq-seed-data.js +1059 -1172
- package/dist/seed/hot-wheels-seed-data.d.ts +7 -0
- package/dist/seed/hot-wheels-seed-data.js +1365 -0
- package/dist/seed/index.d.ts +6 -1
- package/dist/seed/index.js +6 -1
- package/dist/seed/pokemon-carousel-slides-seed-data.d.ts +4 -2
- package/dist/seed/pokemon-carousel-slides-seed-data.js +152 -268
- package/dist/seed/pokemon-categories-seed-data.d.ts +18 -21
- package/dist/seed/pokemon-categories-seed-data.js +424 -1004
- package/dist/seed/pokemon-coupons-seed-data.d.ts +6 -0
- package/dist/seed/pokemon-coupons-seed-data.js +465 -0
- package/dist/seed/pokemon-homepage-sections-seed-data.d.ts +3 -2
- package/dist/seed/pokemon-homepage-sections-seed-data.js +67 -289
- package/dist/seed/pokemon-products-seed-data.js +662 -0
- package/dist/seed/pokemon-seed-bundle.d.ts +32 -11
- package/dist/seed/pokemon-seed-bundle.js +41 -11
- package/dist/seed/pokemon-stores-seed-data.d.ts +2 -3
- package/dist/seed/pokemon-stores-seed-data.js +56 -31
- package/dist/seed/pokemon-users-seed-data.d.ts +2 -2
- package/dist/seed/pokemon-users-seed-data.js +245 -261
- package/dist/seed/reviews-seed-data.d.ts +17 -2
- package/dist/seed/reviews-seed-data.js +519 -483
- package/dist/seed/site-settings-seed-data.js +14 -14
- package/dist/seed/store-addresses-seed-data.js +68 -50
- package/dist/seed/transformers-seed-data.d.ts +7 -0
- package/dist/seed/transformers-seed-data.js +510 -0
- package/dist/seed/wishlists-seed-data.d.ts +5 -1
- package/dist/seed/wishlists-seed-data.js +82 -4
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +2 -0
- package/dist/ui/components/BaseListingCard.js +24 -26
- package/dist/ui/components/BaseListingCard.style.css +5 -5
- package/dist/ui/components/HorizontalScroller.d.ts +1 -1
- package/dist/ui/components/HorizontalScroller.js +19 -5
- package/dist/ui/components/SideDrawer.style.css +3 -11
- package/dist/ui/rich-text/RichText.js +19 -1
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
5
|
+
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
|
+
import { Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
|
|
7
|
+
import { usePromotions } from "../hooks/usePromotions";
|
|
8
|
+
import { CouponCard } from "./CouponCard";
|
|
9
|
+
const COUPON_SORT_OPTIONS = [
|
|
10
|
+
{ value: "name", label: "Name A–Z" },
|
|
11
|
+
{ value: "-name", label: "Name Z–A" },
|
|
12
|
+
{ value: "-validity.endDate", label: "Expiring Soon" },
|
|
13
|
+
{ value: "-createdAt", label: "Newest First" },
|
|
14
|
+
{ value: "createdAt", label: "Oldest First" },
|
|
15
|
+
];
|
|
16
|
+
const COUPON_TYPES = [
|
|
17
|
+
{ value: "percentage", label: "% Off" },
|
|
18
|
+
{ value: "fixed", label: "Fixed Amount" },
|
|
19
|
+
{ value: "free_shipping", label: "Free Shipping" },
|
|
20
|
+
{ value: "buy_x_get_y", label: "Buy X Get Y" },
|
|
21
|
+
];
|
|
22
|
+
export function CouponsIndexListing({ initialCoupons, storeSlug, sellerId, }) {
|
|
23
|
+
const table = useUrlTable({ defaults: { pageSize: "12", sort: "-createdAt" } });
|
|
24
|
+
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
25
|
+
const [filterOpen, setFilterOpen] = useState(false);
|
|
26
|
+
const [copiedCode, setCopiedCode] = useState(null);
|
|
27
|
+
// Build Sieve filter string
|
|
28
|
+
const buildFilters = () => {
|
|
29
|
+
const parts = ["validity.isActive==true"];
|
|
30
|
+
const typeFilter = table.get("type");
|
|
31
|
+
if (typeFilter)
|
|
32
|
+
parts.push(`type==${typeFilter}`);
|
|
33
|
+
const dateFrom = table.get("dateFrom");
|
|
34
|
+
if (dateFrom)
|
|
35
|
+
parts.push(`validity.startDate>=${dateFrom}`);
|
|
36
|
+
const dateTo = table.get("dateTo");
|
|
37
|
+
if (dateTo)
|
|
38
|
+
parts.push(`validity.endDate<=${dateTo}`);
|
|
39
|
+
if (sellerId)
|
|
40
|
+
parts.push(`sellerId==${sellerId}`);
|
|
41
|
+
return parts.join(",");
|
|
42
|
+
};
|
|
43
|
+
const { promotions: coupons, total, totalPages, isLoading } = usePromotions({
|
|
44
|
+
page: table.getNumber("page", 1),
|
|
45
|
+
pageSize: table.getNumber("pageSize", 12),
|
|
46
|
+
sort: table.get("sort") || "-createdAt",
|
|
47
|
+
filters: buildFilters(),
|
|
48
|
+
});
|
|
49
|
+
// Use initial data on first load if available and no search/filter active
|
|
50
|
+
const displayCoupons = !isLoading && coupons.length > 0
|
|
51
|
+
? coupons
|
|
52
|
+
: !isLoading && initialCoupons && !table.get("q") && !table.get("type")
|
|
53
|
+
? initialCoupons
|
|
54
|
+
: coupons;
|
|
55
|
+
const commitSearch = useCallback(() => {
|
|
56
|
+
table.set("q", searchInput.trim());
|
|
57
|
+
table.setPage(1);
|
|
58
|
+
}, [searchInput, table]);
|
|
59
|
+
const handleKeyDown = (e) => {
|
|
60
|
+
if (e.key === "Enter")
|
|
61
|
+
commitSearch();
|
|
62
|
+
};
|
|
63
|
+
const handleCopy = useCallback((code) => {
|
|
64
|
+
navigator.clipboard.writeText(code).catch(() => { });
|
|
65
|
+
setCopiedCode(code);
|
|
66
|
+
setTimeout(() => setCopiedCode(null), 2000);
|
|
67
|
+
}, []);
|
|
68
|
+
const activeType = table.get("type");
|
|
69
|
+
const hasActiveFilters = !!activeType || !!table.get("dateFrom") || !!table.get("dateTo");
|
|
70
|
+
const clearFilters = () => {
|
|
71
|
+
table.set("type", "");
|
|
72
|
+
table.set("dateFrom", "");
|
|
73
|
+
table.set("dateTo", "");
|
|
74
|
+
table.setPage(1);
|
|
75
|
+
};
|
|
76
|
+
return (_jsxs("div", { className: "min-h-[40vh]", children: [_jsxs("div", { className: "sticky top-0 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
|
|
77
|
+
? "border-primary bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
|
78
|
+
: "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("q", ""); table.setPage(1); }, 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("sort") || "-createdAt", onChange: (v) => { table.set("sort", v); table.setPage(1); }, 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", ""); table.setPage(1); }, "aria-label": "Remove type filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get("dateFrom") && (_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("dateFrom"), _jsx("button", { type: "button", onClick: () => { table.set("dateFrom", ""); table.setPage(1); }, "aria-label": "Remove from-date filter", children: _jsx(X, { className: "h-3 w-3" }) })] })), table.get("dateTo") && (_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("dateTo"), _jsx("button", { type: "button", onClick: () => { table.set("dateTo", ""); table.setPage(1); }, "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: {
|
|
79
|
+
copy: copiedCode === coupon.code ? "Copied!" : "Copy",
|
|
80
|
+
copied: "Copied!",
|
|
81
|
+
expires: "Expires",
|
|
82
|
+
minOrder: "Min. order",
|
|
83
|
+
off: "OFF",
|
|
84
|
+
freeShipping: "Free Shipping",
|
|
85
|
+
}, onCopy: handleCopy }, 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); table.setPage(1); }, className: "accent-primary" }), t.label] }, t.value))), table.get("type") && (_jsx("button", { type: "button", onClick: () => { table.set("type", ""); table.setPage(1); }, 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("dateFrom") || "", onChange: (e) => { table.set("dateFrom", e.target.value); table.setPage(1); }, 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("dateTo") || "", onChange: (e) => { table.set("dateTo", e.target.value); table.setPage(1); }, 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" })] })] })] }))] }));
|
|
86
|
+
}
|
|
@@ -9,12 +9,20 @@ export interface PromotionProductItem {
|
|
|
9
9
|
export interface PromotionsViewProductSectionProps {
|
|
10
10
|
title: string;
|
|
11
11
|
subtitle?: string;
|
|
12
|
-
/** Render the product grid — use your app's ProductGrid/ProductCard here */
|
|
13
12
|
renderProducts: () => React.ReactNode;
|
|
14
|
-
/** If false/undefined, the section is hidden */
|
|
15
13
|
hasProducts?: boolean;
|
|
16
14
|
}
|
|
17
15
|
export declare function PromotionsViewProductSection({ title, subtitle, renderProducts, hasProducts, }: PromotionsViewProductSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
16
|
+
export interface PromotionsHeroProps {
|
|
17
|
+
labels: {
|
|
18
|
+
exclusiveOffersBadge: string;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle: string;
|
|
21
|
+
};
|
|
22
|
+
heroBannerClass?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Slim hero banner for the promotions page — tabs + content live in the page. */
|
|
25
|
+
export declare function PromotionsHero({ labels, heroBannerClass, }: PromotionsHeroProps): import("react/jsx-runtime").JSX.Element;
|
|
18
26
|
export interface PromotionsViewProps {
|
|
19
27
|
labels: {
|
|
20
28
|
exclusiveOffersBadge: string;
|
|
@@ -32,12 +40,10 @@ export interface PromotionsViewProps {
|
|
|
32
40
|
};
|
|
33
41
|
hasContent: boolean;
|
|
34
42
|
heroBannerClass?: string;
|
|
35
|
-
/** Render the coupon grid */
|
|
36
43
|
renderCoupons?: () => React.ReactNode;
|
|
37
44
|
couponsCount?: number;
|
|
38
|
-
/** Render the promoted products section */
|
|
39
45
|
renderDealsSection?: () => React.ReactNode;
|
|
40
|
-
/** Render the featured products section */
|
|
41
46
|
renderFeaturedSection?: () => React.ReactNode;
|
|
42
47
|
}
|
|
48
|
+
/** @deprecated Use PromotionsHero + per-tab content in your page instead. */
|
|
43
49
|
export declare function PromotionsView({ labels, hasContent, heroBannerClass, renderCoupons, couponsCount, renderDealsSection, renderFeaturedSection, }: PromotionsViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -5,6 +5,11 @@ export function PromotionsViewProductSection({ title, subtitle, renderProducts,
|
|
|
5
5
|
return null;
|
|
6
6
|
return (_jsxs(Section, { children: [_jsxs(Div, { className: "mb-6", children: [_jsx(Heading, { level: 2, children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1", children: subtitle }))] }), renderProducts()] }));
|
|
7
7
|
}
|
|
8
|
+
/** Slim hero banner for the promotions page — tabs + content live in the page. */
|
|
9
|
+
export function PromotionsHero({ labels, heroBannerClass = "bg-gradient-to-br from-rose-500 to-orange-500", }) {
|
|
10
|
+
return (_jsx(Div, { className: `${heroBannerClass} text-white py-14`, children: _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center", children: [_jsxs(Text, { className: "text-white font-medium mb-2 uppercase tracking-widest text-sm", children: ["\uD83C\uDF89 ", labels.exclusiveOffersBadge] }), _jsx(Heading, { level: 1, variant: "none", className: "text-white mb-4", children: labels.title }), _jsx(Text, { variant: "none", className: "text-lg text-white/90 max-w-2xl mx-auto", children: labels.subtitle })] }) }));
|
|
11
|
+
}
|
|
12
|
+
/** @deprecated Use PromotionsHero + per-tab content in your page instead. */
|
|
8
13
|
export function PromotionsView({ labels, hasContent, heroBannerClass = "bg-gradient-to-br from-rose-500 to-orange-500", renderCoupons, couponsCount = 0, renderDealsSection, renderFeaturedSection, }) {
|
|
9
|
-
return (_jsxs(Div, { className: "min-h-screen bg-white dark:bg-slate-900", children: [_jsx(
|
|
14
|
+
return (_jsxs(Div, { className: "min-h-screen bg-white dark:bg-slate-900", children: [_jsx(PromotionsHero, { labels: labels, heroBannerClass: heroBannerClass }), _jsxs(Div, { className: "max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12", children: [!hasContent && (_jsxs(Div, { className: "text-center py-16", children: [_jsx(Heading, { level: 2, className: "mb-2", children: labels.emptyDeals }), _jsx(Text, { variant: "secondary", children: labels.checkBack })] })), hasContent && (_jsxs(Div, { className: "space-y-12", children: [renderCoupons && (_jsxs(Section, { children: [_jsxs(Div, { className: "mb-6", children: [_jsx(Heading, { level: 2, children: labels.couponsTitle }), couponsCount > 0 && (_jsx(Text, { variant: "secondary", className: "mt-1", children: labels.couponsSubtitle }))] }), couponsCount > 0 ? (renderCoupons()) : (_jsx(Text, { variant: "secondary", size: "sm", children: labels.emptyCoupons }))] })), renderDealsSection?.(), renderFeaturedSection?.()] }))] })] }));
|
|
10
15
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { CouponCard } from "./CouponCard";
|
|
2
|
-
export {
|
|
3
|
-
export type {
|
|
2
|
+
export { CouponsIndexListing } from "./CouponsIndexListing";
|
|
3
|
+
export type { CouponsIndexListingProps } from "./CouponsIndexListing";
|
|
4
|
+
export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
|
|
5
|
+
export type { PromotionsViewProps, PromotionsViewProductSectionProps, PromotionProductItem, PromotionsHeroProps, } from "./PromotionsView";
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { CouponCard } from "./CouponCard";
|
|
2
|
-
export {
|
|
2
|
+
export { CouponsIndexListing } from "./CouponsIndexListing";
|
|
3
|
+
export { PromotionsView, PromotionsViewProductSection, PromotionsHero, } from "./PromotionsView";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { reviewRepository, storeRepository } from "../../../repositories";
|
|
4
|
+
import { Container, Main, Section } from "../../../ui";
|
|
5
|
+
import { ROUTES } from "../../../next";
|
|
6
|
+
import { ReviewDetailShell } from "./ReviewDetailShell";
|
|
7
|
+
export async function ReviewDetailPageView({ id }) {
|
|
8
|
+
const review = await reviewRepository.findById(id).catch(() => null);
|
|
9
|
+
// Resolve store slug from sellerId so the shell can link to the store page
|
|
10
|
+
const storeSlug = review?.sellerId
|
|
11
|
+
? await storeRepository
|
|
12
|
+
.findAll({ filters: `ownerId==${review.sellerId}`, pageSize: 1 })
|
|
13
|
+
.then((r) => r?.items?.[0]?.storeSlug ?? null)
|
|
14
|
+
.catch(() => null)
|
|
15
|
+
: null;
|
|
16
|
+
if (!review || review.status !== "approved") {
|
|
17
|
+
return (_jsx(Main, { children: _jsx(Section, { className: "py-24", children: _jsx(Container, { size: "sm", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-5xl mb-4", "aria-hidden": "true", children: "\uD83D\uDD0D" }), _jsx("h1", { className: "text-2xl font-bold text-neutral-900 dark:text-white mb-2", children: "Review not found" }), _jsx("p", { className: "text-sm text-neutral-500 dark:text-zinc-400 mb-6", children: "This review may have been removed or is no longer available." }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-600 transition-colors", children: "\u2190 Back to Reviews" })] }) }) }) }));
|
|
18
|
+
}
|
|
19
|
+
return (_jsxs(Main, { children: [_jsx("div", { className: "border-b border-neutral-100 dark:border-zinc-800 bg-neutral-50 dark:bg-zinc-950 py-2.5 px-4", children: _jsxs("nav", { className: "mx-auto max-w-3xl flex items-center gap-1.5 text-xs text-neutral-400 dark:text-zinc-500", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary transition-colors", children: "Home" }), _jsx("span", { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "hover:text-primary transition-colors", children: "Reviews" }), _jsx("span", { children: "/" }), _jsx("span", { className: "text-neutral-700 dark:text-zinc-300 truncate max-w-[200px]", children: review.title ?? review.id })] }) }), _jsx(ReviewDetailShell, { review: review, storeHref: storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Review } from "../types";
|
|
2
|
+
interface ReviewDetailShellProps {
|
|
3
|
+
review: Review;
|
|
4
|
+
storeHref?: string | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function ReviewDetailShell({ review, storeHref }: ReviewDetailShellProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { StarRating } from "../../../ui";
|
|
6
|
+
import { RichText } from "../../../ui";
|
|
7
|
+
import { maskName } from "../../../security";
|
|
8
|
+
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
9
|
+
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
10
|
+
import { ROUTES } from "../../../next";
|
|
11
|
+
import { apiClient } from "../../../http";
|
|
12
|
+
import { REVIEW_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
|
+
export function ReviewDetailShell({ review, storeHref }) {
|
|
14
|
+
const displayName = maskName(review.userName);
|
|
15
|
+
const initials = displayName.charAt(0).toUpperCase();
|
|
16
|
+
const date = review.createdAt
|
|
17
|
+
? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
|
|
18
|
+
year: "numeric",
|
|
19
|
+
month: "long",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
})
|
|
22
|
+
: "";
|
|
23
|
+
// ── Lightbox ────────────────────────────────────────────────────────────────
|
|
24
|
+
const images = review.images ?? [];
|
|
25
|
+
const [lightboxIdx, setLightboxIdx] = useState(null);
|
|
26
|
+
const closeLightbox = useCallback(() => setLightboxIdx(null), []);
|
|
27
|
+
const prevImage = useCallback(() => setLightboxIdx((i) => (i === null ? null : (i - 1 + images.length) % images.length)), [images.length]);
|
|
28
|
+
const nextImage = useCallback(() => setLightboxIdx((i) => (i === null ? null : (i + 1) % images.length)), [images.length]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (lightboxIdx === null)
|
|
31
|
+
return;
|
|
32
|
+
const onKey = (e) => {
|
|
33
|
+
if (e.key === "Escape")
|
|
34
|
+
closeLightbox();
|
|
35
|
+
if (e.key === "ArrowLeft")
|
|
36
|
+
prevImage();
|
|
37
|
+
if (e.key === "ArrowRight")
|
|
38
|
+
nextImage();
|
|
39
|
+
};
|
|
40
|
+
window.addEventListener("keydown", onKey);
|
|
41
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
42
|
+
}, [lightboxIdx, closeLightbox, prevImage, nextImage]);
|
|
43
|
+
// ── Helpful vote ────────────────────────────────────────────────────────────
|
|
44
|
+
const storageKey = `review_voted_${review.id}`;
|
|
45
|
+
const [helpfulCount, setHelpfulCount] = useState(review.helpfulCount ?? 0);
|
|
46
|
+
const [voted, setVoted] = useState(false);
|
|
47
|
+
const [voting, setVoting] = useState(false);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setVoted(localStorage.getItem(storageKey) === "1");
|
|
50
|
+
}, [storageKey]);
|
|
51
|
+
const handleVote = async () => {
|
|
52
|
+
if (voted || voting)
|
|
53
|
+
return;
|
|
54
|
+
setVoting(true);
|
|
55
|
+
try {
|
|
56
|
+
await apiClient.post(`${REVIEW_ENDPOINTS.LIST}/${review.id}/vote`, {});
|
|
57
|
+
setHelpfulCount((c) => c + 1);
|
|
58
|
+
setVoted(true);
|
|
59
|
+
localStorage.setItem(storageKey, "1");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// silently fail
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setVoting(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// ── Links ───────────────────────────────────────────────────────────────────
|
|
69
|
+
const productHref = review.productId
|
|
70
|
+
? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
|
|
71
|
+
: null;
|
|
72
|
+
const sellerHref = storeHref ?? null;
|
|
73
|
+
const reviewerHref = String(ROUTES.PUBLIC.PROFILE(review.userId));
|
|
74
|
+
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
|
+
? "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-white scale-110"
|
|
79
|
+
: "border-transparent opacity-60 hover:opacity-100"}`, style: { backgroundImage: `url(${img.thumbnailUrl ?? img.url})` } }, i))) }))] }))] }));
|
|
80
|
+
}
|
|
@@ -2,9 +2,9 @@ import type { FacetOption } from "../../filters/FilterFacetSection";
|
|
|
2
2
|
import type { UrlTable } from "../../filters/FilterPanel";
|
|
3
3
|
export type ReviewFilterVariant = "admin" | "seller" | "public";
|
|
4
4
|
export declare const REVIEW_FILTER_KEYS: {
|
|
5
|
-
readonly admin: readonly ["status", "rating", "brand", "verified", "featured"];
|
|
6
|
-
readonly seller: readonly ["status", "rating", "brand"];
|
|
7
|
-
readonly public: readonly ["rating", "brand"];
|
|
5
|
+
readonly admin: readonly ["status", "rating", "brand", "verified", "featured", "dateFrom", "dateTo"];
|
|
6
|
+
readonly seller: readonly ["status", "rating", "brand", "dateFrom", "dateTo"];
|
|
7
|
+
readonly public: readonly ["rating", "brand", "dateFrom", "dateTo"];
|
|
8
8
|
};
|
|
9
9
|
export declare const REVIEW_ADMIN_SORT_OPTIONS: readonly [{
|
|
10
10
|
readonly value: "-createdAt";
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useTranslations } from "next-intl";
|
|
3
3
|
import { FilterFacetSection } from "../../filters/FilterFacetSection";
|
|
4
|
+
import { RangeFilter } from "../../filters/RangeFilter";
|
|
4
5
|
import { SwitchFilter } from "../../filters/SwitchFilter";
|
|
5
6
|
import { Div } from "../../../ui";
|
|
6
7
|
export const REVIEW_FILTER_KEYS = {
|
|
7
|
-
admin: ["status", "rating", "brand", "verified", "featured"],
|
|
8
|
-
seller: ["status", "rating", "brand"],
|
|
9
|
-
public: ["rating", "brand"],
|
|
8
|
+
admin: ["status", "rating", "brand", "verified", "featured", "dateFrom", "dateTo"],
|
|
9
|
+
seller: ["status", "rating", "brand", "dateFrom", "dateTo"],
|
|
10
|
+
public: ["rating", "brand", "dateFrom", "dateTo"],
|
|
10
11
|
};
|
|
11
12
|
export const REVIEW_ADMIN_SORT_OPTIONS = [
|
|
12
13
|
{ value: "-createdAt", key: "sortNewest" },
|
|
@@ -55,5 +56,5 @@ export function ReviewFilters({ table, variant = "admin", brandOptions, }) {
|
|
|
55
56
|
: [];
|
|
56
57
|
const isAdmin = variant === "admin";
|
|
57
58
|
const showStatus = variant !== "public";
|
|
58
|
-
return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), _jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" }), brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : ""), defaultCollapsed: true })] }))] }));
|
|
59
|
+
return (_jsxs(Div, { children: [showStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions, selected: selectedStatus, onChange: (vals) => table.set("status", vals.join("|")), searchable: false, defaultCollapsed: false })), _jsx(FilterFacetSection, { title: t("rating"), options: ratingOptions, selected: selectedRating, onChange: (vals) => table.set("rating", vals.join("|")), searchable: false, defaultCollapsed: variant === "admin" }), brandOptions && brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: table.get("brand") ? [table.get("brand")] : [], onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 6, selectionMode: "single", defaultCollapsed: true })), _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: true }), isAdmin && (_jsxs(_Fragment, { children: [_jsx(SwitchFilter, { title: t("verified"), label: t("showVerifiedOnly"), checked: table.get("verified") === "true", onChange: (v) => table.set("verified", v ? "true" : ""), defaultCollapsed: true }), _jsx(SwitchFilter, { title: t("featured"), label: t("showFeaturedOnly"), checked: table.get("featured") === "true", onChange: (v) => table.set("featured", v ? "true" : ""), defaultCollapsed: true })] })), _jsx(RangeFilter, { title: t("votesRange"), minValue: table.get("minVotes"), maxValue: table.get("maxVotes"), onMinChange: (v) => table.set("minVotes", v), onMaxChange: (v) => table.set("maxVotes", v), minBound: 0, maxBound: 10000, step: 1, minPlaceholder: t("minVotes"), maxPlaceholder: t("maxVotes"), defaultCollapsed: true })] }));
|
|
59
60
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReviewListResponse } from "../types";
|
|
2
2
|
export interface ReviewsIndexListingProps {
|
|
3
|
-
|
|
3
|
+
initialData?: ReviewListResponse;
|
|
4
|
+
variant?: "admin" | "seller" | "public";
|
|
4
5
|
}
|
|
5
|
-
export declare function ReviewsIndexListing({
|
|
6
|
+
export declare function ReviewsIndexListing({ initialData, variant, }: ReviewsIndexListingProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,55 +1,39 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
5
|
+
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
|
+
import { Pagination, SortDropdown } from "../../../ui";
|
|
5
7
|
import { ReviewCard } from "./ReviewsList";
|
|
8
|
+
import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
|
|
9
|
+
import { useReviews } from "../hooks/useReviews";
|
|
6
10
|
const PAGE_SIZE = 12;
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return a.rating - b.rating;
|
|
36
|
-
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
37
|
-
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
38
|
-
return sort === "createdAt" ? aTime - bTime : bTime - aTime;
|
|
39
|
-
});
|
|
40
|
-
return result;
|
|
41
|
-
}, [reviews, sort, starFilter]);
|
|
42
|
-
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
|
43
|
-
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
44
|
-
const handleSort = (val) => {
|
|
45
|
-
setSort(val);
|
|
46
|
-
setPage(1);
|
|
47
|
-
};
|
|
48
|
-
const handleStarFilter = (val) => {
|
|
49
|
-
setStarFilter(val);
|
|
50
|
-
setPage(1);
|
|
51
|
-
};
|
|
52
|
-
return (_jsx(Div, { className: "min-h-screen", children: _jsx(SlottedListingView, { portal: "public", manageSearch: false, manageSort: false, inlineToolbar: true, renderSort: () => (_jsx(SortDropdown, { value: sort, onChange: handleSort, options: SORT_OPTIONS })), renderFilters: () => (_jsxs(Div, { className: "space-y-2 p-4", children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Filter by Rating" }), STAR_OPTIONS.map((opt) => (_jsx("button", { type: "button", onClick: () => handleStarFilter(opt.value), className: `block w-full rounded-lg px-3 py-2 text-left text-sm transition ${starFilter === opt.value
|
|
53
|
-
? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
|
|
54
|
-
: "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-slate-800"}`, children: opt.label }, opt.value)))] })), renderTable: () => paginated.length === 0 ? (_jsx(Stack, { align: "center", gap: "3", className: "justify-center py-24 text-center", children: _jsx(Text, { className: "text-xl font-medium text-zinc-900 dark:text-zinc-50", children: "No reviews found" }) })) : (_jsx(Grid, { cols: 3, gap: "md", children: paginated.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })), renderPagination: () => totalPages > 1 ? (_jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage })) : null, total: filtered.length, isLoading: false }) }));
|
|
11
|
+
export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
12
|
+
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-createdAt" } });
|
|
13
|
+
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
14
|
+
const [filterOpen, setFilterOpen] = useState(false);
|
|
15
|
+
const sort = table.get("sort") || "-createdAt";
|
|
16
|
+
const currentPage = table.getNumber("page", 1);
|
|
17
|
+
const commitSearch = useCallback(() => {
|
|
18
|
+
table.set("q", searchInput.trim());
|
|
19
|
+
table.setPage(1);
|
|
20
|
+
}, [searchInput, table]);
|
|
21
|
+
const closeFilters = () => setFilterOpen(false);
|
|
22
|
+
const ratingRaw = table.get("rating");
|
|
23
|
+
const { reviews, total, totalPages, isLoading } = useReviews({
|
|
24
|
+
q: table.get("q") || undefined,
|
|
25
|
+
rating: ratingRaw || undefined,
|
|
26
|
+
dateFrom: table.get("dateFrom") || undefined,
|
|
27
|
+
dateTo: table.get("dateTo") || undefined,
|
|
28
|
+
minVotes: table.get("minVotes") ? Number(table.get("minVotes")) : undefined,
|
|
29
|
+
maxVotes: table.get("maxVotes") ? Number(table.get("maxVotes")) : undefined,
|
|
30
|
+
sort,
|
|
31
|
+
page: currentPage,
|
|
32
|
+
perPage: PAGE_SIZE,
|
|
33
|
+
}, { initialData });
|
|
34
|
+
const sortOptions = REVIEW_PUBLIC_SORT_OPTIONS.map((opt) => ({
|
|
35
|
+
value: opt.value,
|
|
36
|
+
label: opt.key,
|
|
37
|
+
}));
|
|
38
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx("div", { className: "sticky top-0 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 border-zinc-300 dark:border-slate-600 px-3.5 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: "Filters" })] }), _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: (e) => e.key === "Enter" && commitSearch(), placeholder: "Search reviews by product name...", 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("q", ""); table.setPage(1); }, className: "px-2 text-zinc-400 hover:text-zinc-600 transition-colors", "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: sort, onChange: (v) => { table.set("sort", v); table.setPage(1); }, options: sortOptions })] })] }) }), _jsxs("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." })) : (_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))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: closeFilters }), _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: closeFilters, "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: table, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsx("button", { type: "button", onClick: closeFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply filters" }) })] })] }))] }));
|
|
55
39
|
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
type SearchParams = Record<string, string | string[]>;
|
|
2
|
+
export interface ReviewsIndexPageViewProps {
|
|
3
|
+
searchParams?: SearchParams;
|
|
4
|
+
}
|
|
5
|
+
export declare function ReviewsIndexPageView({ searchParams }: ReviewsIndexPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
6
|
+
export {};
|
|
@@ -3,7 +3,53 @@ import { reviewRepository } from "../../../repositories";
|
|
|
3
3
|
import { Container, Heading, Main, Section } from "../../../ui";
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
5
5
|
import { ReviewsIndexListing } from "./ReviewsIndexListing";
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
return (
|
|
6
|
+
function sp(params, key) {
|
|
7
|
+
const v = params[key];
|
|
8
|
+
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
9
|
+
}
|
|
10
|
+
function buildReviewFilters(params) {
|
|
11
|
+
const parts = ["status==approved"];
|
|
12
|
+
const rating = sp(params, "rating");
|
|
13
|
+
if (rating) {
|
|
14
|
+
const values = rating.split("|").filter(Boolean);
|
|
15
|
+
if (values.length === 1)
|
|
16
|
+
parts.push(`rating==${values[0]}`);
|
|
17
|
+
else if (values.length > 1)
|
|
18
|
+
parts.push(`rating==${values.join("|")}`);
|
|
19
|
+
}
|
|
20
|
+
const minVotes = sp(params, "minVotes");
|
|
21
|
+
if (minVotes)
|
|
22
|
+
parts.push(`helpfulCount>=${minVotes}`);
|
|
23
|
+
const maxVotes = sp(params, "maxVotes");
|
|
24
|
+
if (maxVotes)
|
|
25
|
+
parts.push(`helpfulCount<=${maxVotes}`);
|
|
26
|
+
const dateFrom = sp(params, "dateFrom");
|
|
27
|
+
const dateTo = sp(params, "dateTo");
|
|
28
|
+
if (dateFrom)
|
|
29
|
+
parts.push(`createdAt>=${dateFrom}`);
|
|
30
|
+
if (dateTo)
|
|
31
|
+
parts.push(`createdAt<=${dateTo}`);
|
|
32
|
+
const q = sp(params, "q");
|
|
33
|
+
if (q)
|
|
34
|
+
parts.push(`productTitle@=*${q}`);
|
|
35
|
+
return parts.join(",");
|
|
36
|
+
}
|
|
37
|
+
export async function ReviewsIndexPageView({ searchParams = {} }) {
|
|
38
|
+
const sort = sp(searchParams, "sort") || "-createdAt";
|
|
39
|
+
const page = Number(sp(searchParams, "page")) || 1;
|
|
40
|
+
const filters = buildReviewFilters(searchParams);
|
|
41
|
+
const result = await reviewRepository
|
|
42
|
+
.listAll({ filters, sorts: sort, page, pageSize: 12 })
|
|
43
|
+
.catch(() => null);
|
|
44
|
+
const initialData = result
|
|
45
|
+
? {
|
|
46
|
+
items: result.items,
|
|
47
|
+
total: result.total,
|
|
48
|
+
page: result.page,
|
|
49
|
+
pageSize: result.pageSize,
|
|
50
|
+
totalPages: result.totalPages,
|
|
51
|
+
hasMore: result.hasMore,
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
54
|
+
return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8 text-3xl font-semibold text-zinc-900 dark:text-zinc-50", children: "Reviews" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(ReviewsIndexListing, { initialData: initialData }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
9
55
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
2
3
|
import { Div, Heading, Pagination, RichText, Row, Skeleton, Span, Stack, Text, } from "../../../ui";
|
|
3
4
|
import { StarRating } from "../../../ui";
|
|
5
|
+
import { THEME_CONSTANTS } from "../../../tokens";
|
|
4
6
|
import { maskName } from "../../../security";
|
|
5
7
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
6
8
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
9
|
+
import { ROUTES } from "../../../next";
|
|
7
10
|
export function ReviewCard({ review, className = "" }) {
|
|
8
11
|
const date = review.createdAt
|
|
9
12
|
? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
|
|
@@ -14,7 +17,12 @@ export function ReviewCard({ review, className = "" }) {
|
|
|
14
17
|
: "";
|
|
15
18
|
const displayName = maskName(review.userName);
|
|
16
19
|
const initials = displayName.charAt(0).toUpperCase();
|
|
17
|
-
|
|
20
|
+
const reviewHref = String(ROUTES.PUBLIC.REVIEW_DETAIL(review.id));
|
|
21
|
+
const productHref = review.productId
|
|
22
|
+
? String(ROUTES.PUBLIC.PRODUCT_DETAIL(review.productId))
|
|
23
|
+
: null;
|
|
24
|
+
const card = (_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 cursor-pointer ${className}`, 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"] })), productHref && (_jsxs(Div, { className: "mt-auto pt-3 flex items-center gap-1.5 text-xs font-medium text-neutral-500 dark:text-zinc-400 border-t border-neutral-100 dark:border-zinc-800", 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" })] }))] }));
|
|
25
|
+
return (_jsx(Link, { href: reviewHref, className: "block h-full", children: card }));
|
|
18
26
|
}
|
|
19
27
|
export function ReviewsList({ reviews, isLoading, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No reviews yet", }) {
|
|
20
28
|
if (isLoading) {
|
|
@@ -10,3 +10,4 @@ export { ReviewsListView } from "./ReviewsListView";
|
|
|
10
10
|
export type { ReviewsListViewProps } from "./ReviewsListView";
|
|
11
11
|
export { ReviewsIndexListing } from "./ReviewsIndexListing";
|
|
12
12
|
export type { ReviewsIndexListingProps } from "./ReviewsIndexListing";
|
|
13
|
+
export type { ReviewDetailPageViewProps } from "./ReviewDetailPageView";
|