@mohasinac/appkit 2.7.19 → 2.7.20
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/client/features/layout/DashboardLayoutClient.js +12 -8
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +15 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +19 -0
- package/dist/_internal/server/features/site-settings/actions.d.ts +14 -0
- package/dist/_internal/server/features/site-settings/actions.js +29 -0
- package/dist/_internal/shared/features/layout/types.d.ts +11 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +2 -0
- package/dist/contracts/auth.d.ts +2 -0
- package/dist/features/admin/utils/checkActionAllowed.d.ts +4 -0
- package/dist/features/admin/utils/checkActionAllowed.js +21 -0
- package/dist/features/admin/utils/getDisabledRoutes.d.ts +2 -0
- package/dist/features/admin/utils/getDisabledRoutes.js +6 -0
- package/dist/features/admin/utils/getSiteSettingsGlobal.d.ts +2 -0
- package/dist/features/admin/utils/getSiteSettingsGlobal.js +4 -0
- package/dist/features/categories/components/CategoryProductsListing.js +18 -13
- package/dist/features/layout/AppLayoutShell.js +8 -1
- package/dist/features/layout/ListingLayout.js +1 -1
- package/dist/features/layout/MainNavbar.d.ts +4 -0
- package/dist/features/layout/NavItem.js +1 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +22 -14
- package/dist/features/products/components/AuctionsIndexListing.js +19 -16
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/products/components/ProductsIndexListing.js +34 -26
- package/dist/features/stores/components/StoreAuctionsListing.js +15 -8
- package/dist/features/stores/components/StoreProductsListing.js +18 -13
- package/dist/react/contexts/SessionContext.d.ts +2 -0
- package/dist/react/hooks/useAuthGate.d.ts +8 -0
- package/dist/react/hooks/useAuthGate.js +35 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +6 -0
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,8 @@ import { X, ShoppingCart, Heart, SlidersHorizontal, Columns } from "lucide-react
|
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
7
7
|
import { useProducts } from "../hooks/useProducts";
|
|
8
|
-
import { Pagination, useToast, BulkActionBar, ListingToolbar } from "../../../ui";
|
|
8
|
+
import { Pagination, useToast, BulkActionBar, ListingToolbar, LoginRequiredModal } from "../../../ui";
|
|
9
|
+
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
10
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-defs";
|
|
10
11
|
import { CompareOverlay } from "./CompareOverlay";
|
|
11
12
|
import { ROUTES } from "../../../next";
|
|
@@ -24,6 +25,7 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
24
25
|
const router = useRouter();
|
|
25
26
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
26
27
|
const { showToast } = useToast();
|
|
28
|
+
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
27
29
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
28
30
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
29
31
|
const [compareIds, setCompareIds] = useState([]);
|
|
@@ -123,17 +125,19 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
123
125
|
};
|
|
124
126
|
const handleWishlistToggle = useCallback((productId) => {
|
|
125
127
|
const isWishlisted = wishlistedIds.has(productId);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
requireAuth(isWishlisted ? ACTION_ID.REMOVE_FROM_WISHLIST : ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
129
|
+
if (isWishlisted) {
|
|
130
|
+
localWishlist.remove(productId, "product");
|
|
131
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "product" });
|
|
132
|
+
showToast("Removed from wishlist", "info");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
localWishlist.add(productId, "product");
|
|
136
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "product" });
|
|
137
|
+
showToast("Added to wishlist", "success");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}, [wishlistedIds, localWishlist, showToast, requireAuth]);
|
|
137
141
|
const handleAddToCart = useCallback((product) => {
|
|
138
142
|
localCart.add(product.id, 1, {
|
|
139
143
|
productTitle: product.title,
|
|
@@ -144,14 +148,16 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
144
148
|
showToast("Added to cart", "success");
|
|
145
149
|
}, [localCart, showToast]);
|
|
146
150
|
const handleBuyNow = useCallback((product) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
requireAuth(ACTION_ID.BUY_NOW, () => {
|
|
152
|
+
localCart.add(product.id, 1, {
|
|
153
|
+
productTitle: product.title,
|
|
154
|
+
productImage: product.mainImage,
|
|
155
|
+
price: product.price,
|
|
156
|
+
});
|
|
157
|
+
pushCartOp({ op: "add", productId: product.id, quantity: 1, productTitle: product.title, productImage: product.mainImage, price: product.price });
|
|
158
|
+
router.push(String(ROUTES.USER.CART));
|
|
151
159
|
});
|
|
152
|
-
|
|
153
|
-
router.push(String(ROUTES.USER.CART));
|
|
154
|
-
}, [localCart, router]);
|
|
160
|
+
}, [localCart, router, requireAuth]);
|
|
155
161
|
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show sold" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showSold, onClick: () => table.set(TABLE_KEYS.SHOW_SOLD, showSold ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showSold ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showSold ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
156
162
|
{
|
|
157
163
|
id: ACTION_ID.ADD_TO_CART,
|
|
@@ -174,13 +180,15 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
174
180
|
icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
|
|
175
181
|
variant: "secondary",
|
|
176
182
|
onClick: () => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
requireAuth(ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
184
|
+
const selected = products.filter((p) => selection.selectedIdSet.has(p.id));
|
|
185
|
+
selected.forEach((p) => {
|
|
186
|
+
localWishlist.add(p.id, "product");
|
|
187
|
+
pushWishlistOp({ op: "add", itemId: p.id, type: "product" });
|
|
188
|
+
});
|
|
189
|
+
showToast(`${selected.length} items added to wishlist`, "success");
|
|
190
|
+
selection.clearSelection();
|
|
181
191
|
});
|
|
182
|
-
showToast(`${selected.length} items added to wishlist`, "success");
|
|
183
|
-
selection.clearSelection();
|
|
184
192
|
},
|
|
185
193
|
},
|
|
186
194
|
{
|
|
@@ -197,5 +205,5 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
197
205
|
] }), 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view === "grid" ? "card" : "list", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart, onBuyNow: handleBuyNow, selectionMode: selection.isSelecting, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "product", onClose: () => {
|
|
198
206
|
setCompareIds([]);
|
|
199
207
|
selection.clearSelection();
|
|
200
|
-
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== 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: [_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"] }), _jsxs("div", { className: "flex items-center gap-2", children: [pendingFilterCount > 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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions }) }), _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", pendingFilterCount > 0 ? ` (${pendingFilterCount})` : ""] }) })] })] }))] }));
|
|
208
|
+
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== 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: [_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"] }), _jsxs("div", { className: "flex items-center gap-2", children: [pendingFilterCount > 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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions }) }), _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", pendingFilterCount > 0 ? ` (${pendingFilterCount})` : ""] }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
201
209
|
}
|
|
@@ -4,7 +4,9 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { SlidersHorizontal, X } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Pagination, useToast, ListingToolbar } from "../../../ui";
|
|
7
|
+
import { Pagination, useToast, ListingToolbar, LoginRequiredModal } from "../../../ui";
|
|
8
|
+
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
|
+
import { ACTION_ID } from "../../products/constants/action-defs";
|
|
8
10
|
import { MarketplaceAuctionGrid } from "../../auctions/components/MarketplaceAuctionGrid";
|
|
9
11
|
import { ProductFilters } from "../../products/components/ProductFilters";
|
|
10
12
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
@@ -21,6 +23,7 @@ const FILTER_KEYS = ["minPrice", "maxPrice"];
|
|
|
21
23
|
export function StoreAuctionsListing({ storeId, initialData }) {
|
|
22
24
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: "auctionEndDate" } });
|
|
23
25
|
const { showToast } = useToast();
|
|
26
|
+
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
24
27
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
25
28
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
26
29
|
const [view, setView] = useState(table.get("view") || "grid");
|
|
@@ -113,20 +116,24 @@ export function StoreAuctionsListing({ storeId, initialData }) {
|
|
|
113
116
|
}, [searchInput, table]);
|
|
114
117
|
const wishlistActions = {
|
|
115
118
|
addToWishlist: (productId) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
requireAuth(ACTION_ID.WATCH_AUCTION, () => {
|
|
120
|
+
localWishlist.add(productId, "auction");
|
|
121
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "auction" });
|
|
122
|
+
showToast("Added to wishlist", "success");
|
|
123
|
+
});
|
|
119
124
|
return Promise.resolve();
|
|
120
125
|
},
|
|
121
126
|
removeFromWishlist: (productId) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
requireAuth(ACTION_ID.UNWATCH_AUCTION, () => {
|
|
128
|
+
localWishlist.remove(productId, "auction");
|
|
129
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "auction" });
|
|
130
|
+
showToast("Removed from wishlist", "info");
|
|
131
|
+
});
|
|
125
132
|
return Promise.resolve();
|
|
126
133
|
},
|
|
127
134
|
isWishlisted: (productId) => wishlistedIds.has(productId),
|
|
128
135
|
};
|
|
129
136
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4";
|
|
130
137
|
return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "auctionEndDate", sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: (v) => { if (v === "table")
|
|
131
|
-
return; setView(v); table.set("view", v); }, 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : (_jsx(MarketplaceAuctionGrid, { auctions: auctions, variant: view === "list" ? "list" : "grid", gridClassName: view === "list" ? "flex flex-col gap-4" : gridClass, wishlistActions: wishlistActions, labels: { emptyTitle: "No auctions yet", emptyDescription: "This store has no active auctions." } })) }), 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"] }), _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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9" }) }), _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})` : ""] }) })] })] }))] }));
|
|
138
|
+
return; setView(v); table.set("view", v); }, 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : (_jsx(MarketplaceAuctionGrid, { auctions: auctions, variant: view === "list" ? "list" : "grid", gridClassName: view === "list" ? "flex flex-col gap-4" : gridClass, wishlistActions: wishlistActions, labels: { emptyTitle: "No auctions yet", emptyDescription: "This store has no active auctions." } })) }), 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"] }), _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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9" }) }), _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})` : ""] }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
132
139
|
}
|
|
@@ -4,7 +4,9 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { SlidersHorizontal, X } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Pagination, useToast, ListingToolbar } from "../../../ui";
|
|
7
|
+
import { Pagination, useToast, ListingToolbar, LoginRequiredModal } from "../../../ui";
|
|
8
|
+
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
|
+
import { ACTION_ID } from "../../products/constants/action-defs";
|
|
8
10
|
import { ROUTES } from "../../../next";
|
|
9
11
|
import { ProductGrid } from "../../products/components/ProductGrid";
|
|
10
12
|
import { ProductFilters, PRODUCT_PUBLIC_SORT_OPTIONS } from "../../products/components/ProductFilters";
|
|
@@ -15,6 +17,7 @@ const FILTER_KEYS = ["condition", "brand", "minPrice", "maxPrice"];
|
|
|
15
17
|
export function StoreProductsListing({ storeId, initialData }) {
|
|
16
18
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
|
|
17
19
|
const { showToast } = useToast();
|
|
20
|
+
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
18
21
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
19
22
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
20
23
|
const [view, setView] = useState(table.get("view") || "card");
|
|
@@ -96,17 +99,19 @@ export function StoreProductsListing({ storeId, initialData }) {
|
|
|
96
99
|
};
|
|
97
100
|
const handleWishlistToggle = useCallback((productId) => {
|
|
98
101
|
const isWishlisted = wishlistedIds.has(productId);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
requireAuth(isWishlisted ? ACTION_ID.REMOVE_FROM_WISHLIST : ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
103
|
+
if (isWishlisted) {
|
|
104
|
+
localWishlist.remove(productId, "product");
|
|
105
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "product" });
|
|
106
|
+
showToast("Removed from wishlist", "info");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
localWishlist.add(productId, "product");
|
|
110
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "product" });
|
|
111
|
+
showToast("Added to wishlist", "success");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}, [wishlistedIds, localWishlist, showToast, requireAuth]);
|
|
110
115
|
const handleAddToCart = useCallback((product) => {
|
|
111
116
|
localCart.add(product.id, 1, {
|
|
112
117
|
productTitle: product.title,
|
|
@@ -123,5 +128,5 @@ export function StoreProductsListing({ storeId, initialData }) {
|
|
|
123
128
|
});
|
|
124
129
|
showToast("Added to cart", "success");
|
|
125
130
|
}, [localCart, showToast]);
|
|
126
|
-
return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view, emptyLabel: "This store has no products yet.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })) }), 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"] }), _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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9" }) }), _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})` : ""] }) })] })] }))] }));
|
|
131
|
+
return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search store products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), 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: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 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-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view, emptyLabel: "This store has no products yet.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })) }), 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"] }), _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(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9" }) }), _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})` : ""] }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
127
132
|
}
|
|
@@ -30,6 +30,8 @@ export interface SessionUser {
|
|
|
30
30
|
stats?: Record<string, unknown>;
|
|
31
31
|
metadata?: Record<string, unknown>;
|
|
32
32
|
scamAwarenessAcknowledgedAt?: Date | null;
|
|
33
|
+
/** RBAC permission keys granted to this user (from their role's permission set). */
|
|
34
|
+
permissions?: string[];
|
|
33
35
|
}
|
|
34
36
|
export interface SessionContextValue {
|
|
35
37
|
user: SessionUser | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ActionId } from "../../features/products/constants/action-defs";
|
|
2
|
+
export interface UseAuthGateReturn {
|
|
3
|
+
requireAuth: (actionId: ActionId, fn: () => void | Promise<void>) => void;
|
|
4
|
+
modalOpen: boolean;
|
|
5
|
+
modalMessage: string;
|
|
6
|
+
closeModal: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useAuthGate(): UseAuthGateReturn;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { useAuth } from "../contexts/SessionContext";
|
|
4
|
+
import { useSiteSettings } from "../../core/hooks/useSiteSettings";
|
|
5
|
+
import { ACTION_META } from "../../features/products/constants/action-defs";
|
|
6
|
+
const DEFAULT_MSG = "You need to be signed in to continue.";
|
|
7
|
+
const DISABLED_MSG = "This action is currently unavailable.";
|
|
8
|
+
const PERMISSION_MSG = "You don't have permission to perform this action.";
|
|
9
|
+
export function useAuthGate() {
|
|
10
|
+
const { user } = useAuth();
|
|
11
|
+
const { data: settings } = useSiteSettings();
|
|
12
|
+
const [state, setState] = useState({ open: false, message: "" });
|
|
13
|
+
const requireAuth = useCallback((actionId, fn) => {
|
|
14
|
+
const meta = ACTION_META[actionId];
|
|
15
|
+
const enabled = settings
|
|
16
|
+
?.actionConfig?.[actionId]?.enabled ??
|
|
17
|
+
meta?.defaultEnabled ??
|
|
18
|
+
true;
|
|
19
|
+
if (!enabled) {
|
|
20
|
+
setState({ open: true, message: DISABLED_MSG });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (meta?.requiresAuth && !user?.uid) {
|
|
24
|
+
setState({ open: true, message: meta.authMessage ?? DEFAULT_MSG });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (meta?.requiredPermission && !user?.permissions?.includes(meta.requiredPermission)) {
|
|
28
|
+
setState({ open: true, message: PERMISSION_MSG });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
void fn();
|
|
32
|
+
}, [user, settings]);
|
|
33
|
+
const closeModal = useCallback(() => setState((s) => ({ ...s, open: false })), []);
|
|
34
|
+
return { requireAuth, modalOpen: state.open, modalMessage: state.message, closeModal };
|
|
35
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -487,3 +487,7 @@ export type { UserSoftBan } from "./features/auth/schemas/firestore";
|
|
|
487
487
|
export type { BannedAction } from "./features/auth/permissions/constants";
|
|
488
488
|
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE } from "./_internal/server/features/seo";
|
|
489
489
|
export type { SitemapOptions, RobotsOptions, ManifestOptions, DefaultOgOptions } from "./_internal/server/features/seo";
|
|
490
|
+
export { checkActionAllowed } from "./features/admin/utils/checkActionAllowed";
|
|
491
|
+
export { getDisabledRoutes } from "./features/admin/utils/getDisabledRoutes";
|
|
492
|
+
export { getSiteSettingsGlobal } from "./features/admin/utils/getSiteSettingsGlobal";
|
|
493
|
+
export { updateActionConfigDomain, updateNavConfigDomain } from "./_internal/server/features/site-settings/actions";
|
package/dist/server.js
CHANGED
|
@@ -1362,3 +1362,9 @@ export { getStoreCapabilities, storeHasCapability, } from "./_internal/server/fe
|
|
|
1362
1362
|
export { isSoftBanned, getBanSummary } from "./features/auth/server/checkSoftBan";
|
|
1363
1363
|
// -- SEO builders (sitemap / robots / manifest / og-image) --
|
|
1364
1364
|
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE } from "./_internal/server/features/seo";
|
|
1365
|
+
// -- Action gate + nav route helpers (server-side) --
|
|
1366
|
+
export { checkActionAllowed } from "./features/admin/utils/checkActionAllowed";
|
|
1367
|
+
export { getDisabledRoutes } from "./features/admin/utils/getDisabledRoutes";
|
|
1368
|
+
export { getSiteSettingsGlobal } from "./features/admin/utils/getSiteSettingsGlobal";
|
|
1369
|
+
// -- Site-settings domain actions (updateActionConfig / updateNavConfig) --
|
|
1370
|
+
export { updateActionConfigDomain, updateNavConfigDomain } from "./_internal/server/features/site-settings/actions";
|