@mohasinac/appkit 3.9.0 → 4.0.0
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/shared/actions/action-registry.js +8 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -4
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/features/account/hooks/useDataViewMode.d.ts +17 -0
- package/dist/features/account/hooks/useDataViewMode.js +55 -0
- package/dist/features/account/hooks/useProfile.d.ts +2 -0
- package/dist/features/admin/components/AdminBundlesView.d.ts +3 -4
- package/dist/features/admin/components/AdminBundlesView.js +2 -2
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminCategoriesView.d.ts +2 -6
- package/dist/features/admin/components/AdminCategoriesView.js +1 -1
- package/dist/features/admin/components/AdminFaqsView.d.ts +0 -2
- package/dist/features/admin/components/AdminPrizeDrawsView.js +1 -1
- package/dist/features/admin/components/AdminSectionsView.js +4 -4
- package/dist/features/admin/components/DataListingView.d.ts +8 -2
- package/dist/features/admin/components/DataListingView.js +7 -3
- package/dist/features/admin/components/DataTable.d.ts +10 -2
- package/dist/features/admin/components/DataTable.js +4 -2
- package/dist/features/admin/components/sections/adminSectionsBuildParse.js +13 -5
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.js +9 -5
- package/dist/features/admin/hooks/useAdminListing.d.ts +2 -2
- package/dist/features/admin/hooks/useAdminListing.js +6 -1
- package/dist/features/categories/components/BrandDetailTabs.js +2 -12
- package/dist/features/categories/components/CategoryDetailTabs.js +2 -12
- package/dist/features/events/components/AdminEventsView.d.ts +1 -5
- package/dist/features/faq/components/FAQAccordion.js +3 -2
- package/dist/features/faq/components/RelatedFAQs.js +1 -1
- package/dist/features/homepage/components/BrandsSection.d.ts +4 -1
- package/dist/features/homepage/components/BrandsSection.js +2 -2
- package/dist/features/homepage/components/CustomCardsSection.js +3 -3
- package/dist/features/homepage/components/CustomerReviewsSection.d.ts +4 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
- package/dist/features/homepage/components/EventsSection.d.ts +4 -1
- package/dist/features/homepage/components/EventsSection.js +2 -2
- package/dist/features/homepage/components/FAQSection.js +3 -3
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +2 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +2 -2
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +2 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +2 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +2 -1
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/FeaturedStoresSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedStoresSection.js +2 -2
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +4 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.d.ts +3 -1
- package/dist/features/homepage/components/SectionCarousel.js +2 -2
- package/dist/features/homepage/components/ShopByCategorySection.d.ts +4 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +2 -2
- package/dist/features/homepage/lib/section-renderer.js +9 -9
- package/dist/features/homepage/schemas/firestore.d.ts +8 -0
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/lottery/schemas/zod.d.ts +4 -4
- package/dist/features/payments/schemas/firestore.d.ts +6 -0
- package/dist/features/payments/schemas/firestore.js +2 -0
- package/dist/features/seller/components/SellerBundlesView.d.ts +3 -4
- package/dist/features/seller/components/SellerBundlesView.js +2 -2
- package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -5
- package/dist/features/seller/components/SellerPayoutsView.js +100 -21
- package/dist/features/seller/components/SellerPreOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPrizeDrawsView.js +1 -1
- package/dist/features/seller/components/SellerProductsView.js +6 -2
- package/dist/features/shipments/components/AdminShipmentsView.d.ts +1 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +3 -3
- package/dist/react/index.d.ts +0 -2
- package/dist/react/index.js +0 -2
- package/dist/seed/faq-seed-data.js +11 -11
- package/dist/seed/homepage-sections-seed-data.js +11 -1
- package/dist/styles.css +30 -99
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +10 -0
- package/dist/ui/components/DetailPageTabs.d.ts +1 -1
- package/dist/ui/components/DetailPageTabs.js +6 -30
- package/dist/ui/components/HorizontalScroller.js +3 -0
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +13 -8
- package/dist/ui/components/Tabs.style.css +19 -0
- package/dist/ui/components/index.style.css +0 -1
- package/dist/ui/index.d.ts +0 -2
- package/dist/ui/index.js +0 -1
- package/package.json +1 -1
|
@@ -310,6 +310,14 @@ export const ACTIONS = {
|
|
|
310
310
|
kind: "ghost",
|
|
311
311
|
permissions: ["seller", "admin"],
|
|
312
312
|
},
|
|
313
|
+
"set-payout-reminder": {
|
|
314
|
+
id: "store.set-payout-reminder",
|
|
315
|
+
label: "Remind Me",
|
|
316
|
+
ariaLabel: "Toggle a follow-up reminder on this payout",
|
|
317
|
+
description: "Flag this payout for a personal follow-up reminder.",
|
|
318
|
+
kind: "ghost",
|
|
319
|
+
permissions: ["seller"],
|
|
320
|
+
},
|
|
313
321
|
// ── Review management ───────────────────────────────────────────────
|
|
314
322
|
"reply-review": {
|
|
315
323
|
id: "store.reply-review",
|
package/dist/client.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ export { Skeleton } from "./ui/components/Skeleton";
|
|
|
32
32
|
export { Alert } from "./ui/components/Alert";
|
|
33
33
|
export type { SkeletonProps } from "./ui/components/Skeleton";
|
|
34
34
|
export { Divider } from "./ui/components/Divider";
|
|
35
|
-
export {
|
|
35
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/components/Tabs";
|
|
36
|
+
export type { TabsProps, TabsListProps, TabsTriggerProps, TabsContentProps } from "./ui/components/Tabs";
|
|
36
37
|
export { SellerProductsView } from "./features/seller/components/SellerProductsView";
|
|
37
38
|
export { Drawer } from "./ui/components/Drawer";
|
|
38
39
|
export { FilterDrawer } from "./ui/components/FilterDrawer";
|
|
@@ -69,7 +70,6 @@ export { useSwipe } from "./react/hooks/useSwipe";
|
|
|
69
70
|
export { useInfiniteScroll } from "./react/hooks/useInfiniteScroll";
|
|
70
71
|
export type { UseInfiniteScrollOptions, UseInfiniteScrollResult, } from "./react/hooks/useInfiniteScroll";
|
|
71
72
|
export { useUnsavedChanges } from "./react/hooks/useUnsavedChanges";
|
|
72
|
-
export { useVisibleItems } from "./react/hooks/useVisibleItems";
|
|
73
73
|
export { FirebaseClientAuthProvider } from "./providers/firebase-client/index";
|
|
74
74
|
export { FirebaseClientRealtimeProvider } from "./providers/firebase-client/index";
|
|
75
75
|
export { EventFormDrawer, EventParticipateView, EventPollWidget } from "./features/events/index";
|
package/dist/client.js
CHANGED
|
@@ -41,7 +41,7 @@ export { EmptyState } from "./ui/components/EmptyState";
|
|
|
41
41
|
export { Skeleton } from "./ui/components/Skeleton";
|
|
42
42
|
export { Alert } from "./ui/components/Alert";
|
|
43
43
|
export { Divider } from "./ui/components/Divider";
|
|
44
|
-
export {
|
|
44
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/components/Tabs";
|
|
45
45
|
export { SellerProductsView } from "./features/seller/components/SellerProductsView";
|
|
46
46
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
47
47
|
// Drawer - Component for drawer.
|
|
@@ -113,9 +113,6 @@ export { useInfiniteScroll } from "./react/hooks/useInfiniteScroll";
|
|
|
113
113
|
// useUnsavedChanges - React hook for use unsaved changes.
|
|
114
114
|
export { useUnsavedChanges } from "./react/hooks/useUnsavedChanges";
|
|
115
115
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
116
|
-
// useVisibleItems - React hook for use visible items.
|
|
117
|
-
export { useVisibleItems } from "./react/hooks/useVisibleItems";
|
|
118
|
-
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
119
116
|
// FirebaseClientAuthProvider - Component for firebase client auth provider.
|
|
120
117
|
export { FirebaseClientAuthProvider } from "./providers/firebase-client/index";
|
|
121
118
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
@@ -359,6 +359,7 @@ export declare const SELLER_ENDPOINTS: {
|
|
|
359
359
|
readonly COUPON_BY_ID: (id: string) => string;
|
|
360
360
|
readonly OFFERS: "/api/store/offers";
|
|
361
361
|
readonly PAYOUTS: "/api/store/payouts";
|
|
362
|
+
readonly PAYOUT_BY_ID: (id: string) => string;
|
|
362
363
|
readonly PAYOUT_SETTINGS: "/api/store/payout-settings";
|
|
363
364
|
readonly PAYOUT_REQUEST: "/api/store/payouts/request";
|
|
364
365
|
readonly REVIEWS: "/api/store/reviews";
|
|
@@ -805,6 +806,7 @@ export declare const API_ENDPOINTS: {
|
|
|
805
806
|
readonly COUPON_BY_ID: (id: string) => string;
|
|
806
807
|
readonly OFFERS: "/api/store/offers";
|
|
807
808
|
readonly PAYOUTS: "/api/store/payouts";
|
|
809
|
+
readonly PAYOUT_BY_ID: (id: string) => string;
|
|
808
810
|
readonly PAYOUT_SETTINGS: "/api/store/payout-settings";
|
|
809
811
|
readonly PAYOUT_REQUEST: "/api/store/payouts/request";
|
|
810
812
|
readonly REVIEWS: "/api/store/reviews";
|
|
@@ -1253,6 +1255,7 @@ export declare const API_ROUTES: {
|
|
|
1253
1255
|
readonly COUPON_BY_ID: (id: string) => string;
|
|
1254
1256
|
readonly OFFERS: "/api/store/offers";
|
|
1255
1257
|
readonly PAYOUTS: "/api/store/payouts";
|
|
1258
|
+
readonly PAYOUT_BY_ID: (id: string) => string;
|
|
1256
1259
|
readonly PAYOUT_SETTINGS: "/api/store/payout-settings";
|
|
1257
1260
|
readonly PAYOUT_REQUEST: "/api/store/payouts/request";
|
|
1258
1261
|
readonly REVIEWS: "/api/store/reviews";
|
|
@@ -477,6 +477,7 @@ export const SELLER_ENDPOINTS = {
|
|
|
477
477
|
COUPON_BY_ID: (id) => `/api/store/coupons/${id}`,
|
|
478
478
|
OFFERS: "/api/store/offers",
|
|
479
479
|
PAYOUTS: "/api/store/payouts",
|
|
480
|
+
PAYOUT_BY_ID: (id) => `/api/store/payouts/${id}`,
|
|
480
481
|
PAYOUT_SETTINGS: "/api/store/payout-settings",
|
|
481
482
|
PAYOUT_REQUEST: "/api/store/payouts/request",
|
|
482
483
|
REVIEWS: "/api/store/reviews",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type DataViewMode = "table" | "grid" | "list";
|
|
2
|
+
/**
|
|
3
|
+
* Persisted, per-user "last saved" listing view-mode preference (table vs
|
|
4
|
+
* grid vs list) — a single global value shared across every listing page,
|
|
5
|
+
* not scoped per page. When the user has never explicitly chosen a view,
|
|
6
|
+
* defaults to "list" (AdminViewCards' one-full-width-card-per-row layout —
|
|
7
|
+
* easier to read than a compacted table) below the mobile breakpoint, and
|
|
8
|
+
* "table" at desktop widths.
|
|
9
|
+
*
|
|
10
|
+
* Only the view-mode toggle is persisted here. Filters, sort, search, and
|
|
11
|
+
* pagination already live in the URL via useUrlTable() and must never be
|
|
12
|
+
* added to this hook — see CLAUDE.md Phase 3 / user directive 2026-08-18.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useDataViewMode(fallback?: DataViewMode): {
|
|
15
|
+
view: DataViewMode;
|
|
16
|
+
setView: (next: DataViewMode) => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useCurrentProfile, useUpdateCurrentProfile } from "./useProfile";
|
|
4
|
+
const PERSIST_DEBOUNCE_MS = 600;
|
|
5
|
+
const MOBILE_BREAKPOINT_PX = 768;
|
|
6
|
+
/**
|
|
7
|
+
* Persisted, per-user "last saved" listing view-mode preference (table vs
|
|
8
|
+
* grid vs list) — a single global value shared across every listing page,
|
|
9
|
+
* not scoped per page. When the user has never explicitly chosen a view,
|
|
10
|
+
* defaults to "list" (AdminViewCards' one-full-width-card-per-row layout —
|
|
11
|
+
* easier to read than a compacted table) below the mobile breakpoint, and
|
|
12
|
+
* "table" at desktop widths.
|
|
13
|
+
*
|
|
14
|
+
* Only the view-mode toggle is persisted here. Filters, sort, search, and
|
|
15
|
+
* pagination already live in the URL via useUrlTable() and must never be
|
|
16
|
+
* added to this hook — see CLAUDE.md Phase 3 / user directive 2026-08-18.
|
|
17
|
+
*/
|
|
18
|
+
export function useDataViewMode(fallback = "table") {
|
|
19
|
+
const { data: profile } = useCurrentProfile();
|
|
20
|
+
const updateProfile = useUpdateCurrentProfile();
|
|
21
|
+
const debounceRef = useRef(null);
|
|
22
|
+
const [view, setViewState] = useState(fallback);
|
|
23
|
+
const hasAppliedDefault = useRef(false);
|
|
24
|
+
// Apply the persisted preference (or a viewport-based default when none
|
|
25
|
+
// exists yet) exactly once per mount, so a later profile refetch doesn't
|
|
26
|
+
// clobber a view the user has since changed locally.
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (hasAppliedDefault.current)
|
|
29
|
+
return;
|
|
30
|
+
if (profile === undefined)
|
|
31
|
+
return; // still loading — wait rather than flash "table" then jump
|
|
32
|
+
hasAppliedDefault.current = true;
|
|
33
|
+
const saved = profile.uiPreferences?.dataViewMode;
|
|
34
|
+
if (saved) {
|
|
35
|
+
setViewState(saved);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof window !== "undefined" && window.innerWidth < MOBILE_BREAKPOINT_PX) {
|
|
39
|
+
setViewState("list");
|
|
40
|
+
}
|
|
41
|
+
}, [profile]);
|
|
42
|
+
useEffect(() => () => {
|
|
43
|
+
if (debounceRef.current)
|
|
44
|
+
clearTimeout(debounceRef.current);
|
|
45
|
+
}, []);
|
|
46
|
+
const setView = useCallback((next) => {
|
|
47
|
+
setViewState(next);
|
|
48
|
+
if (debounceRef.current)
|
|
49
|
+
clearTimeout(debounceRef.current);
|
|
50
|
+
debounceRef.current = setTimeout(() => {
|
|
51
|
+
updateProfile.mutate({ uiPreferences: { dataViewMode: next } });
|
|
52
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
53
|
+
}, [updateProfile]);
|
|
54
|
+
return { view, setView };
|
|
55
|
+
}
|
|
@@ -13,6 +13,7 @@ export interface UserProfile {
|
|
|
13
13
|
googleLinkedEmail?: string | null;
|
|
14
14
|
uiPreferences?: {
|
|
15
15
|
collapsedSections?: string[];
|
|
16
|
+
dataViewMode?: "table" | "grid" | "list";
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export interface UpdateCurrentProfileInput {
|
|
@@ -31,6 +32,7 @@ export interface UpdateCurrentProfileInput {
|
|
|
31
32
|
profileIsPublic?: boolean;
|
|
32
33
|
uiPreferences?: {
|
|
33
34
|
collapsedSections?: string[];
|
|
35
|
+
dataViewMode?: "table" | "grid" | "list";
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
export declare function useCurrentProfile(options?: {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
export interface AdminBundlesViewProps {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}) => string;
|
|
3
|
+
/** Plain string with an `{id}` placeholder — safe to build in a Server Component page.tsx. */
|
|
4
|
+
editHrefTemplate: string;
|
|
6
5
|
newHref: string;
|
|
7
6
|
}
|
|
8
|
-
export declare function AdminBundlesView({
|
|
7
|
+
export declare function AdminBundlesView({ editHrefTemplate, newHref }: AdminBundlesViewProps): React.JSX.Element;
|
|
@@ -40,7 +40,7 @@ const COLUMNS = [
|
|
|
40
40
|
},
|
|
41
41
|
{ key: "updatedAt", header: "Updated", className: "w-28" },
|
|
42
42
|
];
|
|
43
|
-
export function AdminBundlesView({
|
|
43
|
+
export function AdminBundlesView({ editHrefTemplate, newHref }) {
|
|
44
44
|
const [rebuildingId, setRebuildingId] = useState(null);
|
|
45
45
|
const toast = useToast();
|
|
46
46
|
const handleRebuild = useCallback(async (bundleId) => {
|
|
@@ -99,7 +99,7 @@ export function AdminBundlesView({ getEditHref, newHref }) {
|
|
|
99
99
|
parts.push(sieveFilter("bundleStockStatus", SIEVE_OP.EQ, filterState.bundleStockStatus));
|
|
100
100
|
return parts.join(",") || undefined;
|
|
101
101
|
},
|
|
102
|
-
|
|
102
|
+
rowHrefTemplate: editHrefTemplate,
|
|
103
103
|
toolbarExtra: (_jsx(Button, { asChild: true, size: "sm", variant: "primary", children: _jsxs(TextLink, { href: newHref, layout: "flex", align: "center", gap: "xs", children: ["+ ", BUNDLE_COPY.adminList.newButton] }) })),
|
|
104
104
|
// Rule #7: bulk-action array sourced from the ADMIN_BULK_ACTIONS preset.
|
|
105
105
|
buildBulkActions: (selection) => {
|
|
@@ -145,5 +145,5 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
145
145
|
] })),
|
|
146
146
|
},
|
|
147
147
|
];
|
|
148
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one",
|
|
148
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Carousel Slide", message: "Delete this carousel slide? This cannot be undone.", onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
|
|
149
149
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ListingLayoutProps } from "../../../ui";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
id: string;
|
|
6
|
-
}) => string;
|
|
7
|
-
}
|
|
8
|
-
export declare function AdminCategoriesView({ children, getRowHref: _getRowHref, ...props }: AdminCategoriesViewProps): React.JSX.Element;
|
|
3
|
+
export type AdminCategoriesViewProps = ListingLayoutProps;
|
|
4
|
+
export declare function AdminCategoriesView({ children, ...props }: AdminCategoriesViewProps): React.JSX.Element;
|
|
@@ -95,7 +95,7 @@ const ADMIN_CATEGORIES_CONFIG = {
|
|
|
95
95
|
renderEditor: ({ editId, closePanel }) => (_jsx(AdminCategoryEditorView, { categoryId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })),
|
|
96
96
|
resolveEditorTitle: ({ isCreate }) => (isCreate ? "Add Category" : "Edit Category"),
|
|
97
97
|
};
|
|
98
|
-
export function AdminCategoriesView({ children,
|
|
98
|
+
export function AdminCategoriesView({ children, ...props }) {
|
|
99
99
|
if (React.Children.count(children) > 0) {
|
|
100
100
|
return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Categories" }), children] }));
|
|
101
101
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ListingLayoutProps } from "../../../ui";
|
|
3
|
-
import type { AdminListingScaffoldRow } from "./DataListingView";
|
|
4
3
|
export interface AdminFaqsViewProps extends ListingLayoutProps {
|
|
5
4
|
actionHref?: string;
|
|
6
|
-
getRowHref?: (row: AdminListingScaffoldRow) => string;
|
|
7
5
|
onBulkArchive?: (ids: string[]) => Promise<void>;
|
|
8
6
|
onBulkDelete?: (ids: string[]) => Promise<void>;
|
|
9
7
|
}
|
|
@@ -93,7 +93,7 @@ export function AdminPrizeDrawsView({ children, ...props }) {
|
|
|
93
93
|
const status = state.status && state.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, state.status) : null;
|
|
94
94
|
return ["listingType==prize-draw", status].filter(Boolean).join(",");
|
|
95
95
|
},
|
|
96
|
-
|
|
96
|
+
rowHrefTemplate: String(ROUTES.ADMIN.PRIZE_DRAWS_EDIT("{id}")),
|
|
97
97
|
// Rule #7: bulk-action array sourced from the ADMIN_BULK_ACTIONS preset.
|
|
98
98
|
buildBulkActions: (selection) => ADMIN_BULK_ACTIONS.prizeDraws.map((id) => ({
|
|
99
99
|
id,
|
|
@@ -703,7 +703,7 @@ export function AdminSectionsView({ children }) {
|
|
|
703
703
|
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reviews Section Builder" }), _jsx(Input, { label: "Section title", value: reviewsBuilder.title, onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Select, { label: "Reviews source", value: reviewsBuilder.source, onValueChange: (v) => setReviewsBuilder((prev) => ({ ...prev, source: v })), options: [
|
|
704
704
|
{ label: "Platform reviews (LetItRip)", value: "platform" },
|
|
705
705
|
{ label: "Google Business Reviews", value: "google" },
|
|
706
|
-
] }), reviewsBuilder.source === "google" ? (_jsx(Input, { label: "Google Place ID", value: reviewsBuilder.placeId, onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, placeId: e.target.value })), placeholder: "ChIJ...", helperText: "Find your Place ID at maps.google.com/placesinventory" })) : null, _jsx(Select, { label: "Max reviews", value: String(reviewsBuilder.maxReviews), onValueChange: (v) => setReviewsBuilder((prev) => ({ ...prev, maxReviews: Number(v) })), options: [{ label: "5", value: "5" }, { label: "10", value: "10" }, { label: "20", value: "20" }] }), _jsx(Select, { label: "Items per view", value: String(reviewsBuilder.itemsPerView), onValueChange: (v) => setReviewsBuilder((prev) => ({ ...prev, itemsPerView: Number(v) })), options: [{ label: "1", value: "1" }, { label: "2", value: "2" }, { label: "3", value: "3" }] }), _jsx(Checkbox, { checked: reviewsBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(reviewsBuilder.scrollInterval), onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) })] }));
|
|
706
|
+
] }), reviewsBuilder.source === "google" ? (_jsx(Input, { label: "Google Place ID", value: reviewsBuilder.placeId, onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, placeId: e.target.value })), placeholder: "ChIJ...", helperText: "Find your Place ID at maps.google.com/placesinventory" })) : null, _jsx(Select, { label: "Max reviews", value: String(reviewsBuilder.maxReviews), onValueChange: (v) => setReviewsBuilder((prev) => ({ ...prev, maxReviews: Number(v) })), options: [{ label: "5", value: "5" }, { label: "10", value: "10" }, { label: "20", value: "20" }] }), _jsx(Select, { label: "Items per view", value: String(reviewsBuilder.itemsPerView), onValueChange: (v) => setReviewsBuilder((prev) => ({ ...prev, itemsPerView: Number(v) })), options: [{ label: "1", value: "1" }, { label: "2", value: "2" }, { label: "3", value: "3" }] }), _jsx(Checkbox, { checked: reviewsBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(reviewsBuilder.scrollInterval), onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) }), _jsx(Checkbox, { checked: reviewsBuilder.loop, label: "Loop carousel", onChange: (e) => setReviewsBuilder((prev) => ({ ...prev, loop: e.target.checked })) })] }));
|
|
707
707
|
}
|
|
708
708
|
function renderWhatsAppBuilder() {
|
|
709
709
|
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "WhatsApp Community Builder" }), _jsx(Input, { label: "Title", value: whatsappBuilder.title, onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Textarea, { label: "Description", value: whatsappBuilder.description, onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, description: e.target.value })), rows: 2 }), _jsx(Input, { label: "WhatsApp group link", type: "url", value: whatsappBuilder.groupLink, onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, groupLink: e.target.value })), placeholder: "https://chat.whatsapp.com/..." }), _jsx(Input, { label: "Member count", type: "number", min: 0, value: String(whatsappBuilder.memberCount), onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, memberCount: Math.max(0, Number(e.target.value) || 0) })) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Benefits" }), whatsappBuilder.benefits.map((benefit, index) => (_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Input, { value: benefit, onChange: (e) => setWhatsappBuilder((prev) => { const next = [...prev.benefits]; next[index] = e.target.value; return { ...prev, benefits: next }; }), className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setWhatsappBuilder((prev) => ({ ...prev, benefits: prev.benefits.filter((_, i) => i !== index) })), children: "\u2715" })] }, `wb-${index}`))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setWhatsappBuilder((prev) => ({ ...prev, benefits: [...prev.benefits, ""] })), children: "+ Add benefit" })] }), _jsx(Input, { label: "Button text", value: whatsappBuilder.buttonText, onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, buttonText: e.target.value })) }), _jsx(Input, { label: "Testimonial (optional)", value: whatsappBuilder.testimonial, onChange: (e) => setWhatsappBuilder((prev) => ({ ...prev, testimonial: e.target.value })) })] }));
|
|
@@ -715,10 +715,10 @@ export function AdminSectionsView({ children }) {
|
|
|
715
715
|
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Trust Indicators Builder" }), _jsx(Input, { label: "Section title", value: trustIndicatorsBuilder.title, onChange: (e) => setTrustIndicatorsBuilder((prev) => ({ ...prev, title: e.target.value })) }), trustIndicatorsBuilder.indicators.map((ind, index) => (_jsxs(Stack, { className: `${__P.p3}`, gap: "sm", rounded: "md", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Indicator ", index + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setTrustIndicatorsBuilder((prev) => ({ ...prev, indicators: prev.indicators.filter((_, i) => i !== index) })), children: "Remove" })] }), _jsx(Input, { label: "Icon (emoji or text)", value: ind.icon, onChange: (e) => setTrustIndicatorsBuilder((prev) => { const next = [...prev.indicators]; next[index] = { ...next[index], icon: e.target.value }; return { ...prev, indicators: next }; }) }), _jsx(Input, { label: "Title", value: ind.title, onChange: (e) => setTrustIndicatorsBuilder((prev) => { const next = [...prev.indicators]; next[index] = { ...next[index], title: e.target.value }; return { ...prev, indicators: next }; }) }), _jsx(Input, { label: "Description", value: ind.description, onChange: (e) => setTrustIndicatorsBuilder((prev) => { const next = [...prev.indicators]; next[index] = { ...next[index], description: e.target.value }; return { ...prev, indicators: next }; }) })] }, ind.id))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setTrustIndicatorsBuilder((prev) => ({ ...prev, indicators: [...prev.indicators, { id: `ti-${Date.now()}`, icon: "✨", title: "", description: "" }] })), children: "+ Add indicator" })] }));
|
|
716
716
|
}
|
|
717
717
|
function renderCategoriesBuilder() {
|
|
718
|
-
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Categories Section Builder" }), _jsx(Input, { label: "Section title", value: categoriesBuilder.title, onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Input, { label: "Max categories (4\u201312)", type: "number", min: 4, max: 12, value: String(categoriesBuilder.maxCategories), onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, maxCategories: Math.min(12, Math.max(4, Number(e.target.value) || 4)) })) }), _jsx(Checkbox, { checked: categoriesBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(categoriesBuilder.scrollInterval), onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) })] }));
|
|
718
|
+
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Categories Section Builder" }), _jsx(Input, { label: "Section title", value: categoriesBuilder.title, onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Input, { label: "Max categories (4\u201312)", type: "number", min: 4, max: 12, value: String(categoriesBuilder.maxCategories), onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, maxCategories: Math.min(12, Math.max(4, Number(e.target.value) || 4)) })) }), _jsx(Checkbox, { checked: categoriesBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(categoriesBuilder.scrollInterval), onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) }), _jsx(Checkbox, { checked: categoriesBuilder.loop, label: "Loop carousel", onChange: (e) => setCategoriesBuilder((prev) => ({ ...prev, loop: e.target.checked })) })] }));
|
|
719
719
|
}
|
|
720
720
|
function renderBrandsBuilder() {
|
|
721
|
-
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Brands Section Builder" }), _jsx(Input, { label: "Section title", value: brandsBuilder.title, onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Input, { label: "Subtitle", value: brandsBuilder.subtitle, onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, subtitle: e.target.value })) }), _jsx(Input, { label: "Max brands", type: "number", min: 1, max: 30, value: String(brandsBuilder.maxBrands), onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, maxBrands: Math.max(1, Number(e.target.value) || 1) })) }), _jsx(Checkbox, { checked: brandsBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(brandsBuilder.scrollInterval), onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) })] }));
|
|
721
|
+
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Brands Section Builder" }), _jsx(Input, { label: "Section title", value: brandsBuilder.title, onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, title: e.target.value })) }), _jsx(Input, { label: "Subtitle", value: brandsBuilder.subtitle, onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, subtitle: e.target.value })) }), _jsx(Input, { label: "Max brands", type: "number", min: 1, max: 30, value: String(brandsBuilder.maxBrands), onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, maxBrands: Math.max(1, Number(e.target.value) || 1) })) }), _jsx(Checkbox, { checked: brandsBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(brandsBuilder.scrollInterval), onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, scrollInterval: Math.max(1000, Number(e.target.value) || 1000) })) }), _jsx(Checkbox, { checked: brandsBuilder.loop, label: "Loop carousel", onChange: (e) => setBrandsBuilder((prev) => ({ ...prev, loop: e.target.checked })) })] }));
|
|
722
722
|
}
|
|
723
723
|
function renderCarouselBuilder() {
|
|
724
724
|
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Carousel Section Builder" }), _jsx(Input, { label: "Section title", value: carouselBuilder.title, onChange: (e) => setCarouselBuilder((prev) => ({ ...prev, title: e.target.value })), placeholder: "Hero Carousel" }), _jsx(Select, { label: "Slide height", value: carouselBuilder.height, onValueChange: (value) => setCarouselBuilder((prev) => ({ ...prev, height: value })), options: [
|
|
@@ -786,7 +786,7 @@ export function AdminSectionsView({ children }) {
|
|
|
786
786
|
] }), _jsx(Checkbox, { checked: customCardsBuilder.autoScroll, label: "Auto-scroll", onChange: (e) => setCustomCardsBuilder((prev) => ({ ...prev, autoScroll: e.target.checked })) }), _jsx(Input, { label: LBL_SCROLL_INTERVAL, type: "number", min: 1000, step: 500, value: String(customCardsBuilder.scrollIntervalMs), onChange: (e) => setCustomCardsBuilder((prev) => ({
|
|
787
787
|
...prev,
|
|
788
788
|
scrollIntervalMs: Math.max(1000, Number(e.target.value) || 4000),
|
|
789
|
-
})) }), _jsxs(Stack, { gap: "3", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Cards (", customCardsBuilder.cards.length, ")"] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addCard, children: "Add card" })] }), customCardsBuilder.cards.map((card, index) => renderCardFields(card, index)), customCardsBuilder.cards.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No cards added yet. Click \"Add card\" to start." })) : null] })] }));
|
|
789
|
+
})) }), _jsx(Checkbox, { checked: customCardsBuilder.loop, label: "Loop carousel", onChange: (e) => setCustomCardsBuilder((prev) => ({ ...prev, loop: e.target.checked })) }), _jsxs(Stack, { gap: "3", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Cards (", customCardsBuilder.cards.length, ")"] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addCard, children: "Add card" })] }), customCardsBuilder.cards.map((card, index) => renderCardFields(card, index)), customCardsBuilder.cards.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No cards added yet. Click \"Add card\" to start." })) : null] })] }));
|
|
790
790
|
}
|
|
791
791
|
function renderGoogleReviewsBuilder() {
|
|
792
792
|
return (_jsxs(Div, { className: CLS_SECTION_PANEL, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Google Reviews Builder" }), _jsx(Input, { label: "Google Place ID", value: googleReviewsBuilder.placeId, onChange: (e) => setGoogleReviewsBuilder((prev) => ({ ...prev, placeId: e.target.value })), placeholder: "ChIJ...", helperText: "Find your Place ID at developers.google.com/maps/documentation/places/web-service/place-id" }), _jsx(Input, { label: "Max reviews to show", type: "number", min: 1, max: 20, value: String(googleReviewsBuilder.maxReviews), onChange: (e) => setGoogleReviewsBuilder((prev) => ({
|
|
@@ -108,8 +108,14 @@ export interface ListingViewConfig<TResponse, TRow extends {
|
|
|
108
108
|
onRowClick?: (row: TRow, panel: {
|
|
109
109
|
openEditPanel: (id: string) => void;
|
|
110
110
|
}) => void;
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* When set, the table renders rows as anchor links to this href. Prefer a
|
|
113
|
+
* plain string with an `{id}` placeholder (e.g. "/admin/x/{id}/edit") —
|
|
114
|
+
* it's the only form safe to build inside a Server Component page.tsx,
|
|
115
|
+
* since RSC cannot pass function values across the server/client boundary.
|
|
116
|
+
* A function is only safe when the config itself is assembled client-side.
|
|
117
|
+
*/
|
|
118
|
+
rowHrefTemplate?: string | ((row: TRow) => string);
|
|
113
119
|
/** Hide the table toggle in the toolbar; views with no table column set should use this. */
|
|
114
120
|
hideTableView?: boolean;
|
|
115
121
|
/** Initial view mode (defaults to "table" when not hidden, "grid" when hidden). */
|
|
@@ -44,15 +44,19 @@ export function DataListingView({ config, }) {
|
|
|
44
44
|
const isEditorOpen = panel.isCreateOpen || panel.isEditOpen;
|
|
45
45
|
return (_jsxs(Div, { className: config.className ?? "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: config.searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || config.defaultSort, sortOptions: config.sortOptions, onSortChange: (v) => table.set("sort", v), showTableView: !config.hideTableView, toggles: config.toggles, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: config.primaryAction || config.toolbarExtra ? (_jsxs(Row, { align: "center", gap: "sm", children: [config.toolbarExtra, config.primaryAction && (_jsxs(Button, { gap: "sm", size: "sm", onClick: () => config.primaryAction.onClick({
|
|
46
46
|
openCreatePanel: panel.openCreatePanel,
|
|
47
|
-
}), children: [config.primaryAction.icon ?? _jsx(Plus, { className: "h-4 w-4" }), config.primaryAction.label] }))] })) : undefined }), config.renderAboveContent?.(), bulkActionItems && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActionItems })), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p), pageSize: pageSize, onPageSizeChange: setPageSize, paginationConfig: { showPageSizeSelector: true, pageSizeOptions: [10, 25, 50, 100] } }) }) })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), view === "table" ? (_jsx(DataTable, { columns: config.columns, rows: rows, isLoading: isLoading, emptyLabel: config.emptyLabel ?? `No ${config.title.toLowerCase()} found`, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next
|
|
47
|
+
}), children: [config.primaryAction.icon ?? _jsx(Plus, { className: "h-4 w-4" }), config.primaryAction.label] }))] })) : undefined }), config.renderAboveContent?.(), bulkActionItems && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActionItems })), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p), pageSize: pageSize, onPageSizeChange: setPageSize, paginationConfig: { showPageSizeSelector: true, pageSizeOptions: [10, 25, 50, 100] } }) }) })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), view === "table" && !config.hideTableView ? (_jsx(DataTable, { columns: config.columns, rows: rows, isLoading: isLoading, emptyLabel: config.emptyLabel ?? `No ${config.title.toLowerCase()} found`, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next
|
|
48
48
|
? selection.setSelectedIds(rows.map((r) => r.id))
|
|
49
|
-
: selection.clearSelection(),
|
|
49
|
+
: selection.clearSelection(), rowHrefTemplate: config.rowHrefTemplate, onRowClick: config.onRowClick
|
|
50
50
|
? (row) => config.onRowClick(row, {
|
|
51
51
|
openEditPanel: panel.openEditPanel,
|
|
52
52
|
})
|
|
53
53
|
: config.renderEditor
|
|
54
54
|
? (row) => panel.openEditPanel(row.id)
|
|
55
|
-
: undefined, renderRowActions: config.renderRowActions })) : config.renderCards ? (
|
|
55
|
+
: undefined, renderRowActions: config.renderRowActions })) : config.renderCards ? (
|
|
56
|
+
// The persisted view-mode preference is global, so a "table"
|
|
57
|
+
// value can still reach here on a hideTableView-only view (see
|
|
58
|
+
// the DataTable branch's guard above) — fall back to "grid".
|
|
59
|
+
config.renderCards(rows, view === "table" ? "grid" : view, selectionContext, isLoading)) : (_jsx(AdminViewCards, { rows: rows, view: view === "table" ? "grid" : view, isLoading: isLoading, emptyLabel: config.emptyLabel ?? `No ${config.title.toLowerCase()} found`, onRowClick: (row) => panel.openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), config.renderFilterPanel && (_jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: config.renderFilterPanel({
|
|
56
60
|
pendingFilters,
|
|
57
61
|
setPendingFilters,
|
|
58
62
|
applyFilters,
|
|
@@ -6,7 +6,15 @@ interface DataTableProps<T extends {
|
|
|
6
6
|
columns?: AdminTableColumn<T>[];
|
|
7
7
|
rows: T[];
|
|
8
8
|
isLoading?: boolean;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Row link target. Prefer a plain string containing an `{id}` placeholder
|
|
11
|
+
* (e.g. "/admin/products/{id}/edit") — it's the only form safe to construct
|
|
12
|
+
* inside a Server Component page.tsx, since React Server Components cannot
|
|
13
|
+
* pass function values to Client Components. A function is only safe when
|
|
14
|
+
* both the caller and DataTable are already client-side (e.g. conditional
|
|
15
|
+
* per-row routing inside an already-"use client" view).
|
|
16
|
+
*/
|
|
17
|
+
rowHrefTemplate?: string | ((row: T) => string);
|
|
10
18
|
onRowClick?: (row: T) => void;
|
|
11
19
|
renderRowActions?: (row: T) => React.ReactNode;
|
|
12
20
|
sortKey?: string;
|
|
@@ -23,5 +31,5 @@ interface DataTableProps<T extends {
|
|
|
23
31
|
}
|
|
24
32
|
export declare function DataTable<T extends {
|
|
25
33
|
id: string;
|
|
26
|
-
}>({ columns: columnsProp, rows, isLoading, sortKey, sortDir, onSort, totalPages, currentPage, onPageChange, emptyLabel,
|
|
34
|
+
}>({ columns: columnsProp, rows, isLoading, sortKey, sortDir, onSort, totalPages, currentPage, onPageChange, emptyLabel, rowHrefTemplate, onRowClick, renderRowActions, selectedIds, onToggleSelect, onToggleSelectAll, }: DataTableProps<T>): React.JSX.Element;
|
|
27
35
|
export {};
|
|
@@ -43,9 +43,11 @@ function SelectableRow({ row, columns, isSelected, onToggle, renderRowActions, o
|
|
|
43
43
|
? col.render(row)
|
|
44
44
|
: String(row[col.key] ?? "") }, col.key))), renderRowActions && (_jsx(Td, { padding: "xs-3", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
|
|
45
45
|
}
|
|
46
|
-
export function DataTable({ columns: columnsProp, rows, isLoading, sortKey, sortDir, onSort, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No records found",
|
|
46
|
+
export function DataTable({ columns: columnsProp, rows, isLoading, sortKey, sortDir, onSort, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No records found", rowHrefTemplate, onRowClick, renderRowActions, selectedIds, onToggleSelect, onToggleSelectAll, }) {
|
|
47
47
|
const columns = (columnsProp ?? DEFAULT_COLUMNS);
|
|
48
48
|
const selectionEnabled = Boolean(onToggleSelect);
|
|
49
49
|
const allRowsSelected = selectionEnabled && rows.length > 0 && rows.every((r) => selectedIds?.has(r.id));
|
|
50
|
-
return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "↑" : "↓" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref:
|
|
50
|
+
return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "↑" : "↓" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref: typeof rowHrefTemplate === "function"
|
|
51
|
+
? rowHrefTemplate(row)
|
|
52
|
+
: rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: p }, p))) }))] }));
|
|
51
53
|
}
|
|
@@ -190,7 +190,7 @@ export function parseProductsBuilder(config) {
|
|
|
190
190
|
manualResourceIds: toStringArray(resources.ids).join(", "),
|
|
191
191
|
filterByCategory: toStringValue(config.filterByCategory),
|
|
192
192
|
maxCount: ([5, 10, 20].includes(maxCount) ? maxCount : 10),
|
|
193
|
-
loop: toBooleanValue(config.loop,
|
|
193
|
+
loop: toBooleanValue(config.loop, DEFAULT_PRODUCTS_BUILDER.loop),
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
export function parseAuctionsBuilder(config) {
|
|
@@ -210,7 +210,7 @@ export function parseAuctionsBuilder(config) {
|
|
|
210
210
|
manualResourceIds: toStringArray(resources.ids).join(", "),
|
|
211
211
|
filterByCategory: toStringValue(config.filterByCategory),
|
|
212
212
|
maxCount: ([5, 10, 20].includes(maxCount) ? maxCount : 10),
|
|
213
|
-
loop: toBooleanValue(config.loop,
|
|
213
|
+
loop: toBooleanValue(config.loop, DEFAULT_AUCTIONS_BUILDER.loop),
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
export function parseStatsBuilder(config) {
|
|
@@ -271,7 +271,7 @@ export function parsePreOrdersBuilder(config) {
|
|
|
271
271
|
manualResourceIds: toStringArray(resources.ids).join(", "),
|
|
272
272
|
filterByCategory: toStringValue(config.filterByCategory),
|
|
273
273
|
maxCount: ([5, 10, 20].includes(maxCount) ? maxCount : 10),
|
|
274
|
-
loop: toBooleanValue(config.loop,
|
|
274
|
+
loop: toBooleanValue(config.loop, DEFAULT_PRE_ORDERS_BUILDER.loop),
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
export function parseStoresBuilder(config) {
|
|
@@ -292,7 +292,7 @@ export function parseStoresBuilder(config) {
|
|
|
292
292
|
manualResourceIds: toStringArray(resources.ids).join(", "),
|
|
293
293
|
filterByCategory: toStringValue(config.filterByCategory),
|
|
294
294
|
maxCount: ([5, 10, 20].includes(maxCount) ? maxCount : 10),
|
|
295
|
-
loop: toBooleanValue(config.loop,
|
|
295
|
+
loop: toBooleanValue(config.loop, DEFAULT_STORES_BUILDER.loop),
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
export function parseEventsBuilder(config) {
|
|
@@ -313,7 +313,7 @@ export function parseEventsBuilder(config) {
|
|
|
313
313
|
manualResourceIds: toStringArray(resources.ids).join(", "),
|
|
314
314
|
filterByCategory: toStringValue(config.filterByCategory),
|
|
315
315
|
maxCount: ([5, 10, 20].includes(maxCount) ? maxCount : 10),
|
|
316
|
-
loop: toBooleanValue(config.loop,
|
|
316
|
+
loop: toBooleanValue(config.loop, DEFAULT_EVENTS_BUILDER.loop),
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
export function buildSocialFeedConfig(builder) {
|
|
@@ -395,6 +395,7 @@ export function buildCategoriesConfig(builder) {
|
|
|
395
395
|
maxCategories: builder.maxCategories,
|
|
396
396
|
autoScroll: builder.autoScroll,
|
|
397
397
|
scrollInterval: builder.scrollInterval,
|
|
398
|
+
loop: builder.loop,
|
|
398
399
|
};
|
|
399
400
|
}
|
|
400
401
|
export function parseCategoriesBuilder(config) {
|
|
@@ -403,6 +404,7 @@ export function parseCategoriesBuilder(config) {
|
|
|
403
404
|
maxCategories: toNumberValue(config.maxCategories, DEFAULT_CATEGORIES_BUILDER.maxCategories),
|
|
404
405
|
autoScroll: toBooleanValue(config.autoScroll, DEFAULT_CATEGORIES_BUILDER.autoScroll),
|
|
405
406
|
scrollInterval: toNumberValue(config.scrollInterval, DEFAULT_CATEGORIES_BUILDER.scrollInterval),
|
|
407
|
+
loop: toBooleanValue(config.loop, DEFAULT_CATEGORIES_BUILDER.loop),
|
|
406
408
|
};
|
|
407
409
|
}
|
|
408
410
|
export function buildBrandsConfig(builder) {
|
|
@@ -412,6 +414,7 @@ export function buildBrandsConfig(builder) {
|
|
|
412
414
|
maxBrands: builder.maxBrands,
|
|
413
415
|
autoScroll: builder.autoScroll,
|
|
414
416
|
scrollInterval: builder.scrollInterval,
|
|
417
|
+
loop: builder.loop,
|
|
415
418
|
};
|
|
416
419
|
}
|
|
417
420
|
export function parseBrandsBuilder(config) {
|
|
@@ -421,6 +424,7 @@ export function parseBrandsBuilder(config) {
|
|
|
421
424
|
maxBrands: toNumberValue(config.maxBrands, DEFAULT_BRANDS_BUILDER.maxBrands),
|
|
422
425
|
autoScroll: toBooleanValue(config.autoScroll, DEFAULT_BRANDS_BUILDER.autoScroll),
|
|
423
426
|
scrollInterval: toNumberValue(config.scrollInterval, DEFAULT_BRANDS_BUILDER.scrollInterval),
|
|
427
|
+
loop: toBooleanValue(config.loop, DEFAULT_BRANDS_BUILDER.loop),
|
|
424
428
|
};
|
|
425
429
|
}
|
|
426
430
|
export function buildBannerConfig(builder) {
|
|
@@ -486,6 +490,7 @@ export function buildReviewsConfig(builder) {
|
|
|
486
490
|
mobileItemsPerView: 1,
|
|
487
491
|
autoScroll: builder.autoScroll,
|
|
488
492
|
scrollInterval: builder.scrollInterval,
|
|
493
|
+
loop: builder.loop,
|
|
489
494
|
source: builder.source,
|
|
490
495
|
placeId: builder.source === "google" ? builder.placeId || undefined : undefined,
|
|
491
496
|
};
|
|
@@ -497,6 +502,7 @@ export function parseReviewsBuilder(config) {
|
|
|
497
502
|
itemsPerView: toNumberValue(config.itemsPerView, DEFAULT_REVIEWS_BUILDER.itemsPerView),
|
|
498
503
|
autoScroll: toBooleanValue(config.autoScroll, DEFAULT_REVIEWS_BUILDER.autoScroll),
|
|
499
504
|
scrollInterval: toNumberValue(config.scrollInterval, DEFAULT_REVIEWS_BUILDER.scrollInterval),
|
|
505
|
+
loop: toBooleanValue(config.loop, DEFAULT_REVIEWS_BUILDER.loop),
|
|
500
506
|
source: toStringValue(config.source, "platform"),
|
|
501
507
|
placeId: toStringValue(config.placeId),
|
|
502
508
|
};
|
|
@@ -613,6 +619,7 @@ export function buildCustomCardsConfig(builder) {
|
|
|
613
619
|
columns: builder.columns,
|
|
614
620
|
autoScroll: builder.autoScroll,
|
|
615
621
|
scrollIntervalMs: builder.scrollIntervalMs,
|
|
622
|
+
loop: builder.loop,
|
|
616
623
|
cards: builder.cards.map((card, index) => ({
|
|
617
624
|
id: card.id || `card-${index + 1}`,
|
|
618
625
|
image: card.image || undefined,
|
|
@@ -639,6 +646,7 @@ export function parseCustomCardsBuilder(config) {
|
|
|
639
646
|
columns: ([1, 2, 3, 4].includes(cols) ? cols : DEFAULT_CUSTOM_CARDS_BUILDER.columns),
|
|
640
647
|
autoScroll: toBooleanValue(config.autoScroll, DEFAULT_CUSTOM_CARDS_BUILDER.autoScroll),
|
|
641
648
|
scrollIntervalMs: toNumberValue(config.scrollIntervalMs, DEFAULT_CUSTOM_CARDS_BUILDER.scrollIntervalMs),
|
|
649
|
+
loop: toBooleanValue(config.loop, DEFAULT_CUSTOM_CARDS_BUILDER.loop),
|
|
642
650
|
cards: cardsRaw.map((item, index) => {
|
|
643
651
|
const c = (item ?? {});
|
|
644
652
|
const br = toStringValue(c.borderRadius, "none");
|
|
@@ -150,6 +150,7 @@ export interface CategoriesBuilderState {
|
|
|
150
150
|
maxCategories: number;
|
|
151
151
|
autoScroll: boolean;
|
|
152
152
|
scrollInterval: number;
|
|
153
|
+
loop: boolean;
|
|
153
154
|
}
|
|
154
155
|
export interface BrandsBuilderState {
|
|
155
156
|
title: string;
|
|
@@ -157,6 +158,7 @@ export interface BrandsBuilderState {
|
|
|
157
158
|
maxBrands: number;
|
|
158
159
|
autoScroll: boolean;
|
|
159
160
|
scrollInterval: number;
|
|
161
|
+
loop: boolean;
|
|
160
162
|
}
|
|
161
163
|
export interface BannerBuilderState {
|
|
162
164
|
height: "sm" | "md" | "lg" | "xl";
|
|
@@ -185,6 +187,7 @@ export interface ReviewsBuilderState {
|
|
|
185
187
|
itemsPerView: number;
|
|
186
188
|
autoScroll: boolean;
|
|
187
189
|
scrollInterval: number;
|
|
190
|
+
loop: boolean;
|
|
188
191
|
source: "platform" | "google";
|
|
189
192
|
placeId: string;
|
|
190
193
|
}
|
|
@@ -249,6 +252,7 @@ export interface CustomCardsBuilderState {
|
|
|
249
252
|
columns: 1 | 2 | 3 | 4;
|
|
250
253
|
autoScroll: boolean;
|
|
251
254
|
scrollIntervalMs: number;
|
|
255
|
+
loop: boolean;
|
|
252
256
|
cards: CustomCardsCardBuilderEntry[];
|
|
253
257
|
}
|
|
254
258
|
export interface GoogleReviewsBuilderState {
|