@mohasinac/appkit 4.0.3 → 4.1.1

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 (67) hide show
  1. package/dist/_internal/client/features/layout/useSidebarSearch.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/useSidebarSearch.js +21 -0
  3. package/dist/_internal/server/features/auth/permissions.d.ts +2 -0
  4. package/dist/_internal/server/features/auth/permissions.js +4 -2
  5. package/dist/_internal/server/features/seo/sitemap.js +3 -2
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +1 -1
  8. package/dist/features/about/components/FAQPageView.d.ts +4 -1
  9. package/dist/features/about/components/FAQPageView.js +17 -7
  10. package/dist/features/account/components/UserSidebar.js +15 -9
  11. package/dist/features/admin/components/AdminSidebar.js +15 -9
  12. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +4 -1
  13. package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
  14. package/dist/features/admin/components/AdminUserEditorView.js +10 -2
  15. package/dist/features/admin/components/AdminUsersView.js +1 -1
  16. package/dist/features/auth/role-predicates.d.ts +14 -0
  17. package/dist/features/auth/role-predicates.js +1 -0
  18. package/dist/features/auth/schemas/firestore.d.ts +2 -0
  19. package/dist/features/auth/schemas/firestore.js +1 -0
  20. package/dist/features/events/actions/event-actions.js +15 -8
  21. package/dist/features/events/repository/events.repository.d.ts +2 -0
  22. package/dist/features/events/repository/events.repository.js +9 -0
  23. package/dist/features/events/schemas/firestore.d.ts +1 -0
  24. package/dist/features/events/schemas/firestore.js +1 -0
  25. package/dist/features/faq/components/FAQSearchableList.d.ts +11 -0
  26. package/dist/features/faq/components/FAQSearchableList.js +21 -0
  27. package/dist/features/homepage/components/BeforeAfterCard.js +2 -1
  28. package/dist/features/homepage/components/BrandsSection.js +2 -1
  29. package/dist/features/homepage/components/CharacterHotspot.js +2 -1
  30. package/dist/features/homepage/components/CharacterHotspotForm.js +4 -3
  31. package/dist/features/homepage/components/HeroBanner.js +2 -1
  32. package/dist/features/homepage/components/PromoGrid.js +2 -1
  33. package/dist/features/homepage/components/ShopByCategorySection.js +2 -1
  34. package/dist/features/seller/components/SellerSidebar.js +15 -9
  35. package/dist/features/tester/actions/checklist-item-actions.d.ts +6 -0
  36. package/dist/features/tester/actions/checklist-item-actions.js +2 -0
  37. package/dist/features/tester/components/TesterHubView.js +5 -4
  38. package/dist/features/tester/schemas/firestore.d.ts +3 -1
  39. package/dist/features/tester/schemas/firestore.js +2 -0
  40. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +2 -0
  41. package/dist/features/tester/seed-data/bids-tester-seed-data.js +37 -0
  42. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +2 -0
  43. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +76 -0
  44. package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -2
  45. package/dist/features/tester/seed-data/index.d.ts +3 -0
  46. package/dist/features/tester/seed-data/index.js +3 -0
  47. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +2 -0
  48. package/dist/features/tester/seed-data/orders-tester-seed-data.js +265 -0
  49. package/dist/features/tester/seed-data/products-tester-seed-data.js +213 -8
  50. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +421 -9
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +1 -1
  53. package/dist/next/api/routeHandler.js +22 -4
  54. package/dist/react/contexts/SessionContext.d.ts +2 -0
  55. package/dist/react/contexts/SessionContext.js +2 -0
  56. package/dist/seed/index.d.ts +1 -1
  57. package/dist/seed/index.js +1 -1
  58. package/dist/seed/manifest.js +4 -4
  59. package/dist/seed/orders-seed-data.js +4 -0
  60. package/dist/seed/products-classifieds-seed-data.js +48 -5
  61. package/dist/seed/products-digital-codes-seed-data.js +48 -5
  62. package/dist/seed/products-live-items-seed-data.js +6 -1
  63. package/dist/seed/products-prize-draws-seed-data.js +58 -5
  64. package/dist/styles.css +1 -1
  65. package/dist/tailwind-utilities.css +1 -1
  66. package/package.json +1 -1
  67. package/scripts/seed-cli.mjs +10 -4
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { Div, Input } from "../../../ui";
5
+ import { FAQAccordion } from "./FAQAccordion";
6
+ const STRIP_HTML = /<[^>]+>/g;
7
+ export function FAQSearchableList({ faqs, labels }) {
8
+ const [query, setQuery] = useState("");
9
+ const filtered = useMemo(() => {
10
+ const q = query.trim().toLowerCase();
11
+ if (!q)
12
+ return faqs;
13
+ return faqs.filter((faq) => {
14
+ const answerText = faq.answer.text.replace(STRIP_HTML, "").toLowerCase();
15
+ return (faq.question.toLowerCase().includes(q) ||
16
+ answerText.includes(q) ||
17
+ (faq.tags?.some((tag) => tag.toLowerCase().includes(q)) ?? false));
18
+ });
19
+ }, [faqs, query]);
20
+ return (_jsxs(Div, { children: [_jsx(Div, { className: "mb-6", children: _jsx(Input, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: labels.searchPlaceholder, "aria-label": labels.searchPlaceholder }) }), _jsx(FAQAccordion, { faqs: filtered, labels: { noResults: labels.noResults } })] }));
21
+ }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState, useCallback, useEffect } from "react";
4
4
  import Image from "next/image";
5
5
  import { Div, Row, Span, Text } from "../../../ui";
6
+ import { resolveMediaUrl } from "../../../utils/media-url";
6
7
  const __P = {
7
8
  p4: "p-[var(--appkit-space-4)]",
8
9
  };
@@ -68,5 +69,5 @@ export function BeforeAfterCard({ item, beforeLabel = "Before", afterLabel = "Af
68
69
  setPosition((p) => Math.max(0, p - 2));
69
70
  if (e.key === "ArrowRight")
70
71
  setPosition((p) => Math.min(100, p + 2));
71
- }, children: [_jsx(Image, { src: item.afterImage, alt: afterLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }), _jsx(Div, { ref: beforeClipRef, className: "absolute inset-0 overflow-hidden", children: _jsx(Image, { src: item.beforeImage, alt: beforeLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }) }), _jsx(Div, { ref: dividerRef, shadow: "md", className: "absolute top-0 bottom-0 z-10 w-0.5 bg-[var(--appkit-color-surface)]", children: _jsx(Row, { className: "absolute top-1/2 left-1/2 h-8 w-8 -translate-x-1/2 -translate-y-1/2 bg-card", align: "center", justify: "center", rounded: "full", shadow: "lg", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 8L1 5M4 8L1 11M4 8H12M12 8L15 5M12 8L15 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 left-3 z-10", padding: "pill-sm", rounded: "full", children: beforeLabel }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 right-3 z-10", padding: "pill-sm", rounded: "full", children: afterLabel })] }), _jsx(Div, { className: `${__P.p4}`, children: _jsx(Text, { className: "text-foreground", size: "sm", weight: "medium", children: item.caption }) })] }));
72
+ }, children: [_jsx(Image, { src: resolveMediaUrl(item.afterImage), alt: afterLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }), _jsx(Div, { ref: beforeClipRef, className: "absolute inset-0 overflow-hidden", children: _jsx(Image, { src: resolveMediaUrl(item.beforeImage), alt: beforeLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }) }), _jsx(Div, { ref: dividerRef, shadow: "md", className: "absolute top-0 bottom-0 z-10 w-0.5 bg-[var(--appkit-color-surface)]", children: _jsx(Row, { className: "absolute top-1/2 left-1/2 h-8 w-8 -translate-x-1/2 -translate-y-1/2 bg-card", align: "center", justify: "center", rounded: "full", shadow: "lg", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 8L1 5M4 8L1 11M4 8H12M12 8L15 5M12 8L15 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 left-3 z-10", padding: "pill-sm", rounded: "full", children: beforeLabel }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 right-3 z-10", padding: "pill-sm", rounded: "full", children: afterLabel })] }), _jsx(Div, { className: `${__P.p4}`, children: _jsx(Text, { className: "text-foreground", size: "sm", weight: "medium", children: item.caption }) })] }));
72
73
  }
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed";
7
7
  import { Button, Div, Heading, HorizontalScroller, Row, Section, Text } from "../../../ui";
8
8
  import { ROUTES } from "../../../next";
9
+ import { resolveMediaUrl } from "../../../utils/media-url";
9
10
  import { useTopBrands } from "../hooks/useTopBrands";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
@@ -27,7 +28,7 @@ function BrandLogo({ brand }) {
27
28
  const iconSrc = brand.display?.icon;
28
29
  const coverImage = brand.display?.coverImage;
29
30
  const initial = brand.name[0]?.toUpperCase() ?? "?";
30
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: iconSrc ?? coverImage, alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
31
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: resolveMediaUrl(iconSrc ?? coverImage), alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
31
32
  }
32
33
  export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, viewMoreHref, viewMoreLabel = "All brands →", className = "", initialItems, cta, filters, autoScroll = true, scrollInterval = 4000, loop = true, }) {
33
34
  const themed = { textPrimary: THEMED_TEXT_PRIMARY };
@@ -7,6 +7,7 @@ import { GRID_COLS_6_LG } from "../../../_internal/shared/styles/grid";
7
7
  import { useMediaQuery } from "../../../react";
8
8
  import { Button, Div, Heading, Row, Section, Span, Stack, Text } from "../../../ui";
9
9
  import { ROUTES } from "../../../next/routing/route-map";
10
+ import { resolveMediaUrl } from "../../../utils/media-url";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
12
13
  };
@@ -203,7 +204,7 @@ function HotspotHeaderOverlay({ heading, subheading, shopAllHref, }) {
203
204
  }, children: "SHOP ALL \u2192" })] }) }));
204
205
  }
205
206
  function HotspotImageLayer({ panoramicImage, panoramicAlt, }) {
206
- return (_jsxs(Div, { className: `absolute inset-0 ${__O.hidden}`, children: [_jsx(Image, { src: panoramicImage, alt: panoramicAlt, fill: true, className: "object-cover object-top", sizes: "100vw", priority: false }), _jsx(Div, { className: "absolute inset-0", style: {
207
+ return (_jsxs(Div, { className: `absolute inset-0 ${__O.hidden}`, children: [_jsx(Image, { src: resolveMediaUrl(panoramicImage), alt: panoramicAlt, fill: true, className: "object-cover object-top", sizes: "100vw", priority: false }), _jsx(Div, { className: "absolute inset-0", style: {
207
208
  background: "linear-gradient(to right, rgba(10,10,18,0.72) 0%, rgba(10,10,18,0.30) 50%, rgba(10,10,18,0.72) 100%)",
208
209
  } }), _jsx(Div, { className: "absolute inset-0", style: {
209
210
  background: "linear-gradient(to bottom, rgba(10,10,18,0.25) 0%, transparent 40%, rgba(10,10,18,0.65) 100%)",
@@ -4,6 +4,7 @@ import { normalizeError } from "../../../errors/normalize";
4
4
  import { useState, useRef } from "react";
5
5
  import Image from "next/image";
6
6
  import { Button, Details, Div, Heading, Input, Label, Li, Row, Span, Stack, Summary, Text, Textarea, Ul } from "../../../ui";
7
+ import { resolveMediaUrl } from "../../../utils/media-url";
7
8
  import { DEFAULT_ACCENT_HEX } from "../lib/franchise-colors";
8
9
  function randomId() {
9
10
  return Math.random().toString(36).slice(2, 9);
@@ -143,7 +144,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
143
144
  className: "mt-1 text-center text-[10px]", style: { color: "var(--color-muted)" }, children: s.label })] }, s.key))) }), step === "image" && (_jsxs(Div, { className: CLS_STEP, style: {
144
145
  borderColor: "var(--border-ink)",
145
146
  background: "var(--surface-elevated)",
146
- }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
147
+ }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
147
148
  borderColor: "var(--border-ink)",
148
149
  background: "var(--surface-elevated)",
149
150
  color: "var(--color-black)",
@@ -160,7 +161,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
160
161
  paddingTop: "56.25%",
161
162
  cursor: "crosshair",
162
163
  background: "black",
163
- }, onClick: handleImageClick, children: [_jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
164
+ }, onClick: handleImageClick, children: [_jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
164
165
  left: `${pin.xPct}%`,
165
166
  top: `${pin.yPct}%`,
166
167
  transform: STY_CENTERED,
@@ -289,7 +290,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
289
290
  }, children: "Save Pin & Add Another \u2192" })] })] })] })), step === "review" && (_jsxs(Div, { className: CLS_STEP, style: {
290
291
  borderColor: "var(--border-ink)",
291
292
  background: "var(--surface-elevated)",
292
- }, children: [_jsxs(Row, { justify: "between", children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Review & Save" }), _jsxs(Span, { rounded: "full", padding: "pill-md", size: "sm", weight: "bold", style: { background: "var(--surface-warm)" }, children: [pins.length, " pin", pins.length !== 1 ? "s" : ""] })] }), _jsxs(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "56.25%", background: "black" }, children: [_jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
293
+ }, children: [_jsxs(Row, { justify: "between", children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Review & Save" }), _jsxs(Span, { rounded: "full", padding: "pill-md", size: "sm", weight: "bold", style: { background: "var(--surface-warm)" }, children: [pins.length, " pin", pins.length !== 1 ? "s" : ""] })] }), _jsxs(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "56.25%", background: "black" }, children: [_jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
293
294
  left: `${pin.xPct}%`,
294
295
  top: `${pin.yPct}%`,
295
296
  transform: STY_CENTERED,
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
4
4
  import Image from "next/image";
5
5
  import Link from "next/link";
6
6
  import { Button, Div, Heading, Row, Span, Stack } from "../../../ui";
7
+ import { resolveMediaUrl } from "../../../utils/media-url";
7
8
  export function HeroBanner({ banners, autoplayMs = 5000 }) {
8
9
  const [current, setCurrent] = useState(0);
9
10
  const [animating, setAnimating] = useState(false);
@@ -38,5 +39,5 @@ export function HeroBanner({ banners, autoplayMs = 5000 }) {
38
39
  }, [banner.backgroundColor, banner.textColor]);
39
40
  if (banners.length === 0)
40
41
  return null;
41
- return (_jsxs(Div, { ref: rootRef, className: "relative overflow-hidden min-h-svh [background:var(--hero-bg,var(--dark-section-deep))]", children: [banners.map((b, i) => (_jsx(Div, { className: `absolute inset-0 transition-opacity duration-700 z-[1] ${i === current ? "opacity-100" : "opacity-0"}`, children: b.backgroundImage && (_jsx(Image, { src: b.backgroundImage, alt: b.title, fill: true, className: "object-cover object-center", priority: i === 0, sizes: "100vw" })) }, b.id))), _jsx(Div, { className: "absolute inset-0 z-[2] [background:linear-gradient(110deg,rgba(0,0,0,0.82)_0%,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.18)_100%)]" }), _jsx(Div, { className: "absolute inset-0 z-[3] [background:linear-gradient(to_bottom,rgba(0,0,0,0.08)_0%,transparent_30%,rgba(0,0,0,0.55)_100%)]" }), _jsxs(Stack, { justify: "end", className: "relative pb-[4rem] sm:px-[3rem] sm:pb-[6rem] lg:px-[5rem] min-h-svh z-[10] pt-[var(--header-height,4rem)]", padding: "x-md", children: [banner.subtitle && (_jsx(Div, { className: "mb-3", children: _jsx(Span, { className: "inline-block font-black uppercase tracking-[0.18em] bg-[var(--color-red)] text-white [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.16em]", padding: "pill-md", size: "xs", children: banner.subtitle }) })), _jsx(Heading, { level: 2, textShadow: "soft-dark", className: "mb-5 max-w-xl leading-none [font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(3rem,9vw,6.5rem)] tracking-[0.04em] text-[var(--hero-text,white)]", children: banner.title }), banner.ctaLabel && banner.ctaUrl && (_jsx(Row, { wrap: true, gap: "md", children: _jsxs(Link, { href: banner.ctaUrl, className: "inline-flex items-center gap-[var(--appkit-space-2)] sm:gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3)] sm:px-[var(--appkit-space-8)] sm:py-[var(--appkit-space-3-5)] font-black uppercase transition-all hover:-translate-y-0.5 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em] text-[clamp(0.85rem,2.5vw,1.1rem)] bg-[var(--color-yellow)] text-[var(--appkit-color-text)] [box-shadow:0_4px_24px_rgba(255,229,0,0.35)]", children: [banner.ctaLabel, _jsx(Span, { "aria-hidden": "true", className: "leading-none", smSize: "xl", size: "lg", children: "\u2192" })] }) }))] }), banners.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Button, { rounded: "full", onClick: prev, "aria-label": "Previous slide", variant: "ghost", size: "sm", className: "absolute left-3 sm:left-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { rounded: "full", onClick: next, "aria-label": "Next slide", variant: "ghost", size: "sm", className: "absolute right-3 sm:right-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] })), banners.length > 1 && (_jsxs(Row, { className: "absolute bottom-8 right-6 sm:right-12 z-[15]", gap: "3", children: [_jsxs(Span, { className: "font-black tabular-nums text-white/60 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em]", size: "xs", children: [String(current + 1).padStart(2, "0"), " /", " ", String(banners.length).padStart(2, "0")] }), _jsx(Row, { gap: "xs", className: "", children: banners.map((_, i) => (_jsx(Button, { onClick: () => goTo(i), "aria-label": `Go to slide ${i + 1}`, variant: "ghost", size: "sm", className: `transition-all h-[3px] rounded-[2px] ${i === current ? "w-7 bg-[var(--color-yellow)]" : "w-2.5 bg-white/35"}` }, i))) })] })), _jsx(Stack, { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[15] opacity-50", align: "center", gap: "xs", "aria-hidden": "true", children: _jsx(Div, { className: "h-8 w-px [background:linear-gradient(to_bottom,rgba(255,255,255,0),rgba(255,255,255,0.8))]" }) })] }));
42
+ return (_jsxs(Div, { ref: rootRef, className: "relative overflow-hidden min-h-svh [background:var(--hero-bg,var(--dark-section-deep))]", children: [banners.map((b, i) => (_jsx(Div, { className: `absolute inset-0 transition-opacity duration-700 z-[1] ${i === current ? "opacity-100" : "opacity-0"}`, children: b.backgroundImage && (_jsx(Image, { src: resolveMediaUrl(b.backgroundImage), alt: b.title, fill: true, className: "object-cover object-center", priority: i === 0, sizes: "100vw" })) }, b.id))), _jsx(Div, { className: "absolute inset-0 z-[2] [background:linear-gradient(110deg,rgba(0,0,0,0.82)_0%,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.18)_100%)]" }), _jsx(Div, { className: "absolute inset-0 z-[3] [background:linear-gradient(to_bottom,rgba(0,0,0,0.08)_0%,transparent_30%,rgba(0,0,0,0.55)_100%)]" }), _jsxs(Stack, { justify: "end", className: "relative pb-[4rem] sm:px-[3rem] sm:pb-[6rem] lg:px-[5rem] min-h-svh z-[10] pt-[var(--header-height,4rem)]", padding: "x-md", children: [banner.subtitle && (_jsx(Div, { className: "mb-3", children: _jsx(Span, { className: "inline-block font-black uppercase tracking-[0.18em] bg-[var(--color-red)] text-white [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.16em]", padding: "pill-md", size: "xs", children: banner.subtitle }) })), _jsx(Heading, { level: 2, textShadow: "soft-dark", className: "mb-5 max-w-xl leading-none [font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(3rem,9vw,6.5rem)] tracking-[0.04em] text-[var(--hero-text,white)]", children: banner.title }), banner.ctaLabel && banner.ctaUrl && (_jsx(Row, { wrap: true, gap: "md", children: _jsxs(Link, { href: banner.ctaUrl, className: "inline-flex items-center gap-[var(--appkit-space-2)] sm:gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3)] sm:px-[var(--appkit-space-8)] sm:py-[var(--appkit-space-3-5)] font-black uppercase transition-all hover:-translate-y-0.5 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em] text-[clamp(0.85rem,2.5vw,1.1rem)] bg-[var(--color-yellow)] text-[var(--appkit-color-text)] [box-shadow:0_4px_24px_rgba(255,229,0,0.35)]", children: [banner.ctaLabel, _jsx(Span, { "aria-hidden": "true", className: "leading-none", smSize: "xl", size: "lg", children: "\u2192" })] }) }))] }), banners.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Button, { rounded: "full", onClick: prev, "aria-label": "Previous slide", variant: "ghost", size: "sm", className: "absolute left-3 sm:left-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { rounded: "full", onClick: next, "aria-label": "Next slide", variant: "ghost", size: "sm", className: "absolute right-3 sm:right-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] })), banners.length > 1 && (_jsxs(Row, { className: "absolute bottom-8 right-6 sm:right-12 z-[15]", gap: "3", children: [_jsxs(Span, { className: "font-black tabular-nums text-white/60 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em]", size: "xs", children: [String(current + 1).padStart(2, "0"), " /", " ", String(banners.length).padStart(2, "0")] }), _jsx(Row, { gap: "xs", className: "", children: banners.map((_, i) => (_jsx(Button, { onClick: () => goTo(i), "aria-label": `Go to slide ${i + 1}`, variant: "ghost", size: "sm", className: `transition-all h-[3px] rounded-[2px] ${i === current ? "w-7 bg-[var(--color-yellow)]" : "w-2.5 bg-white/35"}` }, i))) })] })), _jsx(Stack, { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[15] opacity-50", align: "center", gap: "xs", "aria-hidden": "true", children: _jsx(Div, { className: "h-8 w-px [background:linear-gradient(to_bottom,rgba(255,255,255,0),rgba(255,255,255,0.8))]" }) })] }));
42
43
  }
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Image from "next/image";
3
3
  import Link from "next/link";
4
4
  import { Div, Heading, Row, Section, Span, Text } from "../../../ui";
5
+ import { resolveMediaUrl } from "../../../utils/media-url";
5
6
  const __P = {
6
7
  p4: "p-[var(--appkit-space-4)]",
7
8
  };
8
9
  export function PromoGrid({ banners, eyebrow = "LIMITED TIME", heading = "HOT DEALS & PROMOS", }) {
9
10
  if (banners.length === 0)
10
11
  return null;
11
- return (_jsx(Section, { paddingY: "y-4xl", className: "bg-[var(--dark-section-alt)] [border-top:1px_solid_rgba(255,255,255,0.06)] [border-bottom:1px_solid_rgba(255,255,255,0.06)]", children: _jsxs(Div, { className: "mx-auto max-w-7xl", padding: "x-md", children: [_jsx(Row, { wrap: true, align: "end", justify: "between", gap: "sm", className: "mb-7 gap-y-2", children: _jsxs(Div, { children: [_jsx(Text, { className: "mb-1 font-black uppercase tracking-widest text-[var(--color-red)] tracking-[0.18em]", size: "xs", children: eyebrow }), _jsx(Heading, { level: 2, className: "[font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(1.6rem,4vw,2.4rem)] tracking-[0.08em] text-white leading-none", children: heading })] }) }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: banners.slice(0, 4).map((banner, i) => (_jsxs(Link, { href: banner.ctaUrl, className: `group relative block overflow-hidden bg-[var(--dark-section-card)] ${i === 0 ? "sm:row-span-2 min-h-[clamp(220px,32vh,480px)]" : "min-h-[clamp(130px,16vh,240px)]"}`, children: [_jsx(Image, { src: banner.image, alt: banner.title, fill: true, className: "object-cover transition-transform duration-500 group-hover:scale-105", sizes: i === 0
12
+ return (_jsx(Section, { paddingY: "y-4xl", className: "bg-[var(--dark-section-alt)] [border-top:1px_solid_rgba(255,255,255,0.06)] [border-bottom:1px_solid_rgba(255,255,255,0.06)]", children: _jsxs(Div, { className: "mx-auto max-w-7xl", padding: "x-md", children: [_jsx(Row, { wrap: true, align: "end", justify: "between", gap: "sm", className: "mb-7 gap-y-2", children: _jsxs(Div, { children: [_jsx(Text, { className: "mb-1 font-black uppercase tracking-widest text-[var(--color-red)] tracking-[0.18em]", size: "xs", children: eyebrow }), _jsx(Heading, { level: 2, className: "[font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(1.6rem,4vw,2.4rem)] tracking-[0.08em] text-white leading-none", children: heading })] }) }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: banners.slice(0, 4).map((banner, i) => (_jsxs(Link, { href: banner.ctaUrl, className: `group relative block overflow-hidden bg-[var(--dark-section-card)] ${i === 0 ? "sm:row-span-2 min-h-[clamp(220px,32vh,480px)]" : "min-h-[clamp(130px,16vh,240px)]"}`, children: [_jsx(Image, { src: resolveMediaUrl(banner.image), alt: banner.title, fill: true, className: "object-cover transition-transform duration-500 group-hover:scale-105", sizes: i === 0
12
13
  ? "(max-width: 640px) 100vw, 40vw"
13
14
  : "(max-width: 640px) 50vw, 30vw" }), _jsx(Div, { className: `absolute inset-0 ${i === 0 ? "[background:linear-gradient(to_top,rgba(0,0,0,0.9)_0%,rgba(0,0,0,0.3)_50%,transparent_100%)]" : "[background:linear-gradient(to_top,rgba(0,0,0,0.85)_0%,rgba(0,0,0,0.15)_100%)]"}` }), _jsx(Div, { className: "absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100 ring-2 ring-[var(--color-yellow)] ring-inset" }), _jsxs(Div, { className: `absolute bottom-0 left-0 right-0 ${__P.p4}`, children: [_jsx(Text, { className: `leading-tight [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.06em] text-white ${i === 0 ? "text-[1.35rem]" : "text-[1rem]"}`, children: banner.title }), _jsxs(Span, { layout: "inline-flex", gap: "xs", className: "mt-1 font-black uppercase text-[var(--color-yellow)]", size: "xs", children: [banner.ctaLabel, " \u2192"] })] })] }, banner.id))) })] }) }));
14
15
  }
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed";
7
7
  import { Button, Div, Heading, HorizontalScroller, Row, Section, Span, Stack, Text } from "../../../ui";
8
8
  import { ROUTES } from "../../../next";
9
+ import { resolveMediaUrl } from "../../../utils/media-url";
9
10
  import { useTopCategories } from "../hooks/useTopCategories";
10
11
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
11
12
  const __P = {
@@ -22,7 +23,7 @@ function CategoryChip({ category }) {
22
23
  const coverImage = category.display?.coverImage;
23
24
  const initial = category.name[0]?.toUpperCase() ?? "?";
24
25
  const productCount = category.metrics?.productCount ?? 0;
25
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: coverImage, alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: iconSrc, alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
26
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: resolveMediaUrl(coverImage), alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: resolveMediaUrl(iconSrc), alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
26
27
  }
27
28
  const CTA_CLASSES = {
28
29
  filled: "rounded-lg bg-[var(--appkit-color-primary)] px-[var(--appkit-space-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white hover:opacity-90",
@@ -3,10 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useEffect, useState, useCallback } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import Link from "next/link";
6
- import { Button, Div, IconButton, Li, Nav, Row, Span, Stack, Text, Ul } from "../../../ui";
6
+ import { Button, Div, IconButton, Input, Li, Nav, Row, Span, Stack, Text, Ul } from "../../../ui";
7
7
  import { MediaImage } from "../../media/MediaImage";
8
8
  import { BottomSheet } from "../../layout/BottomSheet";
9
9
  import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
10
+ import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
12
13
  yAuto: "overflow-y-auto",
@@ -37,14 +38,19 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
37
38
  const match = groups.find((g) => g.defaultOpen ?? g.items.some((i) => activeHref === i.href));
38
39
  return match?.title ?? null;
39
40
  });
40
- const toggle = useCallback((title) => setOpenGroup((p) => (p === title ? null : title)), []);
41
- return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsx(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: groups.map((group) => {
42
- const isOpen = openGroup === group.title;
43
- const hasActive = group.items.some((i) => activeHref === i.href);
44
- return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
45
- ? CLS_NAV_ICON_ACTIVE
46
- : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isNavItemActive(item, activeHref), onClick: onItemClick }) }, item.href))) }))] }, group.title));
47
- }) })] }));
41
+ const { query, setQuery, isSearching, filteredGroups } = useSidebarSearch(groups);
42
+ const toggle = useCallback((title) => {
43
+ if (isSearching)
44
+ return;
45
+ setOpenGroup((p) => (p === title ? null : title));
46
+ }, [isSearching]);
47
+ return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsxs(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: [_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search navigation\u2026", "aria-label": "Search navigation" }) }), isSearching && filteredGroups.length === 0 && (_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsxs(Span, { size: "xs", color: "muted", children: ["No matches for \u201C", query, "\u201D."] }) })), filteredGroups.map((group) => {
48
+ const isOpen = isSearching || openGroup === group.title;
49
+ const hasActive = group.items.some((i) => activeHref === i.href);
50
+ return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
51
+ ? CLS_NAV_ICON_ACTIVE
52
+ : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isNavItemActive(item, activeHref), onClick: onItemClick }) }, item.href))) }))] }, group.title));
53
+ })] })] }));
48
54
  }
49
55
  function DrawerPanel({ title, onClose, children }) {
50
56
  return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: "fixed top-0 right-0 z-50 h-full w-64 border-l", surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
@@ -10,6 +10,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
10
10
  href: z.ZodOptional<z.ZodString>;
11
11
  order: z.ZodDefault<z.ZodNumber>;
12
12
  isActive: z.ZodDefault<z.ZodBoolean>;
13
+ adminOnly: z.ZodDefault<z.ZodBoolean>;
13
14
  }, "strip", z.ZodTypeAny, {
14
15
  label: string;
15
16
  order: number;
@@ -18,6 +19,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
18
19
  pageKey: string;
19
20
  groupLabel: string;
20
21
  pageLabel: string;
22
+ adminOnly: boolean;
21
23
  href?: string | undefined;
22
24
  description?: string | undefined;
23
25
  }, {
@@ -30,6 +32,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
30
32
  description?: string | undefined;
31
33
  order?: number | undefined;
32
34
  isActive?: boolean | undefined;
35
+ adminOnly?: boolean | undefined;
33
36
  }>;
34
37
  export declare const checklistItemUpdateSchema: z.ZodObject<{
35
38
  groupKey: z.ZodOptional<z.ZodString>;
@@ -41,6 +44,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
41
44
  href: z.ZodOptional<z.ZodOptional<z.ZodString>>;
42
45
  order: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
43
46
  isActive: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
47
+ adminOnly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
44
48
  }, "strip", z.ZodTypeAny, {
45
49
  label?: string | undefined;
46
50
  href?: string | undefined;
@@ -51,6 +55,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
51
55
  pageKey?: string | undefined;
52
56
  groupLabel?: string | undefined;
53
57
  pageLabel?: string | undefined;
58
+ adminOnly?: boolean | undefined;
54
59
  }, {
55
60
  label?: string | undefined;
56
61
  href?: string | undefined;
@@ -61,6 +66,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
61
66
  pageKey?: string | undefined;
62
67
  groupLabel?: string | undefined;
63
68
  pageLabel?: string | undefined;
69
+ adminOnly?: boolean | undefined;
64
70
  }>;
65
71
  export type ChecklistItemCreateInput = z.infer<typeof checklistItemCreateSchema>;
66
72
  export type ChecklistItemUpdateInput = z.infer<typeof checklistItemUpdateSchema>;
@@ -12,6 +12,7 @@ const checklistItemBaseSchema = z.object({
12
12
  href: z.string().max(300).optional(),
13
13
  order: z.number().int().min(0).default(0),
14
14
  isActive: z.boolean().default(true),
15
+ adminOnly: z.boolean().default(false),
15
16
  });
16
17
  export const checklistItemCreateSchema = checklistItemBaseSchema;
17
18
  export const checklistItemUpdateSchema = checklistItemBaseSchema.partial();
@@ -27,6 +28,7 @@ export async function createChecklistItem(input) {
27
28
  href: input.href,
28
29
  order: input.order ?? 0,
29
30
  isActive: input.isActive ?? true,
31
+ adminOnly: input.adminOnly ?? false,
30
32
  });
31
33
  serverLogger.info("createChecklistItem", { itemId: item.id });
32
34
  return item;
@@ -7,11 +7,12 @@ import { apiClient } from "../../../http";
7
7
  import { useApiMutation } from "../../../client";
8
8
  import { useSession } from "../../../react";
9
9
  import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
10
+ import { isAdminUser } from "../../auth/role-predicates";
10
11
  import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
11
12
  function matchesQuery(item, query) {
12
13
  if (!query)
13
14
  return true;
14
- const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel}`.toLowerCase();
15
+ const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel} ${item.href ?? ""}`.toLowerCase();
15
16
  return haystack.includes(query.toLowerCase());
16
17
  }
17
18
  export function TesterHubView({ sandboxExpiresAt }) {
@@ -31,8 +32,8 @@ export function TesterHubView({ sandboxExpiresAt }) {
31
32
  void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
32
33
  },
33
34
  });
34
- if (!user?.isTester) {
35
- return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers. Contact an admin if you believe this is a mistake." }));
35
+ if (!user?.isTester && !isAdminUser(user)) {
36
+ return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers (and admins). Contact an admin if you believe this is a mistake." }));
36
37
  }
37
38
  const items = query.data?.items ?? [];
38
39
  const filtered = items.filter((item) => matchesQuery(item, search));
@@ -53,7 +54,7 @@ export function TesterHubView({ sandboxExpiresAt }) {
53
54
  const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
54
55
  await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
55
56
  };
56
- return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "e.g. checkout, google, bidding..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
57
+ return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
57
58
  const answeredCount = page.items.filter((i) => i.answer).length;
58
59
  return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { children: [page.pageLabel, " (", answeredCount, "/", page.items.length, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: page.items
59
60
  .slice()
@@ -17,13 +17,14 @@ export interface TesterChecklistItemDocument extends BaseDocument {
17
17
  order: number;
18
18
  isActive: boolean;
19
19
  searchTokens: string[];
20
+ adminOnly?: boolean;
20
21
  }
21
22
  export declare const TESTER_CHECKLIST_ITEM_COLLECTION: "testerChecklistItems";
22
23
  export declare const TESTER_CHECKLIST_ITEM_ID_PREFIX: "checklist-";
23
24
  export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "order", "isActive", "searchTokens", "createdAt"];
24
25
  export declare const DEFAULT_TESTER_CHECKLIST_ITEM_DATA: Partial<TesterChecklistItemDocument>;
25
26
  export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "searchTokens">;
26
- export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "isActive">>;
27
+ export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "isActive" | "adminOnly">>;
27
28
  export declare const testerChecklistItemQueryHelpers: {
28
29
  readonly byGroup: (groupKey: string) => readonly ["groupKey", "==", string];
29
30
  readonly byPage: (pageKey: string) => readonly ["pageKey", "==", string];
@@ -66,6 +67,7 @@ export declare const TESTER_CHECKLIST_ITEM_FIELDS: {
66
67
  readonly ORDER: "order";
67
68
  readonly IS_ACTIVE: "isActive";
68
69
  readonly SEARCH_TOKENS: "searchTokens";
70
+ readonly ADMIN_ONLY: "adminOnly";
69
71
  readonly CREATED_AT: "createdAt";
70
72
  readonly UPDATED_AT: "updatedAt";
71
73
  };
@@ -20,6 +20,7 @@ export const DEFAULT_TESTER_CHECKLIST_ITEM_DATA = {
20
20
  order: 0,
21
21
  isActive: true,
22
22
  searchTokens: [],
23
+ adminOnly: false,
23
24
  };
24
25
  export const testerChecklistItemQueryHelpers = {
25
26
  byGroup: (groupKey) => ["groupKey", "==", groupKey],
@@ -74,6 +75,7 @@ export const TESTER_CHECKLIST_ITEM_FIELDS = {
74
75
  ORDER: "order",
75
76
  IS_ACTIVE: "isActive",
76
77
  SEARCH_TOKENS: "searchTokens",
78
+ ADMIN_ONLY: "adminOnly",
77
79
  CREATED_AT: "createdAt",
78
80
  UPDATED_AT: "updatedAt",
79
81
  };
@@ -0,0 +1,2 @@
1
+ import type { BidDocument } from "../../auctions/schemas/firestore";
2
+ export declare const bidsTesterSeedData: Partial<BidDocument>[];
@@ -0,0 +1,37 @@
1
+ /*
2
+ * WHY: Shared tester sandbox — one winning bid on the already-ended auction-tester-sandbox-won
3
+ * (kept separate from the still-biddable auction-tester-sandbox-1 so testers can still
4
+ * place a live bid on the latter) so testers (and the paired orders-tester-seed-data.ts
5
+ * auction-win order) can exercise the "winning an auction creates a payable order" flow.
6
+ * Auto-expires in 7 days, cascading with the parent auction product.
7
+ * WHAT: Exports bidsTesterSeedData — 1 Partial<BidDocument> for the seed runner.
8
+ *
9
+ * EXPORTS:
10
+ * bidsTesterSeedData — Array of 1 Partial<BidDocument> for the seed runner
11
+ *
12
+ * @tag domain:auctions,bids,tester
13
+ * @tag layer:seed
14
+ * @tag pattern:none
15
+ * @tag access:server-only
16
+ * @tag consumers:seed/index.ts,seed-cli.mjs
17
+ * @tag sideEffects:none
18
+ */
19
+ const NOW = new Date();
20
+ const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
21
+ export const bidsTesterSeedData = [
22
+ {
23
+ id: "bid-tester-sandbox-won-tester-qa",
24
+ productId: "auction-tester-sandbox-won",
25
+ productTitle: "Test Auction — Already Won",
26
+ userId: "user-tester-qa",
27
+ userName: "QA Tester",
28
+ userEmail: "tester@letitrip.in",
29
+ bidAmount: 15000,
30
+ currency: "INR",
31
+ status: "won",
32
+ isWinning: true,
33
+ bidDate: daysAgo(1),
34
+ createdAt: daysAgo(1),
35
+ updatedAt: daysAgo(1),
36
+ },
37
+ ];
@@ -0,0 +1,2 @@
1
+ import type { CouponDocument } from "../../promotions/schemas";
2
+ export declare const couponsTesterSeedData: Partial<CouponDocument>[];
@@ -0,0 +1,76 @@
1
+ /*
2
+ * WHY: Shared tester sandbox — 3 disposable test coupons so testers can exercise
3
+ * coupon apply/reject edge cases end to end: a perUserLimit:1 admin coupon (to
4
+ * verify the second-use rejection), an already-expired admin coupon (to verify
5
+ * the expiry rejection), and a seller-scope coupon on store-tester-sandbox.
6
+ * NOTE: CouponDocument does not currently carry isTestData/testDataExpiresAt
7
+ * (unlike categories/stores/products/blogPosts/events) — testerSandboxCleanup
8
+ * does not cascade-delete these. They're small, static, and harmless to leave;
9
+ * extending the cleanup script to cover coupons is a follow-up, not blocking.
10
+ * WHAT: Exports couponsTesterSeedData — 3 Partial<CouponDocument> for the seed runner.
11
+ *
12
+ * EXPORTS:
13
+ * couponsTesterSeedData — Array of 3 Partial<CouponDocument> for the seed runner
14
+ *
15
+ * @tag domain:coupons,tester
16
+ * @tag layer:seed
17
+ * @tag pattern:none
18
+ * @tag access:server-only
19
+ * @tag consumers:seed/index.ts,seed-cli.mjs
20
+ * @tag sideEffects:none
21
+ */
22
+ import { COUPON_FIELDS } from "../../../constants/field-names";
23
+ const NOW = new Date();
24
+ const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
25
+ const daysAhead = (n) => new Date(NOW.getTime() + n * 86400000);
26
+ export const couponsTesterSeedData = [
27
+ {
28
+ id: "coupon-tester-sandbox-limited",
29
+ code: "TESTERLIMITED",
30
+ name: "Tester Sandbox — One Use Per Tester",
31
+ description: "Disposable test coupon: 10% off, capped at one use per tester. Use it twice to verify the second attempt is rejected.",
32
+ type: COUPON_FIELDS.TYPE_VALUES.PERCENTAGE,
33
+ scope: COUPON_FIELDS.SCOPE_VALUES.ADMIN,
34
+ discount: { value: 10, maxDiscount: 100, minPurchase: 100 },
35
+ usage: { totalLimit: undefined, perUserLimit: 1, currentUsage: 0 },
36
+ validity: { startDate: daysAgo(1), endDate: daysAhead(7), isActive: true },
37
+ restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: true },
38
+ createdBy: "user-admin-letitrip",
39
+ createdAt: daysAgo(1),
40
+ updatedAt: daysAgo(1),
41
+ stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
42
+ },
43
+ {
44
+ id: "coupon-tester-sandbox-expired",
45
+ code: "TESTEREXPIRED",
46
+ name: "Tester Sandbox — Already Expired",
47
+ description: "Disposable test coupon that already expired — applying it at checkout should show a specific expiry rejection.",
48
+ type: COUPON_FIELDS.TYPE_VALUES.FIXED,
49
+ scope: COUPON_FIELDS.SCOPE_VALUES.ADMIN,
50
+ discount: { value: 50, maxDiscount: 50, minPurchase: 100 },
51
+ usage: { totalLimit: undefined, perUserLimit: 5, currentUsage: 0 },
52
+ validity: { startDate: daysAgo(30), endDate: daysAgo(1), isActive: true },
53
+ restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: true },
54
+ createdBy: "user-admin-letitrip",
55
+ createdAt: daysAgo(30),
56
+ updatedAt: daysAgo(30),
57
+ stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
58
+ },
59
+ {
60
+ id: "coupon-tester-sandbox-seller",
61
+ code: "TESTERSTORE",
62
+ name: "Tester Sandbox Store — 5% Off",
63
+ description: "Disposable seller-scope test coupon on the tester sandbox store.",
64
+ type: COUPON_FIELDS.TYPE_VALUES.PERCENTAGE,
65
+ scope: COUPON_FIELDS.SCOPE_VALUES.SELLER,
66
+ storeId: "store-tester-sandbox",
67
+ discount: { value: 5, maxDiscount: 50, minPurchase: 0 },
68
+ usage: { totalLimit: undefined, perUserLimit: 3, currentUsage: 0 },
69
+ validity: { startDate: daysAgo(1), endDate: daysAhead(7), isActive: true },
70
+ restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: true },
71
+ createdBy: "user-admin-letitrip",
72
+ createdAt: daysAgo(1),
73
+ updatedAt: daysAgo(1),
74
+ stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
75
+ },
76
+ ];
@@ -17,6 +17,8 @@
17
17
  import { EVENT_FIELDS } from "../../../constants/field-names";
18
18
  import { seedExtMedia } from "../../../seed/_helpers/media";
19
19
  import { testDataExpiresAt } from "./tester-ttl";
20
+ const TEST_EVENT_COVER_ALT = "Test event cover";
21
+ const TEST_RAFFLE_PRIZE = "Test prize — not a real reward";
20
22
  export const eventsTesterSeedData = [
21
23
  {
22
24
  id: "event-tester-sandbox-spin",
@@ -27,11 +29,11 @@ export const eventsTesterSeedData = [
27
29
  status: EVENT_FIELDS.STATUS_VALUES.ACTIVE,
28
30
  startsAt: new Date(),
29
31
  endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
30
- coverImage: { type: "image", url: seedExtMedia("https://picsum.photos/seed/event-cover-tester-sandbox-spin-20260101/1200/600"), alt: "Test event cover" },
32
+ coverImage: { type: "image", url: seedExtMedia("https://picsum.photos/seed/event-cover-tester-sandbox-spin-20260101/1200/600"), alt: TEST_EVENT_COVER_ALT },
31
33
  tags: ["test"],
32
34
  hasRaffle: true,
33
35
  raffleType: "open_raffle",
34
- rafflePrize: "Test prize — not a real reward",
36
+ rafflePrize: TEST_RAFFLE_PRIZE,
35
37
  raffleEntryCount: 0,
36
38
  spinPrizes: [
37
39
  { id: "spin-tester-win", label: "You Win!", weight: 50, isActive: true },
@@ -47,4 +49,50 @@ export const eventsTesterSeedData = [
47
49
  createdAt: new Date(),
48
50
  updatedAt: new Date(),
49
51
  },
52
+ {
53
+ id: "event-tester-sandbox-top-scorers",
54
+ slug: "tester-sandbox-top-scorers",
55
+ type: EVENT_FIELDS.TYPE_VALUES.RAFFLE,
56
+ title: "Tester Sandbox — Top Scorers Raffle",
57
+ description: "Disposable test event for the tester QA program. Verify top_n_scorers raffle entry and the leaderboard.",
58
+ status: EVENT_FIELDS.STATUS_VALUES.ACTIVE,
59
+ startsAt: new Date(),
60
+ endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
61
+ coverImage: { type: "image", url: seedExtMedia("https://picsum.photos/seed/event-cover-tester-sandbox-top-scorers-20260101/1200/600"), alt: TEST_EVENT_COVER_ALT },
62
+ tags: ["test"],
63
+ hasRaffle: true,
64
+ raffleType: "top_n_scorers",
65
+ raffleTopN: 3,
66
+ rafflePrize: TEST_RAFFLE_PRIZE,
67
+ raffleEntryCount: 0,
68
+ stats: { totalEntries: 0, approvedEntries: 0, flaggedEntries: 0 },
69
+ createdBy: "user-admin-letitrip",
70
+ isTestData: true,
71
+ testDataExpiresAt: testDataExpiresAt(),
72
+ createdAt: new Date(),
73
+ updatedAt: new Date(),
74
+ },
75
+ {
76
+ id: "event-tester-sandbox-top-participants",
77
+ slug: "tester-sandbox-top-participants",
78
+ type: EVENT_FIELDS.TYPE_VALUES.RAFFLE,
79
+ title: "Tester Sandbox — Top Participants Raffle",
80
+ description: "Disposable test event for the tester QA program. Verify top_n_participants raffle entry.",
81
+ status: EVENT_FIELDS.STATUS_VALUES.ACTIVE,
82
+ startsAt: new Date(),
83
+ endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
84
+ coverImage: { type: "image", url: seedExtMedia("https://picsum.photos/seed/event-cover-tester-sandbox-top-participants-20260101/1200/600"), alt: TEST_EVENT_COVER_ALT },
85
+ tags: ["test"],
86
+ hasRaffle: true,
87
+ raffleType: "top_n_participants",
88
+ raffleTopN: 5,
89
+ rafflePrize: TEST_RAFFLE_PRIZE,
90
+ raffleEntryCount: 0,
91
+ stats: { totalEntries: 0, approvedEntries: 0, flaggedEntries: 0 },
92
+ createdBy: "user-admin-letitrip",
93
+ isTestData: true,
94
+ testDataExpiresAt: testDataExpiresAt(),
95
+ createdAt: new Date(),
96
+ updatedAt: new Date(),
97
+ },
50
98
  ];