@mohasinac/appkit 3.9.1 → 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.
Files changed (58) hide show
  1. package/dist/_internal/shared/actions/action-registry.js +8 -0
  2. package/dist/client.d.ts +2 -2
  3. package/dist/client.js +1 -4
  4. package/dist/constants/api-endpoints.d.ts +3 -0
  5. package/dist/constants/api-endpoints.js +1 -0
  6. package/dist/features/account/hooks/useDataViewMode.d.ts +17 -0
  7. package/dist/features/account/hooks/useDataViewMode.js +55 -0
  8. package/dist/features/account/hooks/useProfile.d.ts +2 -0
  9. package/dist/features/admin/components/AdminBundlesView.d.ts +3 -4
  10. package/dist/features/admin/components/AdminBundlesView.js +2 -2
  11. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  12. package/dist/features/admin/components/AdminCategoriesView.d.ts +2 -6
  13. package/dist/features/admin/components/AdminCategoriesView.js +1 -1
  14. package/dist/features/admin/components/AdminFaqsView.d.ts +0 -2
  15. package/dist/features/admin/components/AdminPrizeDrawsView.js +1 -1
  16. package/dist/features/admin/components/DataListingView.d.ts +8 -2
  17. package/dist/features/admin/components/DataListingView.js +7 -3
  18. package/dist/features/admin/components/DataTable.d.ts +10 -2
  19. package/dist/features/admin/components/DataTable.js +4 -2
  20. package/dist/features/admin/hooks/useAdminListing.d.ts +2 -2
  21. package/dist/features/admin/hooks/useAdminListing.js +6 -1
  22. package/dist/features/categories/components/BrandDetailTabs.js +2 -12
  23. package/dist/features/categories/components/CategoryDetailTabs.js +2 -12
  24. package/dist/features/events/components/AdminEventsView.d.ts +1 -5
  25. package/dist/features/faq/components/FAQAccordion.js +3 -2
  26. package/dist/features/faq/components/RelatedFAQs.js +1 -1
  27. package/dist/features/homepage/components/FAQSection.js +3 -3
  28. package/dist/features/layout/FooterLayout.js +2 -2
  29. package/dist/features/lottery/schemas/zod.d.ts +4 -4
  30. package/dist/features/payments/schemas/firestore.d.ts +6 -0
  31. package/dist/features/payments/schemas/firestore.js +2 -0
  32. package/dist/features/seller/components/SellerBundlesView.d.ts +3 -4
  33. package/dist/features/seller/components/SellerBundlesView.js +2 -2
  34. package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -5
  35. package/dist/features/seller/components/SellerPayoutsView.js +100 -21
  36. package/dist/features/seller/components/SellerPreOrdersView.js +1 -1
  37. package/dist/features/seller/components/SellerPrizeDrawsView.js +1 -1
  38. package/dist/features/seller/components/SellerProductsView.js +6 -2
  39. package/dist/features/shipments/components/AdminShipmentsView.d.ts +1 -5
  40. package/dist/index.d.ts +3 -5
  41. package/dist/index.js +3 -3
  42. package/dist/react/index.d.ts +0 -2
  43. package/dist/react/index.js +0 -2
  44. package/dist/seed/faq-seed-data.js +11 -11
  45. package/dist/seed/homepage-sections-seed-data.js +1 -1
  46. package/dist/styles.css +30 -99
  47. package/dist/tailwind-utilities.css +1 -1
  48. package/dist/ui/DataTable.js +1 -1
  49. package/dist/ui/DataTable.style.css +10 -0
  50. package/dist/ui/components/DetailPageTabs.d.ts +1 -1
  51. package/dist/ui/components/DetailPageTabs.js +6 -30
  52. package/dist/ui/components/Tabs.d.ts +9 -1
  53. package/dist/ui/components/Tabs.js +13 -8
  54. package/dist/ui/components/Tabs.style.css +19 -0
  55. package/dist/ui/components/index.style.css +0 -1
  56. package/dist/ui/index.d.ts +0 -2
  57. package/dist/ui/index.js +0 -1
  58. 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 { TabStrip } from "./ui/components/TabStrip";
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 { TabStrip } from "./ui/components/TabStrip";
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
- getEditHref: (row: {
4
- id: string;
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({ getEditHref, newHref }: AdminBundlesViewProps): React.JSX.Element;
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({ getEditHref, newHref }) {
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
- getRowHref: getEditHref,
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", getRowHref: (row) => `/admin/carousel/${row.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 }))] }));
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 interface AdminCategoriesViewProps extends ListingLayoutProps {
4
- getRowHref?: (row: {
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, getRowHref: _getRowHref, ...props }) {
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
- getRowHref: (row) => String(ROUTES.ADMIN.PRIZE_DRAWS_EDIT(row.id)),
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,
@@ -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
- /** When set, the table renders rows as anchor links to this href. */
112
- getRowHref?: (row: TRow) => string;
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(), getRowHref: config.getRowHref, onRowClick: config.onRowClick
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 ? (config.renderCards(rows, view, selectionContext, isLoading)) : (_jsx(AdminViewCards, { rows: rows, view: 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({
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
- getRowHref?: (row: T) => string;
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, getRowHref, onRowClick, renderRowActions, selectedIds, onToggleSelect, onToggleSelectAll, }: DataTableProps<T>): React.JSX.Element;
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", getRowHref, onRowClick, renderRowActions, selectedIds, onToggleSelect, onToggleSelectAll, }) {
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: getRowHref?.(row), 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))) }))] }));
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
  }
@@ -14,8 +14,8 @@ export interface AdminListingConfig<TResponse, TRow extends {
14
14
  export declare function useAdminListing<TResponse, TRow extends {
15
15
  id: string;
16
16
  }>(config: AdminListingConfig<TResponse, TRow>): {
17
- view: "grid" | "list" | "table";
18
- setView: import("react").Dispatch<import("react").SetStateAction<"grid" | "list" | "table">>;
17
+ view: import("../../account/hooks/useDataViewMode").DataViewMode;
18
+ setView: (next: import("../../account/hooks/useDataViewMode").DataViewMode) => void;
19
19
  table: {
20
20
  params: URLSearchParams;
21
21
  get: (key: string) => string;
@@ -3,10 +3,15 @@ import { useState, useCallback } from "react";
3
3
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
4
4
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
5
5
  import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
6
+ import { useDataViewMode } from "../../account/hooks/useDataViewMode";
6
7
  import { useAdminListingData } from "./useAdminListingData";
7
8
  export function useAdminListing(config) {
8
9
  const { filterKeys, defaultSort, pageSize: defaultPageSize = 25, queryKey, endpoint, mapRows, getTotal, buildFilters, } = config;
9
- const [view, setView] = useState(config.initialView ?? "table");
10
+ // Persisted, viewport-aware view-mode: below 768px defaults to "list"
11
+ // (AdminViewCards' one-full-width-card-per-row layout) unless the caller
12
+ // explicitly pins hideTableView (grid-only views like coupons); the user's
13
+ // own explicit choice, once made, always wins and persists across visits.
14
+ const { view, setView } = useDataViewMode(config.initialView ?? "table");
10
15
  const table = useUrlTable({ defaults: { pageSize: String(defaultPageSize), sort: defaultSort } });
11
16
  // Reactive — table.getNumber reads the URL param, so a page-size selector
12
17
  // (Pagination's pageSize/onPageSizeChange) actually takes effect instead
@@ -1,21 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Div } from "../../../ui";
4
+ import { Tabs, TabsList, TabsTrigger } from "../../../ui";
5
5
  import { CategoryProductsListing } from "./CategoryProductsListing";
6
6
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
7
7
  import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
8
8
  import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
9
9
  import { CategoryBundlesListing } from "./CategoryBundlesListing";
10
10
  import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
11
- const __O = {
12
- xAuto: "overflow-x-auto",
13
- };
14
- function tabLabel(label, count) {
15
- if (!count)
16
- return label;
17
- return `${label} (${count.toLocaleString()})`;
18
- }
19
11
  const TAB_TYPE_MAP = {
20
12
  products: { kind: "listing", type: "standard" },
21
13
  auctions: { kind: "listing", type: "auction" },
@@ -48,7 +40,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
48
40
  default: return undefined;
49
41
  }
50
42
  };
51
- return (_jsxs(_Fragment, { children: [_jsx(Div, { layout: "flex", border: "default", className: `border-b mb-6 ${__O.xAuto}`, children: visibleTabs.map((t) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setActiveTab(t.id), paddingX: "lg", paddingY: "y-xs-tall", textSize: "sm", weight: "medium", className: `whitespace-nowrap transition-colors -mb-px border-b-2 rounded-none ${activeTab === t.id
52
- ? "border-primary text-primary"
53
- : "border-transparent text-[var(--appkit-color-text-muted)] hover:text-zinc-900 dark:hover:text-zinc-100"}`, children: tabLabel(t.label, countFor(t.id)) }, t.id))) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
43
+ return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
54
44
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Div } from "../../../ui";
4
+ import { Tabs, TabsList, TabsTrigger } from "../../../ui";
5
5
  import { CategoryProductsListing } from "./CategoryProductsListing";
6
6
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
7
7
  import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
@@ -9,14 +9,6 @@ import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsInde
9
9
  import { CategoryBundlesListing } from "./CategoryBundlesListing";
10
10
  import { CategoryStoresListing } from "./CategoryStoresListing";
11
11
  import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
12
- const __O = {
13
- xAuto: "overflow-x-auto",
14
- };
15
- function tabLabel(label, count) {
16
- if (!count)
17
- return label;
18
- return `${label} (${count.toLocaleString()})`;
19
- }
20
12
  /** Maps CATEGORY_PAGE_TABS id → listing type / category type key for flag filtering. */
21
13
  const TAB_TYPE_MAP = {
22
14
  products: { kind: "listing", type: "standard" },
@@ -54,7 +46,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
54
46
  default: return undefined;
55
47
  }
56
48
  };
57
- return (_jsxs(_Fragment, { children: [_jsx(Div, { layout: "flex", border: "default", className: `border-b mb-6 ${__O.xAuto}`, children: visibleTabs.map((t) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setActiveTab(t.id), paddingX: "lg", paddingY: "y-xs-tall", textSize: "sm", weight: "medium", className: `whitespace-nowrap transition-colors -mb-px border-b-2 rounded-none ${activeTab === t.id
58
- ? "border-primary text-primary"
59
- : "border-transparent text-[var(--appkit-color-text-muted)] hover:text-zinc-900 dark:hover:text-zinc-100"}`, children: tabLabel(t.label, countFor(t.id)) }, t.id))) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
49
+ return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
60
50
  }
@@ -1,8 +1,4 @@
1
1
  import React from "react";
2
2
  import type { ListingLayoutProps } from "../../../ui";
3
- export interface AdminEventsViewProps extends ListingLayoutProps {
4
- getRowHref?: (row: {
5
- id: string;
6
- }) => string;
7
- }
3
+ export type AdminEventsViewProps = ListingLayoutProps;
8
4
  export declare function AdminEventsView({ children, ...props }: AdminEventsViewProps): React.JSX.Element;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Div, RichText, Row, Span, Text } from "../../../ui";
4
+ import { Button, Div, RichText, Row, Span, Tabs, TabsList, TabsTrigger, Text } from "../../../ui";
5
5
  function FAQAccordionItem({ faq, isOpen, onToggle, renderExpandedFooter, }) {
6
6
  return (_jsxs(Div, { className: "border-b border-neutral-200 last:border-0", children: [_jsxs(Button, { variant: "ghost", onClick: onToggle, "aria-expanded": isOpen, className: "w-full justify-[space-between] gap-[1rem] py-[1rem] text-left text-[0.875rem] font-[500] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] transition hover:text-primary", children: [_jsx(Span, { children: faq.question }), _jsx(Span, { className: `flex-shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), isOpen && (_jsxs(Div, { textSize: "sm", className: "text-[var(--appkit-color-text-muted)]", padding: "b-md", children: [faq.tags && faq.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-3", children: faq.tags.map((tag) => (_jsx(Span, { className: "bg-[var(--appkit-color-surface)] py-[0.25rem] text-[var(--appkit-color-text-muted)]", rounded: "md", padding: "x-xs", size: "xs", children: tag }, tag))) })), faq.answer.format === "html" ? (_jsx(RichText, { html: faq.answer.text, className: "prose-sm" })) : (_jsx(Text, { className: "whitespace-pre-line", children: faq.answer.text })), renderExpandedFooter?.(faq)] }))] }));
7
7
  }
@@ -12,6 +12,7 @@ export function FAQAccordion({ faqs, className = "", renderExpandedFooter, label
12
12
  }
13
13
  return (_jsx(Div, { surface: "card", className: `divide-y divide-neutral-100 divide-[var(--appkit-color-border)] ${className}`, padding: "x-md", children: faqs.map((faq) => (_jsx(FAQAccordionItem, { faq: faq, isOpen: openId === faq.id, onToggle: () => setOpenId(openId === faq.id ? null : faq.id), renderExpandedFooter: renderExpandedFooter }, faq.id))) }));
14
14
  }
15
+ const ALL_TAB_VALUE = "__all__";
15
16
  export function FAQCategoryTabs({ categories, active, onSelect, labels = {}, }) {
16
- return (_jsxs(Row, { wrap: true, gap: "sm", className: "scrollbar-none", children: [_jsx(Button, { onClick: () => onSelect(null), className: `rounded-[9999px] px-[1rem] py-[0.375rem] text-[0.875rem] font-[500] transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] dark:text-[var(--appkit-color-text-faint)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), className: `whitespace-nowrap rounded-[9999px] px-[1rem] py-[0.375rem] text-[0.875rem] font-[500] capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] dark:text-[var(--appkit-color-text-faint)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels[cat] ?? cat.replace(/_/g, " ") }, cat)))] }));
17
+ return (_jsx(Tabs, { value: active ?? ALL_TAB_VALUE, onChange: (next) => onSelect(next === ALL_TAB_VALUE ? null : next), children: _jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: ALL_TAB_VALUE, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(TabsTrigger, { value: cat, children: labels[cat] ?? cat.replace(/_/g, " ") }, cat)))] }) }));
17
18
  }
@@ -4,5 +4,5 @@ export function RelatedFAQs({ relatedFAQs, hrefForFaq, labels, }) {
4
4
  if (!relatedFAQs || relatedFAQs.length === 0) {
5
5
  return null;
6
6
  }
7
- return (_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Heading, { level: 3, className: "mb-4", size: "lg", weight: "semibold", children: labels?.title ?? "Related Questions" }), _jsx(Stack, { gap: "3", children: relatedFAQs.map((faq) => (_jsx(TextLink, { rounded: "lg", href: hrefForFaq ? hrefForFaq(faq) : `/faqs#${faq.id}`, className: "group block bg-[var(--appkit-color-surface-elevated)] p-[1rem] transition-colors hover:bg-[var(--appkit-color-border-subtle)]", children: _jsxs(Row, { align: "start", gap: "3", children: [_jsx("svg", { className: "mt-0.5 h-5 w-5 flex-shrink-0 text-[var(--appkit-color-text-muted)]", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }), _jsx(Text, { className: "transition-colors group-hover:text-primary", color: "primary", size: "sm", children: faq.question }), _jsx("svg", { className: "ml-auto mt-0.5 h-4 w-4 flex-shrink-0 text-[var(--appkit-color-text-muted)] opacity-0 transition-opacity group-hover:opacity-100 text-[var(--appkit-color-text-muted)]", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] }) }, faq.id))) })] }));
7
+ return (_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Heading, { level: 3, className: "mb-4", size: "lg", weight: "semibold", children: labels?.title ?? "Related Questions" }), _jsx(Stack, { gap: "none", className: "divide-y divide-[var(--appkit-color-border)]", children: relatedFAQs.map((faq) => (_jsx(TextLink, { href: hrefForFaq ? hrefForFaq(faq) : `/faqs#${faq.id}`, className: "group block p-[1rem] transition-colors hover:bg-[var(--appkit-color-surface-elevated)]", children: _jsxs(Row, { align: "start", gap: "3", children: [_jsx("svg", { className: "mt-0.5 h-5 w-5 flex-shrink-0 text-[var(--appkit-color-text-muted)]", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }), _jsx(Text, { className: "transition-colors group-hover:text-primary", color: "primary", size: "sm", children: faq.question }), _jsx("svg", { className: "ml-auto mt-0.5 h-4 w-4 flex-shrink-0 text-[var(--appkit-color-text-muted)] opacity-0 transition-opacity group-hover:opacity-100 text-[var(--appkit-color-text-muted)]", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] }) }, faq.id))) })] }));
8
8
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useMemo } from "react";
4
4
  import { THEMED_BG_PRIMARY, THEMED_BG_TERTIARY, THEMED_BORDER, THEMED_TEXT_PRIMARY, THEMED_TEXT_SECONDARY, FLEX_BETWEEN } from "../../../_internal/shared/styles/themed";
5
- import { Button, Div, Heading, RichText, Row, Section, Span, Stack, Text, TextLink } from "../../../ui";
5
+ import { Button, Div, Heading, RichText, Section, Span, Stack, Tabs, TabsList, TabsTrigger, Text, TextLink } from "../../../ui";
6
6
  import { ChevronDown } from "lucide-react";
7
7
  const __O = {
8
8
  hidden: "overflow-hidden",
@@ -67,8 +67,8 @@ export function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategor
67
67
  return items;
68
68
  return items.filter((i) => (i.category ?? "general") === activeTab);
69
69
  }, [items, showTabs, activeTab]);
70
- return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsxs(Div, { className: "w-full max-w-4xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, variant: "none", gradient: "brand", className: "mb-3", mdSize: "4xl", size: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `max-w-xl ${themed.textSecondary}`, size: "base", children: subtitle }))] }), showTabs && (_jsxs(Row, { align: "center", justify: "start", gap: "sm", wrap: true, className: `mb-8`, children: [_jsx(Button, { rounded: "full", variant: activeTab === "all" ? "primary" : "ghost", size: "sm", onClick: () => setActiveTab("all"), children: "All" }), derivedTabs.map((tab) => (_jsx(Button, { rounded: "full", variant: activeTab === tab.value ? "primary" : "ghost", size: "sm", onClick: () => setActiveTab(tab.value), children: tab.label }, tab.value)))] })), _jsxs(Stack, { gap: "sm", children: [visibleItems.length === 0 && (_jsx(Text, { paddingY: "3xl", align: "start", className: themed.textSecondary, children: "No FAQs in this category yet." })), visibleItems.map((faq) => {
70
+ return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsxs(Div, { className: "w-full max-w-4xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [_jsx(Heading, { level: 2, variant: "none", gradient: "brand", className: "mb-3", mdSize: "4xl", size: "3xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { className: `max-w-xl ${themed.textSecondary}`, size: "base", children: subtitle }))] }), showTabs && (_jsx(Tabs, { value: activeTab, onChange: setActiveTab, className: "mb-8", children: _jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "all", children: "All" }), derivedTabs.map((tab) => (_jsx(TabsTrigger, { value: tab.value, children: tab.label }, tab.value)))] }) })), _jsxs(Stack, { gap: "none", rounded: "xl", className: `${themed.bgPrimary} ${__O.hidden} border ${themed.border}`, children: [visibleItems.length === 0 && (_jsx(Text, { paddingY: "3xl", align: "start", className: themed.textSecondary, children: "No FAQs in this category yet." })), visibleItems.map((faq, index) => {
71
71
  const isOpen = openIds.has(faq.id);
72
- return (_jsxs(Div, { className: `${themed.bgPrimary} rounded-xl overflow-hidden border transition-all duration-200 ${isOpen ? "border-primary/40 shadow-sm" : `${themed.border}`}`, children: [_jsxs(Button, { variant: "ghost", rounded: "none", className: `w-full text-left px-[1.25rem] py-[1rem] ${flex.between} gap-[1rem] hover:${themed.bgTertiary} transition-colors`, onClick: () => toggle(faq.id), "aria-expanded": isOpen, children: [_jsx(Span, { size: "sm", weight: "medium", className: `flex-1 leading-snug ${isOpen ? "text-primary" : themed.textPrimary}`, children: faq.question }), _jsx(ChevronDown, { className: `w-4 h-4 flex-shrink-0 transition-transform duration-250 ${isOpen ? "rotate-180 text-primary" : themed.textSecondary}` })] }), _jsx(Div, { layout: "grid", className: `transition-all duration-300 ease-out ${isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"}`, children: _jsx(Div, { className: `${__O.hidden}`, children: _jsx(Div, { paddingY: "b-md-lg", padding: "x-md", children: isHtml(faq.answer) ? (_jsx(RichText, { html: faq.answer, proseClass: "prose prose-sm max-w-none dark:prose-invert" })) : (_jsx(Text, { className: `leading-relaxed ${themed.textSecondary}`, size: "sm", children: faq.answer })) }) }) })] }, faq.id));
72
+ return (_jsxs(Div, { className: `transition-colors duration-200 ${index < visibleItems.length - 1 ? "border-b border-[var(--appkit-color-border)]" : ""} ${isOpen ? "bg-[var(--appkit-color-surface-elevated)]" : ""}`, children: [_jsxs(Button, { variant: "ghost", rounded: "none", className: `w-full text-left px-[1.25rem] py-[1rem] ${flex.between} gap-[1rem] hover:${themed.bgTertiary} transition-colors`, onClick: () => toggle(faq.id), "aria-expanded": isOpen, children: [_jsx(Span, { size: "sm", weight: "medium", className: `flex-1 leading-snug ${isOpen ? "text-primary" : themed.textPrimary}`, children: faq.question }), _jsx(ChevronDown, { className: `w-4 h-4 flex-shrink-0 transition-transform duration-250 ${isOpen ? "rotate-180 text-primary" : themed.textSecondary}` })] }), _jsx(Div, { layout: "grid", className: `transition-all duration-300 ease-out ${isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"}`, children: _jsx(Div, { className: `${__O.hidden}`, children: _jsx(Div, { paddingY: "b-md-lg", padding: "x-md", children: isHtml(faq.answer) ? (_jsx(RichText, { html: faq.answer, proseClass: "prose prose-sm max-w-none dark:prose-invert" })) : (_jsx(Text, { className: `leading-relaxed ${themed.textSecondary}`, size: "sm", children: faq.answer })) }) }) })] }, faq.id));
73
73
  })] }), viewMoreHref && (_jsx(Div, { className: "text-left mt-10", children: _jsxs(TextLink, { rounded: "full", paddingX: "xl", paddingY: "sm", href: viewMoreHref, className: "inline-flex items-[center] gap-[0.5rem] bg-primary/10 text-primary hover:bg-primary/20 dark:bg-primary/15 transition-colors", size: "sm", weight: "medium", children: [viewMoreLabel, hasMore && moreCount > 0 && (_jsxs(Span, { color: "inverse", size: "xs", className: "bg-primary", rounded: "full", padding: "pill-xs", children: ["+", moreCount] }))] }) }))] }) }));
74
74
  }