@mohasinac/appkit 2.7.18 → 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/seo/manifest.js +9 -3
- 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/schemas/firestore.d.ts +21 -0
- package/dist/features/admin/schemas/firestore.js +3 -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/products/constants/action-defs.d.ts +64 -22
- package/dist/features/products/constants/action-defs.js +101 -64
- 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/seed/site-settings-seed-data.js +3 -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
|
@@ -4,7 +4,7 @@ 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 "../hooks/useProducts";
|
|
7
|
-
import { Pagination, useToast, ListingToolbar, BulkActionBar } from "../../../ui";
|
|
7
|
+
import { Pagination, useToast, ListingToolbar, BulkActionBar, LoginRequiredModal } from "../../../ui";
|
|
8
8
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
9
9
|
import { MarketplaceAuctionGrid } from "../../auctions/components/MarketplaceAuctionGrid";
|
|
10
10
|
import { AuctionFilters } from "../../auctions/components/AuctionFilters";
|
|
@@ -15,6 +15,8 @@ import { useBrands } from "../hooks/useBrands";
|
|
|
15
15
|
import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
|
|
16
16
|
import { sortBy } from "../../../constants/sort";
|
|
17
17
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
18
|
+
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
19
|
+
import { ACTION_ID } from "../constants/action-defs";
|
|
18
20
|
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC");
|
|
19
21
|
const AUCTION_SORT_OPTIONS = [
|
|
20
22
|
{ value: sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC"), label: "Ending Soonest" },
|
|
@@ -28,6 +30,7 @@ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_BID,
|
|
|
28
30
|
export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
29
31
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
30
32
|
const { showToast } = useToast();
|
|
33
|
+
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
31
34
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
32
35
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
33
36
|
const showEnded = table.get(TABLE_KEYS.SHOW_ENDED) === "true";
|
|
@@ -127,15 +130,19 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
127
130
|
};
|
|
128
131
|
const wishlistActions = {
|
|
129
132
|
addToWishlist: (productId) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
requireAuth(ACTION_ID.WATCH_AUCTION, () => {
|
|
134
|
+
localWishlist.add(productId, "auction");
|
|
135
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "auction" });
|
|
136
|
+
showToast("Added to wishlist", "success");
|
|
137
|
+
});
|
|
133
138
|
return Promise.resolve();
|
|
134
139
|
},
|
|
135
140
|
removeFromWishlist: (productId) => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
requireAuth(ACTION_ID.UNWATCH_AUCTION, () => {
|
|
142
|
+
localWishlist.remove(productId, "auction");
|
|
143
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "auction" });
|
|
144
|
+
showToast("Removed from wishlist", "info");
|
|
145
|
+
});
|
|
139
146
|
return Promise.resolve();
|
|
140
147
|
},
|
|
141
148
|
isWishlisted: (productId) => wishlistedIds.has(productId),
|
|
@@ -143,28 +150,24 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
143
150
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-3 gap-4";
|
|
144
151
|
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_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: auctions.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 ended" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showEnded, onClick: () => table.set(TABLE_KEYS.SHOW_ENDED, showEnded ? "" : "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 ${showEnded ? "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 ${showEnded ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
145
152
|
{
|
|
146
|
-
id:
|
|
153
|
+
id: ACTION_ID.WATCH_AUCTION,
|
|
147
154
|
label: "Add to Watchlist",
|
|
148
155
|
variant: "primary",
|
|
149
156
|
onClick: () => {
|
|
150
157
|
const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
|
|
151
|
-
selected.forEach((a) => {
|
|
152
|
-
wishlistActions.addToWishlist(a.id);
|
|
153
|
-
});
|
|
158
|
+
selected.forEach((a) => { wishlistActions.addToWishlist(a.id); });
|
|
154
159
|
selection.clearSelection();
|
|
155
160
|
},
|
|
156
161
|
},
|
|
157
162
|
{
|
|
158
|
-
id:
|
|
163
|
+
id: ACTION_ID.UNWATCH_AUCTION,
|
|
159
164
|
label: "Remove from Watchlist",
|
|
160
165
|
variant: "secondary",
|
|
161
166
|
onClick: () => {
|
|
162
167
|
const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
|
|
163
|
-
selected.forEach((a) => {
|
|
164
|
-
wishlistActions.removeFromWishlist(a.id);
|
|
165
|
-
});
|
|
168
|
+
selected.forEach((a) => { wishlistActions.removeFromWishlist(a.id); });
|
|
166
169
|
selection.clearSelection();
|
|
167
170
|
},
|
|
168
171
|
},
|
|
169
|
-
] }), 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-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _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 })) }), 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(AuctionFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _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})` : ""] }) })] })] }))] }));
|
|
172
|
+
] }), 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-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _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 })) }), 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(AuctionFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _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 })] }));
|
|
170
173
|
}
|
|
@@ -81,7 +81,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
81
81
|
e.stopPropagation();
|
|
82
82
|
e.preventDefault();
|
|
83
83
|
onAddToCart(product);
|
|
84
|
-
}, className: "flex items-center justify-center gap-1 rounded-xl border-2 border-primary/40 bg-primary/5 py-2 text-xs font-semibold text-
|
|
84
|
+
}, className: "flex items-center justify-center gap-1 rounded-xl border-2 border-primary/40 bg-primary/5 py-2 text-xs font-semibold text-zinc-800 hover:bg-primary/10 hover:border-primary/60 active:scale-[0.97] transition-all duration-150 dark:text-zinc-100 dark:border-primary-400/50 dark:bg-primary/[0.08] dark:hover:bg-primary/[0.15] dark:hover:border-primary-400/70", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
|
|
85
85
|
if (href && !selectionMode) {
|
|
86
86
|
return (_jsx(Link, { href: href, className: "block h-full", children: cardBody }));
|
|
87
87
|
}
|
|
@@ -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
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* action-defs.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Universal CTA registry for the entire platform.
|
|
5
|
+
* Every action — public marketplace CTAs, dashboard row actions, quick actions — is
|
|
6
|
+
* registered here with auth requirements, RBAC permissions, and enable/disable defaults.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Dashboard quick bar — top-level shortcut buttons per dashboard type
|
|
12
|
-
*
|
|
13
|
-
* Pure TypeScript — no React, no JSX, no callbacks.
|
|
14
|
-
* Consuming components resolve iconName → Lucide component, and supply onClick.
|
|
8
|
+
* Three enforcement layers read from this registry:
|
|
9
|
+
* Client — useAuthGate() shows LoginRequiredModal for Tier 1 (public) actions
|
|
10
|
+
* Server — checkActionAllowed() enforces the same guards on every server action
|
|
11
|
+
* Admin — ActionPermissionsManager toggles defaultEnabled via siteSettings.actionConfig
|
|
15
12
|
*/
|
|
16
13
|
export type ActionVariant = "primary" | "secondary" | "outline" | "ghost" | "danger" | "warning";
|
|
17
14
|
export declare const ACTION_ID: {
|
|
@@ -25,8 +22,29 @@ export declare const ACTION_ID: {
|
|
|
25
22
|
readonly PLACE_BID: "place-bid";
|
|
26
23
|
readonly BUY_NOW_AUCTION: "buy-now-auction";
|
|
27
24
|
readonly WATCH_AUCTION: "watch-auction";
|
|
25
|
+
readonly UNWATCH_AUCTION: "unwatch-auction";
|
|
28
26
|
readonly RESERVE_NOW: "reserve-now";
|
|
29
27
|
readonly CANCEL_RESERVATION: "cancel-reservation";
|
|
28
|
+
readonly CHECKOUT: "checkout";
|
|
29
|
+
readonly ENTER_PRIZE_DRAW: "enter-prize-draw";
|
|
30
|
+
readonly ENTER_RAFFLE: "enter-raffle";
|
|
31
|
+
readonly REPORT_LISTING: "report-listing";
|
|
32
|
+
readonly FOLLOW_STORE: "follow-store";
|
|
33
|
+
readonly WRITE_REVIEW: "write-review";
|
|
34
|
+
readonly MESSAGE_SELLER: "message-seller";
|
|
35
|
+
readonly BECOME_SELLER: "become-seller";
|
|
36
|
+
readonly REQUEST_PAYOUT: "request-payout";
|
|
37
|
+
readonly RESPOND_TO_REVIEW: "respond-to-review";
|
|
38
|
+
readonly CANCEL_ORDER: "cancel-order";
|
|
39
|
+
readonly REQUEST_RETURN: "request-return";
|
|
40
|
+
readonly REORDER: "reorder";
|
|
41
|
+
readonly TRACK_ORDER: "track-order";
|
|
42
|
+
readonly EDIT_PROFILE: "edit-profile";
|
|
43
|
+
readonly ADD_ADDRESS: "add-address";
|
|
44
|
+
readonly EDIT_ADDRESS: "edit-address";
|
|
45
|
+
readonly DELETE_ADDRESS: "delete-address";
|
|
46
|
+
readonly CHANGE_PASSWORD: "change-password";
|
|
47
|
+
readonly DELETE_ACCOUNT: "delete-account";
|
|
30
48
|
};
|
|
31
49
|
export type ActionId = (typeof ACTION_ID)[keyof typeof ACTION_ID];
|
|
32
50
|
export interface ActionMeta {
|
|
@@ -37,8 +55,26 @@ export interface ActionMeta {
|
|
|
37
55
|
variant: ActionVariant;
|
|
38
56
|
/** Lucide icon name — consuming component resolves this to the React component */
|
|
39
57
|
iconName?: string;
|
|
40
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Tier 1: shows LoginRequiredModal on public pages if user is not signed in.
|
|
60
|
+
* Server action also enforces via checkActionAllowed().
|
|
61
|
+
*/
|
|
41
62
|
requiresAuth?: boolean;
|
|
63
|
+
/** Message shown in LoginRequiredModal. Required when requiresAuth=true. */
|
|
64
|
+
authMessage?: string;
|
|
65
|
+
/**
|
|
66
|
+
* RBAC permission key required to use this action.
|
|
67
|
+
* If set, user must have this permission in addition to being signed in.
|
|
68
|
+
* Uses the 85+ permission keys from the RBAC system.
|
|
69
|
+
* Example: "products.create", "orders.refund", "admin.users.suspend"
|
|
70
|
+
*/
|
|
71
|
+
requiredPermission?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Whether this action is enabled by default.
|
|
74
|
+
* Can be overridden at runtime via siteSettings.actionConfig[actionId].enabled.
|
|
75
|
+
* Defaults to true when omitted.
|
|
76
|
+
*/
|
|
77
|
+
defaultEnabled?: boolean;
|
|
42
78
|
}
|
|
43
79
|
export declare const ACTION_META: Record<ActionId, ActionMeta>;
|
|
44
80
|
export declare const DETAIL_ACTIONS: {
|
|
@@ -56,7 +92,7 @@ export declare const MOBILE_PRIMARY_ACTIONS: {
|
|
|
56
92
|
};
|
|
57
93
|
export declare const LISTING_BULK_ACTIONS: {
|
|
58
94
|
readonly products: readonly ["add-to-cart", "add-to-wishlist", "compare"];
|
|
59
|
-
readonly auctions: readonly ["watch-auction", "
|
|
95
|
+
readonly auctions: readonly ["watch-auction", "unwatch-auction", "compare"];
|
|
60
96
|
readonly preorders: readonly ["add-to-cart", "add-to-wishlist", "compare"];
|
|
61
97
|
readonly stores: ActionId[];
|
|
62
98
|
};
|
|
@@ -92,6 +128,14 @@ export interface RowActionMeta {
|
|
|
92
128
|
destructive?: boolean;
|
|
93
129
|
/** Adds a separator above this item in the dropdown */
|
|
94
130
|
separator?: boolean;
|
|
131
|
+
/** Tier 2: all row actions require auth (role-gated at layout level) */
|
|
132
|
+
requiresAuth: true;
|
|
133
|
+
/** Minimum role required — enforced by RoleGate/ProtectedRoute at layout, NOT useAuthGate */
|
|
134
|
+
requiredRole?: "admin" | "moderator" | "seller" | "user";
|
|
135
|
+
/** RBAC permission key — shown in admin panel next to the toggle */
|
|
136
|
+
requiredPermission?: string;
|
|
137
|
+
/** Whether this row action is enabled by default (admin can toggle via actionConfig) */
|
|
138
|
+
defaultEnabled?: boolean;
|
|
95
139
|
}
|
|
96
140
|
export declare const ROW_ACTION_META: Record<RowActionId, RowActionMeta>;
|
|
97
141
|
export declare const ADMIN_ROW_ACTIONS: {
|
|
@@ -143,21 +187,11 @@ export interface FormActionMeta {
|
|
|
143
187
|
destructive?: boolean;
|
|
144
188
|
}
|
|
145
189
|
export declare const FORM_ACTION_META: Record<FormActionId, FormActionMeta>;
|
|
146
|
-
/**
|
|
147
|
-
* Preset ordered action groups for common form footer layouts.
|
|
148
|
-
* Consumed by FormShell and DrawerFormFooter to determine which buttons render,
|
|
149
|
-
* in which order (left → right).
|
|
150
|
-
*/
|
|
151
190
|
export declare const FORM_FOOTER_PRESET: {
|
|
152
|
-
/** Standard edit drawer: Cancel | Save Changes */
|
|
153
191
|
readonly drawerEdit: readonly ["form-cancel", "form-submit"];
|
|
154
|
-
/** Edit drawer with delete: Delete | Cancel | Save Changes */
|
|
155
192
|
readonly drawerEditDelete: readonly ["form-delete", "form-cancel", "form-submit"];
|
|
156
|
-
/** Content editor (blog / product): Discard | Save Draft | Publish */
|
|
157
193
|
readonly contentEditor: readonly ["form-discard", "form-save-draft", "form-publish"];
|
|
158
|
-
/** Simple modal / dialog: Cancel | Submit */
|
|
159
194
|
readonly modalForm: readonly ["form-cancel", "form-submit"];
|
|
160
|
-
/** Settings page: Reset | Save Changes */
|
|
161
195
|
readonly settingsForm: readonly ["form-reset", "form-submit"];
|
|
162
196
|
};
|
|
163
197
|
export declare const DASHBOARD_QUICK_ACTION_ID: {
|
|
@@ -187,6 +221,14 @@ export interface DashboardQuickActionMeta {
|
|
|
187
221
|
iconName?: string;
|
|
188
222
|
/** ROUTES key path that the button navigates to — consuming component resolves */
|
|
189
223
|
routeKey?: string;
|
|
224
|
+
/** All dashboard quick actions require auth (role-gated at layout level) */
|
|
225
|
+
requiresAuth: true;
|
|
226
|
+
/** Minimum role required — enforced by RoleGate/ProtectedRoute at layout */
|
|
227
|
+
requiredRole?: "admin" | "moderator" | "seller" | "user";
|
|
228
|
+
/** RBAC permission key — shown in admin panel next to the toggle */
|
|
229
|
+
requiredPermission?: string;
|
|
230
|
+
/** Whether this quick action is enabled by default */
|
|
231
|
+
defaultEnabled?: boolean;
|
|
190
232
|
}
|
|
191
233
|
export declare const DASHBOARD_QUICK_ACTION_META: Record<DashboardQuickActionId, DashboardQuickActionMeta>;
|
|
192
234
|
export declare const DASHBOARD_QUICK_ACTIONS: {
|