@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
|
@@ -27,6 +27,8 @@ import { AdminSidebar } from "../../../../features/admin/components/AdminSidebar
|
|
|
27
27
|
import { StoreSidebar } from "../../../../features/seller/components/SellerSidebar";
|
|
28
28
|
import { UserSidebar } from "../../../../features/account/components/UserSidebar";
|
|
29
29
|
import { DASHBOARD_DESKTOP_MEDIA_QUERY } from "../../../shared/features/layout/config";
|
|
30
|
+
import { filterNavItems } from "./filterNavItems";
|
|
31
|
+
import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
|
|
30
32
|
/**
|
|
31
33
|
* Hoisted drawer-state hook — the matchMedia-aware open/close logic that was
|
|
32
34
|
* triplicated across admin/store/user layouts. Used internally by
|
|
@@ -67,16 +69,15 @@ function useResponsiveDrawer() {
|
|
|
67
69
|
}, [registerNav, unregisterNav, open, close, toggle]);
|
|
68
70
|
return { desktopOpen, mobileOpen, close, toggle };
|
|
69
71
|
}
|
|
70
|
-
/** Filter
|
|
71
|
-
function
|
|
72
|
-
// null = admin (show everything); undefined = no filtering (backwards compat)
|
|
73
|
-
if (permissions === null
|
|
72
|
+
/** Filter nav groups by navConfig (enabled toggle) + requiredPermission. */
|
|
73
|
+
function filterGroups(groups, navConfig, permissions) {
|
|
74
|
+
// null permissions = admin (show everything); undefined = no filtering (backwards compat)
|
|
75
|
+
if (permissions === null && !navConfig)
|
|
74
76
|
return groups;
|
|
75
77
|
return groups
|
|
76
78
|
.map((group) => ({
|
|
77
79
|
...group,
|
|
78
|
-
items: group.items
|
|
79
|
-
permissions.includes(item.requiredPermission)),
|
|
80
|
+
items: filterNavItems(group.items, navConfig, permissions ?? undefined),
|
|
80
81
|
}))
|
|
81
82
|
.filter((group) => group.items.length > 0);
|
|
82
83
|
}
|
|
@@ -84,8 +85,11 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
|
|
|
84
85
|
const pathname = usePathname();
|
|
85
86
|
const activeHref = explicitActiveHref ?? pathname ?? "";
|
|
86
87
|
const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer();
|
|
88
|
+
const { data: settings } = useSiteSettings();
|
|
89
|
+
const navConfig = settings?.navConfig;
|
|
90
|
+
const filteredGroups = filterGroups(groups, navConfig, permissions);
|
|
87
91
|
const adminGroups = variant === "admin"
|
|
88
|
-
?
|
|
92
|
+
? filteredGroups
|
|
89
93
|
: groups;
|
|
90
|
-
return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups:
|
|
94
|
+
return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), children] }));
|
|
91
95
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type FilterableItem = {
|
|
2
|
+
id?: string;
|
|
3
|
+
requiredPermission?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Filters nav items by:
|
|
7
|
+
* 1. siteSettings.navConfig[item.id].enabled (admin toggle)
|
|
8
|
+
* 2. requiredPermission — item hidden when user lacks the permission
|
|
9
|
+
*
|
|
10
|
+
* Items without an `id` always pass through (legacy / unmanaged items).
|
|
11
|
+
*/
|
|
12
|
+
export declare function filterNavItems<T extends FilterableItem>(items: T[], navConfig: Record<string, {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}> | undefined, userPermissions: string[] | undefined): T[];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* Filters nav items by:
|
|
4
|
+
* 1. siteSettings.navConfig[item.id].enabled (admin toggle)
|
|
5
|
+
* 2. requiredPermission — item hidden when user lacks the permission
|
|
6
|
+
*
|
|
7
|
+
* Items without an `id` always pass through (legacy / unmanaged items).
|
|
8
|
+
*/
|
|
9
|
+
export function filterNavItems(items, navConfig, userPermissions) {
|
|
10
|
+
return items.filter((item) => {
|
|
11
|
+
if (!item.id)
|
|
12
|
+
return true;
|
|
13
|
+
if (!(navConfig?.[item.id]?.enabled ?? true))
|
|
14
|
+
return false;
|
|
15
|
+
if (item.requiredPermission && !userPermissions?.includes(item.requiredPermission))
|
|
16
|
+
return false;
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle a single action's enabled state in siteSettings.actionConfig.
|
|
3
|
+
* Performs a sparse merge — only updates the one key.
|
|
4
|
+
*/
|
|
5
|
+
export declare function updateActionConfigDomain(actionId: string, enabled: boolean): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Toggle a single nav item's enabled state in siteSettings.navConfig.
|
|
8
|
+
* Also recomputes the derived disabledRoutes[] array from all current nav items.
|
|
9
|
+
* The `allNavItems` argument is the full { id, href } list so we can derive disabledRoutes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function updateNavConfigDomain(navId: string, enabled: boolean, allNavItems: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
href: string;
|
|
14
|
+
}>): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository";
|
|
2
|
+
/**
|
|
3
|
+
* Toggle a single action's enabled state in siteSettings.actionConfig.
|
|
4
|
+
* Performs a sparse merge — only updates the one key.
|
|
5
|
+
*/
|
|
6
|
+
export async function updateActionConfigDomain(actionId, enabled) {
|
|
7
|
+
await siteSettingsRepository.updateSingleton({
|
|
8
|
+
actionConfig: { [actionId]: { enabled } },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Toggle a single nav item's enabled state in siteSettings.navConfig.
|
|
13
|
+
* Also recomputes the derived disabledRoutes[] array from all current nav items.
|
|
14
|
+
* The `allNavItems` argument is the full { id, href } list so we can derive disabledRoutes.
|
|
15
|
+
*/
|
|
16
|
+
export async function updateNavConfigDomain(navId, enabled, allNavItems) {
|
|
17
|
+
const current = await siteSettingsRepository.getSingleton();
|
|
18
|
+
const merged = {
|
|
19
|
+
...(current.navConfig ?? {}),
|
|
20
|
+
[navId]: { enabled },
|
|
21
|
+
};
|
|
22
|
+
const disabledRoutes = allNavItems
|
|
23
|
+
.filter((item) => (merged[item.id]?.enabled ?? true) === false)
|
|
24
|
+
.map((item) => item.href);
|
|
25
|
+
await siteSettingsRepository.updateSingleton({
|
|
26
|
+
navConfig: merged,
|
|
27
|
+
disabledRoutes,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -23,6 +23,13 @@ export interface SidebarNavItem {
|
|
|
23
23
|
label: string;
|
|
24
24
|
icon?: ReactNode;
|
|
25
25
|
badge?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Stable nav-* slug used as the key in siteSettings.navConfig.
|
|
28
|
+
* If absent, item is always visible (no admin toggle, no permission check).
|
|
29
|
+
*/
|
|
30
|
+
id?: string;
|
|
31
|
+
/** RBAC permission key — item only shows to users with this permission. */
|
|
32
|
+
requiredPermission?: string;
|
|
26
33
|
}
|
|
27
34
|
/** Sidebar group with title + items + default-open state. */
|
|
28
35
|
export interface SidebarNavGroup {
|
|
@@ -37,6 +44,10 @@ export interface MainNavItem {
|
|
|
37
44
|
icon?: ReactNode;
|
|
38
45
|
/** Translation key (consumer fills in `label` at render time via t(key)). */
|
|
39
46
|
key?: string;
|
|
47
|
+
/** Stable nav-* slug used as the key in siteSettings.navConfig. */
|
|
48
|
+
id?: string;
|
|
49
|
+
/** RBAC permission key — item only shows to users with this permission. */
|
|
50
|
+
requiredPermission?: string;
|
|
40
51
|
}
|
|
41
52
|
/** Branding config — propagated to TitleBar / Footer / mobile chrome. */
|
|
42
53
|
export interface BrandingConfig {
|
package/dist/client.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ export { FORM_ACTION_ID, FORM_ACTION_META, FORM_FOOTER_PRESET } from "./features
|
|
|
134
134
|
export type { FormActionId, FormActionMeta } from "./features/products/index";
|
|
135
135
|
export { DASHBOARD_QUICK_ACTION_ID, DASHBOARD_QUICK_ACTION_META, DASHBOARD_QUICK_ACTIONS } from "./features/products/index";
|
|
136
136
|
export type { DashboardQuickActionId, DashboardQuickActionMeta } from "./features/products/index";
|
|
137
|
+
export { useAuthGate } from "./react/hooks/useAuthGate";
|
|
138
|
+
export type { UseAuthGateReturn } from "./react/hooks/useAuthGate";
|
|
137
139
|
export { useActionDispatch } from "./react/hooks/use-action-dispatch";
|
|
138
140
|
export type { DispatchAction, UseActionDispatchOptions } from "./react/hooks/use-action-dispatch";
|
|
139
141
|
export { usePanelStore } from "./stores/panel-store";
|
package/dist/client.js
CHANGED
|
@@ -174,6 +174,8 @@ export { ROW_ACTION_ID, ROW_ACTION_META, ADMIN_ROW_ACTIONS, SELLER_ROW_ACTIONS,
|
|
|
174
174
|
export { FORM_ACTION_ID, FORM_ACTION_META, FORM_FOOTER_PRESET } from "./features/products/index";
|
|
175
175
|
// Dashboard quick actions
|
|
176
176
|
export { DASHBOARD_QUICK_ACTION_ID, DASHBOARD_QUICK_ACTION_META, DASHBOARD_QUICK_ACTIONS } from "./features/products/index";
|
|
177
|
+
// [CLIENT-ONLY] useAuthGate: pre-dispatch auth gate using ACTION_ID registry.
|
|
178
|
+
export { useAuthGate } from "./react/hooks/useAuthGate";
|
|
177
179
|
// Action dispatch hook + panel store
|
|
178
180
|
export { useActionDispatch } from "./react/hooks/use-action-dispatch";
|
|
179
181
|
export { usePanelStore } from "./stores/panel-store";
|
package/dist/contracts/auth.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface AuthPayload {
|
|
|
12
12
|
phoneNumber?: string | null;
|
|
13
13
|
/** Any extra custom claims stored on the token */
|
|
14
14
|
claims?: Record<string, unknown>;
|
|
15
|
+
/** RBAC permission keys granted to this user. */
|
|
16
|
+
permissions?: string[];
|
|
15
17
|
}
|
|
16
18
|
export interface AuthUser {
|
|
17
19
|
uid: string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ActionId } from "../../products/constants/action-defs";
|
|
2
|
+
import type { AuthPayload } from "../../../contracts/auth";
|
|
3
|
+
/** Throws if the action is disabled, requires auth the caller lacks, or requires a permission the caller lacks. */
|
|
4
|
+
export declare function checkActionAllowed(actionId: ActionId, user: AuthPayload | null): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getSiteSettingsGlobal } from "./getSiteSettingsGlobal";
|
|
2
|
+
import { ACTION_META } from "../../products/constants/action-defs";
|
|
3
|
+
import { AuthenticationError } from "../../../errors/authentication-error";
|
|
4
|
+
import { AuthorizationError } from "../../../errors/authorization-error";
|
|
5
|
+
/** Throws if the action is disabled, requires auth the caller lacks, or requires a permission the caller lacks. */
|
|
6
|
+
export async function checkActionAllowed(actionId, user) {
|
|
7
|
+
const settings = await getSiteSettingsGlobal();
|
|
8
|
+
const enabled = settings?.actionConfig?.[actionId]?.enabled ??
|
|
9
|
+
ACTION_META[actionId]?.defaultEnabled ??
|
|
10
|
+
true;
|
|
11
|
+
if (!enabled) {
|
|
12
|
+
throw new AuthorizationError("This action is currently unavailable.");
|
|
13
|
+
}
|
|
14
|
+
const meta = ACTION_META[actionId];
|
|
15
|
+
if (meta?.requiresAuth && !user?.uid) {
|
|
16
|
+
throw new AuthenticationError("Authentication required.");
|
|
17
|
+
}
|
|
18
|
+
if (meta?.requiredPermission && !user?.permissions?.includes(meta.requiredPermission)) {
|
|
19
|
+
throw new AuthorizationError("You don't have permission to perform this action.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getSiteSettingsGlobal } from "./getSiteSettingsGlobal";
|
|
2
|
+
/** Returns the hrefs of all disabled nav items (empty array when none disabled). */
|
|
3
|
+
export async function getDisabledRoutes() {
|
|
4
|
+
const settings = await getSiteSettingsGlobal();
|
|
5
|
+
return settings?.disabledRoutes ?? [];
|
|
6
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
import { siteSettingsRepository } from "../repository/site-settings.repository";
|
|
3
|
+
/** React.cache deduplication — one Firestore read per RSC request tree. */
|
|
4
|
+
export const getSiteSettingsGlobal = cache(() => siteSettingsRepository.getSingleton());
|
|
@@ -4,7 +4,9 @@ import { useState, useCallback } from "react";
|
|
|
4
4
|
import { Search, SlidersHorizontal, LayoutGrid, List, X } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Pagination, SortDropdown, useToast } from "../../../ui";
|
|
7
|
+
import { Pagination, SortDropdown, useToast, 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 } from "../../products/components/ProductFilters";
|
|
@@ -15,6 +17,7 @@ import { pushCartOp, pushWishlistOp } from "../../cart/utils/pending-ops";
|
|
|
15
17
|
export function CategoryProductsListing({ categorySlug, categoryId, brandName, 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");
|
|
@@ -40,17 +43,19 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
|
|
|
40
43
|
}, [searchInput, table]);
|
|
41
44
|
const handleWishlistToggle = useCallback((productId) => {
|
|
42
45
|
const isWishlisted = wishlistedIds.has(productId);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
requireAuth(isWishlisted ? ACTION_ID.REMOVE_FROM_WISHLIST : ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
47
|
+
if (isWishlisted) {
|
|
48
|
+
localWishlist.remove(productId, "product");
|
|
49
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "product" });
|
|
50
|
+
showToast("Removed from wishlist", "info");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
localWishlist.add(productId, "product");
|
|
54
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "product" });
|
|
55
|
+
showToast("Added to wishlist", "success");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}, [wishlistedIds, localWishlist, showToast, requireAuth]);
|
|
54
59
|
const handleAddToCart = useCallback((product) => {
|
|
55
60
|
localCart.add(product.id, 1, {
|
|
56
61
|
productTitle: product.title,
|
|
@@ -65,5 +70,5 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
|
|
|
65
70
|
table.set("view", next);
|
|
66
71
|
};
|
|
67
72
|
const closeFilters = () => setFilterOpen(false);
|
|
68
|
-
return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: _jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: "flex shrink-0 items-center gap-2 rounded-lg border 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 in ${categorySlug.replace(/-/g, " ")}...`, 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" }), _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); }, options: PRODUCT_PUBLIC_SORT_OPTIONS })] }), _jsxs("div", { className: "flex shrink-0 items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => handleViewToggle("card"), "aria-label": "Grid view", className: `p-2 transition-colors ${view === "card" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleViewToggle("list"), "aria-label": "List view", className: `p-2 transition-colors ${view === "list" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(List, { className: "h-4 w-4" }) })] })] }) }), _jsxs("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: "No products found in this category.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, 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(ProductFilters, { table: table, currencyPrefix: "\u20B9" }) }), _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" }) })] })] }))] }));
|
|
73
|
+
return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: _jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: "flex shrink-0 items-center gap-2 rounded-lg border 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 in ${categorySlug.replace(/-/g, " ")}...`, 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" }), _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); }, options: PRODUCT_PUBLIC_SORT_OPTIONS })] }), _jsxs("div", { className: "flex shrink-0 items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => handleViewToggle("card"), "aria-label": "Grid view", className: `p-2 transition-colors ${view === "card" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleViewToggle("list"), "aria-label": "List view", className: `p-2 transition-colors ${view === "list" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(List, { className: "h-4 w-4" }) })] })] }) }), _jsxs("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: "No products found in this category.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, 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(ProductFilters, { table: table, currencyPrefix: "\u20B9" }) }), _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" }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
69
74
|
}
|
|
@@ -10,6 +10,9 @@ const ROLE_DOT_COLORS = {
|
|
|
10
10
|
employee: "#f59e0b",
|
|
11
11
|
};
|
|
12
12
|
import { useTheme } from "../../react";
|
|
13
|
+
import { useAuth } from "../../react/contexts/SessionContext";
|
|
14
|
+
import { useSiteSettings } from "../../core/hooks/useSiteSettings";
|
|
15
|
+
import { filterNavItems } from "../../_internal/client/features/layout/filterNavItems";
|
|
13
16
|
import { useBottomActionsContext } from "./BottomActionsContext";
|
|
14
17
|
import BottomActions from "./BottomActions";
|
|
15
18
|
import { AutoBreadcrumbs } from "./AutoBreadcrumbs";
|
|
@@ -50,6 +53,10 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
50
53
|
const { theme, toggleTheme } = useTheme();
|
|
51
54
|
const { closeNav: closeDashboardNav, hasNav: hasDashboardNav, toggleNav: toggleDashboardNav } = useDashboardNav();
|
|
52
55
|
const { state: bottomActionsState } = useBottomActionsContext();
|
|
56
|
+
const { user: authUser } = useAuth();
|
|
57
|
+
const { data: siteSettingsData } = useSiteSettings();
|
|
58
|
+
const navConfig = siteSettingsData?.navConfig;
|
|
59
|
+
const visibleNavItems = filterNavItems(navItems, navConfig, authUser?.permissions);
|
|
53
60
|
const headerRef = useRef(null);
|
|
54
61
|
useEffect(() => {
|
|
55
62
|
const el = headerRef.current;
|
|
@@ -154,7 +161,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
154
161
|
opacity: darkBackground.overlay?.opacity ?? 0,
|
|
155
162
|
},
|
|
156
163
|
};
|
|
157
|
-
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems:
|
|
164
|
+
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems: visibleNavItems, hiddenNavItems: hiddenNavItems }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsxs(Div, { className: "flex items-center justify-between gap-3", children: [_jsxs(Div, { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsxs(Div, { className: "flex-shrink-0 relative", children: [_jsx(AvatarDisplay, { cropData: user.avatarMetadata
|
|
158
165
|
? {
|
|
159
166
|
url: user.avatarMetadata.url,
|
|
160
167
|
position: user.avatarMetadata.position ?? {
|
|
@@ -76,7 +76,7 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
|
|
|
76
76
|
"text-zinc-600 dark:text-slate-300",
|
|
77
77
|
"hover:bg-zinc-50 dark:hover:bg-slate-800/60 transition-colors",
|
|
78
78
|
filterActiveCount > 0
|
|
79
|
-
? "border-primary/40 bg-primary/5 text-
|
|
79
|
+
? "border-primary/40 bg-primary/5 text-zinc-800 dark:text-zinc-100"
|
|
80
80
|
: "",
|
|
81
81
|
].join(" "), children: [l.filtersTitle, filterActiveCount > 0 && (_jsx(Span, { className: "inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] })), searchSlot && _jsx(Div, { className: "flex-1 min-w-0", children: searchSlot })] }), (sortSlot || viewToggleSlot || actionsSlot) && (_jsx(Div, { className: "flex items-stretch min-h-[44px] gap-2 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: _jsxs(Row, { className: "gap-2 flex-shrink-0 pb-px", children: [sortSlot, viewToggleSlot, actionsSlot] }) }))] }), selectedCount > 0 && bulkActionBarSlot && (_jsx(Div, { className: "hidden md:block pt-2 mt-2 border-t border-zinc-100 dark:border-slate-800", children: bulkActionBarSlot }))] }), _jsxs(Div, { className: "flex gap-4 lg:gap-6 items-start", children: [hasFilter && (_jsx(Aside, { "aria-label": panelTitle, className: [
|
|
82
82
|
"hidden lg:block flex-shrink-0 self-start",
|
|
@@ -8,6 +8,10 @@ export interface MainNavbarItem {
|
|
|
8
8
|
label: string;
|
|
9
9
|
icon?: React.ReactNode;
|
|
10
10
|
highlighted?: boolean;
|
|
11
|
+
/** Stable nav-* slug for siteSettings.navConfig toggling. */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** RBAC permission key — item hidden when user lacks this permission. */
|
|
14
|
+
requiredPermission?: string;
|
|
11
15
|
}
|
|
12
16
|
export interface MainNavbarProps {
|
|
13
17
|
/** Pre-translated nav items to display */
|
|
@@ -18,7 +18,7 @@ export function NavItem({ href, label, icon, isActive = false, variant = "horizo
|
|
|
18
18
|
}
|
|
19
19
|
if (highlighted) {
|
|
20
20
|
return (_jsxs(Link, { href: href, className: [
|
|
21
|
-
"flex items-center gap-2 px-3 py-1 rounded-full border border-primary-700/30 dark:border-primary/30 bg-primary-700/5 dark:bg-primary/5 text-
|
|
21
|
+
"flex items-center gap-2 px-3 py-1 rounded-full border border-primary-700/30 dark:border-primary/30 bg-primary-700/5 dark:bg-primary/5 text-zinc-800 dark:text-zinc-100 text-sm font-medium transition-all hover:bg-primary-700/10 dark:hover:bg-primary/10",
|
|
22
22
|
className,
|
|
23
23
|
]
|
|
24
24
|
.filter(Boolean)
|
|
@@ -4,7 +4,8 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { Columns, Heart, ShoppingCart, 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, BulkActionBar, ListingToolbar } from "../../../ui";
|
|
7
|
+
import { Pagination, useToast, BulkActionBar, ListingToolbar, LoginRequiredModal } from "../../../ui";
|
|
8
|
+
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
8
9
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
|
|
9
10
|
import { CompareOverlay } from "../../products/components/CompareOverlay";
|
|
10
11
|
import { ROUTES } from "../../../next";
|
|
@@ -32,6 +33,7 @@ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_PRICE
|
|
|
32
33
|
export function PreOrdersIndexListing({ initialData, categorySlug, brandName }) {
|
|
33
34
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
|
|
34
35
|
const { showToast } = useToast();
|
|
36
|
+
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
35
37
|
const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
|
|
36
38
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
37
39
|
const [compareIds, setCompareIds] = useState([]);
|
|
@@ -132,15 +134,19 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
132
134
|
};
|
|
133
135
|
const wishlistActions = {
|
|
134
136
|
addToWishlist: (productId) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
requireAuth(ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
138
|
+
localWishlist.add(productId, "preorder");
|
|
139
|
+
pushWishlistOp({ op: "add", itemId: productId, type: "preorder" });
|
|
140
|
+
showToast("Added to wishlist", "success");
|
|
141
|
+
});
|
|
138
142
|
return Promise.resolve();
|
|
139
143
|
},
|
|
140
144
|
removeFromWishlist: (productId) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
requireAuth(ACTION_ID.REMOVE_FROM_WISHLIST, () => {
|
|
146
|
+
localWishlist.remove(productId, "preorder");
|
|
147
|
+
pushWishlistOp({ op: "remove", itemId: productId, type: "preorder" });
|
|
148
|
+
showToast("Removed from wishlist", "info");
|
|
149
|
+
});
|
|
144
150
|
return Promise.resolve();
|
|
145
151
|
},
|
|
146
152
|
isWishlisted: (productId) => wishlistedIds.has(productId),
|
|
@@ -178,13 +184,15 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
178
184
|
icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
|
|
179
185
|
variant: "secondary",
|
|
180
186
|
onClick: () => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
requireAuth(ACTION_ID.ADD_TO_WISHLIST, () => {
|
|
188
|
+
const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
|
|
189
|
+
selected.forEach((p) => {
|
|
190
|
+
localWishlist.add(p.id, "preorder");
|
|
191
|
+
pushWishlistOp({ op: "add", itemId: p.id, type: "preorder" });
|
|
192
|
+
});
|
|
193
|
+
showToast(`${selected.length} items added to wishlist`, "success");
|
|
194
|
+
selection.clearSelection();
|
|
185
195
|
});
|
|
186
|
-
showToast(`${selected.length} items added to wishlist`, "success");
|
|
187
|
-
selection.clearSelection();
|
|
188
196
|
},
|
|
189
197
|
},
|
|
190
198
|
{
|
|
@@ -201,5 +209,5 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
201
209
|
] }), 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: 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" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No pre-orders found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx("div", { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
|
|
202
210
|
setCompareIds([]);
|
|
203
211
|
selection.clearSelection();
|
|
204
|
-
}, 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(PreOrderFilters, { 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})` : ""] }) })] })] }))] }));
|
|
212
|
+
}, 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(PreOrderFilters, { 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 })] }));
|
|
205
213
|
}
|
|
@@ -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
|
}
|