@mohasinac/appkit 2.6.7 → 2.6.9

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 (43) hide show
  1. package/dist/_internal/server/jobs/core/onSupportTicketCreate.d.ts +10 -0
  2. package/dist/_internal/server/jobs/core/onSupportTicketCreate.js +26 -0
  3. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.d.ts +15 -0
  4. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.js +50 -0
  5. package/dist/_internal/server/jobs/core/onUserBanChange.d.ts +17 -0
  6. package/dist/_internal/server/jobs/core/onUserBanChange.js +59 -0
  7. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -0
  8. package/dist/_internal/server/jobs/handlers/index.js +4 -0
  9. package/dist/_internal/server/jobs/handlers/onSupportTicketCreate.d.ts +2 -0
  10. package/dist/_internal/server/jobs/handlers/onSupportTicketCreate.js +7 -0
  11. package/dist/_internal/server/jobs/handlers/onSupportTicketUpdate.d.ts +2 -0
  12. package/dist/_internal/server/jobs/handlers/onSupportTicketUpdate.js +8 -0
  13. package/dist/_internal/server/jobs/handlers/onUserBanChange.d.ts +2 -0
  14. package/dist/_internal/server/jobs/handlers/onUserBanChange.js +10 -0
  15. package/dist/configs/next.js +14 -0
  16. package/dist/features/admin/components/AdminScammerEditorView.d.ts +15 -0
  17. package/dist/features/admin/components/AdminScammerEditorView.js +61 -0
  18. package/dist/features/admin/components/AdminScammersView.d.ts +4 -0
  19. package/dist/features/admin/components/AdminScammersView.js +147 -0
  20. package/dist/features/admin/components/index.d.ts +4 -0
  21. package/dist/features/admin/components/index.js +2 -0
  22. package/dist/features/admin/constants/filter-tabs.d.ts +17 -0
  23. package/dist/features/admin/constants/filter-tabs.js +8 -0
  24. package/dist/features/faq/schemas/firestore.js +1 -0
  25. package/dist/features/faq/types/index.d.ts +1 -1
  26. package/dist/features/layout/AppLayoutShell.d.ts +2 -1
  27. package/dist/features/layout/AppLayoutShell.js +10 -4
  28. package/dist/features/layout/TitleBarLayout.d.ts +3 -1
  29. package/dist/features/layout/TitleBarLayout.js +8 -4
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/jobs.d.ts +1 -1
  33. package/dist/jobs.js +2 -0
  34. package/dist/next/routing/route-map.d.ts +14 -0
  35. package/dist/next/routing/route-map.js +7 -0
  36. package/dist/seed/faq-seed-data.js +303 -0
  37. package/dist/seed/users-seed-data.js +81 -3
  38. package/dist/tailwind-utilities.css +1 -1
  39. package/dist/ui/components/Badge.d.ts +1 -0
  40. package/dist/ui/components/Badge.js +1 -0
  41. package/dist/ui/components/Badge.style.css +11 -0
  42. package/dist/ui/components/RoleBadge.js +2 -0
  43. package/package.json +1 -1
@@ -2,7 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
- import { Main, Div, Text, TextLink, Ul, Li, AvatarDisplay, RoleBadge, BackgroundRenderer, UnsavedChangesModal, } from "../../ui";
5
+ import { Main, Div, Text, TextLink, Ul, Li, AvatarDisplay, BackgroundRenderer, UnsavedChangesModal, } from "../../ui";
6
+ const ROLE_DOT_COLORS = {
7
+ admin: "#9333ea",
8
+ moderator: "#0ea5e9",
9
+ seller: "#0d9488",
10
+ employee: "#f59e0b",
11
+ };
6
12
  import { useTheme } from "../../react";
7
13
  import { useBottomActionsContext } from "./BottomActionsContext";
8
14
  import BottomActions from "./BottomActions";
@@ -37,7 +43,7 @@ function CollapsibleSidebarSection({ section, navItemClass, }) {
37
43
  }
38
44
  return (_jsxs(Div, { className: "space-y-0.5", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center justify-between px-1 py-1 text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors", children: [_jsx("span", { children: section.title }), _jsx("svg", { className: `w-3.5 h-3.5 transition-transform duration-200 ${open ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }), open && (_jsx(Ul, { className: "space-y-0.5", children: section.items.map((item) => (_jsx(Li, { children: _jsxs(TextLink, { href: item.href, variant: "none", className: navItemClass, children: [item.icon && (_jsx("span", { className: "flex-shrink-0 w-5 text-center", "aria-hidden": "true", children: item.icon })), item.label] }) }, `${item.href}-${item.label}`))) }))] }));
39
45
  }
40
- export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
46
+ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
41
47
  const [queryClient] = useState(() => new QueryClient());
42
48
  const [sidebarOpen, setSidebarOpen] = useState(false);
43
49
  const [searchOpen, setSearchOpen] = useState(false);
@@ -148,7 +154,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
148
154
  opacity: darkBackground.overlay?.opacity ?? 0,
149
155
  },
150
156
  };
151
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems: navItems, hiddenNavItems: hiddenNavItems }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsxs(Div, { className: "flex items-center justify-between gap-3", children: [_jsxs(Div, { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsxs(Div, { className: "flex-shrink-0 relative", children: [_jsx(AvatarDisplay, { cropData: user.avatarMetadata
157
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems: navItems, hiddenNavItems: hiddenNavItems }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsxs(Div, { className: "flex items-center justify-between gap-3", children: [_jsxs(Div, { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsxs(Div, { className: "flex-shrink-0 relative", children: [_jsx(AvatarDisplay, { cropData: user.avatarMetadata
152
158
  ? {
153
159
  url: user.avatarMetadata.url,
154
160
  position: user.avatarMetadata.position ?? {
@@ -163,5 +169,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
163
169
  position: { x: 50, y: 50 },
164
170
  zoom: 1,
165
171
  }
166
- : null, size: "md", alt: user.displayName || "User", displayName: user.displayName, email: user.email }), user.role && (_jsx(Div, { className: "absolute -bottom-1 -right-1", children: _jsx(RoleBadge, { role: user.role }) }))] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate", children: user.displayName || "User" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 truncate", children: user.email || "" })] })] }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: () => setSidebarOpen(false), className: "flex-shrink-0 rounded-full p-2 text-zinc-600 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _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" }) }) })] })) : (_jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Div, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: () => setSidebarOpen(false), className: "rounded-full p-2 text-zinc-600 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _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" }) }) })] })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: "container mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 lg:px-8", children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
172
+ : null, size: "md", alt: user.displayName || "User", displayName: user.displayName, email: user.email }), user.role && user.role !== "user" && (_jsx(Div, { className: "absolute -bottom-0.5 -right-0.5 flex items-center justify-center w-4 h-4 rounded-full border-2 border-white dark:border-slate-900 text-white text-[9px] font-bold leading-none select-none", style: { background: ROLE_DOT_COLORS[user.role] ?? "#6b7280" }, title: user.role.charAt(0).toUpperCase() + user.role.slice(1), "aria-label": user.role, children: user.role.charAt(0).toUpperCase() }))] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate", children: user.displayName || "User" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 truncate", children: user.email || "" })] })] }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: () => setSidebarOpen(false), className: "flex-shrink-0 rounded-full p-2 text-zinc-600 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _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" }) }) })] })) : (_jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Div, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: () => setSidebarOpen(false), className: "rounded-full p-2 text-zinc-600 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _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" }) }) })] })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: "container mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 lg:px-8", children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
167
173
  }
@@ -25,6 +25,8 @@ export interface TitleBarLayoutProps {
25
25
  cartHref?: string;
26
26
  cartCount?: number;
27
27
  profileHref?: string;
28
+ loginHref?: string;
29
+ registerHref?: string;
28
30
  user?: TitleBarUser | null;
29
31
  /** Slot rendered beside the profile link (e.g. NotificationBell). */
30
32
  notificationSlot?: React.ReactNode;
@@ -54,4 +56,4 @@ export interface TitleBarLayoutProps {
54
56
  *
55
57
  * Receives all domain data as props — zero domain imports.
56
58
  */
57
- export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
59
+ export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import Image from "next/image";
3
2
  import Link from "next/link";
4
3
  import { BlockHeader, Button, Div, Row, SiteLogo, Span } from "../../ui";
5
4
  /** Shared icon-button class for all action buttons in the title bar. */
@@ -15,7 +14,7 @@ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-cente
15
14
  *
16
15
  * Receives all domain data as props — zero domain imports.
17
16
  */
18
- export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
17
+ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
19
18
  // ── Element builders ────────────────────────────────────────────────────────
20
19
  const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-1 px-3 py-1 rounded-full text-xs font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-200 dark:hover:bg-secondary-900/60 transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
21
20
  const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
@@ -25,8 +24,13 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
25
24
  const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", onClick: onSearchToggle, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
26
25
  const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4.318 6.318a4.5 4.5 0 0 0 0 6.364L12 20.364l7.682-7.682a4.5 4.5 0 0 0-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 0 0-6.364 0z" }) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
27
26
  const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
28
- const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: iconBtn, children: user?.photoURL ? (_jsx(Image, { src: user.photoURL, alt: user.displayName ?? "Profile", width: 28, height: 28, className: "w-7 h-7 rounded-full object-cover" })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0zM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632z" }) })) })) : null;
27
+ const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: iconBtn, children: user?.photoURL ? (
28
+ // eslint-disable-next-line @next/next/no-img-element
29
+ _jsx("img", { src: user.photoURL, alt: user.displayName ?? "Profile", width: 28, height: 28, className: "w-7 h-7 rounded-full object-cover" })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0zM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632z" }) })) })) : null;
30
+ const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex items-center", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 dark:hover:bg-slate-800", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
29
31
  const hasTb2 = !!(wishlistEl || cartEl || profileEl);
30
32
  // ── Render ───────────────────────────────────────────────────────────────────
31
- return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, src: siteLogoUrl, className: "h-7 md:h-9 lg:h-10" }) }) }), navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot }), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl }), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t border-zinc-100 dark:border-slate-800 px-1", children: [wishlistEl, cartEl, profileEl] }))] })] }));
33
+ return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, src: siteLogoUrl, className: "h-7 md:h-9 lg:h-10" }) }) }), navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot }), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
34
+ ? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
35
+ : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t border-zinc-100 dark:border-slate-800 px-1", children: [wishlistEl, cartEl, profileEl] }))] })] }));
32
36
  }
package/dist/index.d.ts CHANGED
@@ -1219,6 +1219,10 @@ export type { AdminTeamViewProps } from "./features/admin/index";
1219
1219
  export type { AdminEmployeeEditorViewProps } from "./features/admin/index";
1220
1220
  export type { AdminSupportTicketsViewProps } from "./features/admin/index";
1221
1221
  export type { AdminSupportTicketDetailViewProps } from "./features/admin/index";
1222
+ export { AdminScammersView } from "./features/admin/index";
1223
+ export { AdminScammerEditorView } from "./features/admin/index";
1224
+ export type { AdminScammersViewProps } from "./features/admin/index";
1225
+ export type { AdminScammerEditorViewProps } from "./features/admin/index";
1222
1226
  export type { DrawerFormFooterProps } from "./features/admin/index";
1223
1227
  export type { FeatureFlagKey } from "./features/admin/index";
1224
1228
  export type { FeatureFlagMeta } from "./features/admin/index";
package/dist/index.js CHANGED
@@ -2311,6 +2311,10 @@ export { toStringValue } from "./features/admin/index";
2311
2311
  // [CLIENT-SSR]-Utility function for converting date values to relative date string.
2312
2312
  // toRelativeDate - Helper for converting date to relative date string.
2313
2313
  export { toRelativeDate } from "./features/admin/index";
2314
+ // AdminScammersView - Admin scammer profile list view.
2315
+ export { AdminScammersView } from "./features/admin/index";
2316
+ // AdminScammerEditorView - Scammer profile review/status SideDrawer.
2317
+ export { AdminScammerEditorView } from "./features/admin/index";
2314
2318
  // ./features/admin/server
2315
2319
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
2316
2320
  // adminBidsGET - Shared export for admin bids get.
package/dist/jobs.d.ts CHANGED
@@ -20,5 +20,5 @@
20
20
  * listingProcessorHandler,
21
21
  * } from "@mohasinac/appkit/jobs";
22
22
  */
23
- export { couponExpiryHandler, offerExpiryHandler, cartPruneHandler, notificationPruneHandler, dailyDataCleanupHandler, cleanupRtdbEventsHandler, auctionSettlementHandler, autoPayoutEligibilityHandler, countersReconcileHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onBidPlacedHandler, onReviewWriteHandler, promotionsHandler, mediaTmpCleanupHandler, pendingOrderTimeoutHandler, productStatsSyncHandler, positionsReconcileHandler, payoutBatchHandler, weeklyPayoutEligibilityHandler, onCategoryWriteHandler, onProductWriteHandler, onStoreWriteHandler, adminAnalyticsHandler, storeAnalyticsHandler, listingProcessorHandler, supportedListingCollections, prizeRevealOpenHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealReminderHandler, bundleStockSyncHandler, onProductStockChangeHandler, triggerEventRaffleHandler, assignSpinPrizeHandler, bindSchedule, bindDocumentWritten, bindDocumentCreated, bindDocumentUpdated, bindCallable, bindHttps, bindToFirebase, } from "./_internal/server/jobs/index.js";
23
+ export { couponExpiryHandler, offerExpiryHandler, cartPruneHandler, notificationPruneHandler, dailyDataCleanupHandler, cleanupRtdbEventsHandler, auctionSettlementHandler, autoPayoutEligibilityHandler, countersReconcileHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onBidPlacedHandler, onReviewWriteHandler, promotionsHandler, mediaTmpCleanupHandler, pendingOrderTimeoutHandler, productStatsSyncHandler, positionsReconcileHandler, payoutBatchHandler, weeklyPayoutEligibilityHandler, onCategoryWriteHandler, onProductWriteHandler, onStoreWriteHandler, adminAnalyticsHandler, storeAnalyticsHandler, listingProcessorHandler, supportedListingCollections, prizeRevealOpenHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealReminderHandler, bundleStockSyncHandler, onProductStockChangeHandler, triggerEventRaffleHandler, assignSpinPrizeHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, bindSchedule, bindDocumentWritten, bindDocumentCreated, bindDocumentUpdated, bindCallable, bindHttps, bindToFirebase, } from "./_internal/server/jobs/index.js";
24
24
  export type { PromotionsCallableResult, AdminAnalyticsResult, StoreAnalyticsInput, StoreAnalyticsResult, ListingRequestBody, ListingResponseBody, JobContext, JobLogger, JobHandlers, ScheduleHandler, FirestoreTriggerHandler, FirestoreTriggerEvent, CallableHandler, BindHttpsOptions, } from "./_internal/server/jobs/index.js";
package/dist/jobs.js CHANGED
@@ -27,5 +27,7 @@ couponExpiryHandler, offerExpiryHandler, cartPruneHandler, notificationPruneHand
27
27
  prizeRevealOpenHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealReminderHandler, bundleStockSyncHandler,
28
28
  // SB-UNI-V — Firestore onWrite trigger for product stock changes.
29
29
  onProductStockChangeHandler, triggerEventRaffleHandler, assignSpinPrizeHandler,
30
+ // BAN9 — support ticket lifecycle + ban audit trail
31
+ onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler,
30
32
  // Firebase binder adapter
31
33
  bindSchedule, bindDocumentWritten, bindDocumentCreated, bindDocumentUpdated, bindCallable, bindHttps, bindToFirebase, } from "./_internal/server/jobs/index.js";
@@ -81,6 +81,7 @@ export declare const DEFAULT_ROUTE_MAP: {
81
81
  readonly SCAM_DETAIL: (id: string) => string;
82
82
  readonly SCAM_REPORT: "/scams/report";
83
83
  readonly SCAM_TYPES: "/scams/types";
84
+ readonly SCAM_FAQS: "/scams/faqs";
84
85
  };
85
86
  readonly ERRORS: {
86
87
  readonly UNAUTHORIZED: "/unauthorized";
@@ -117,6 +118,8 @@ export declare const DEFAULT_ROUTE_MAP: {
117
118
  readonly REVIEWS: "/user/reviews";
118
119
  readonly BIDS: "/user/bids";
119
120
  readonly RETURNS: "/user/returns";
121
+ readonly SUPPORT: "/user/support";
122
+ readonly SUPPORT_TICKET: (id: string) => string;
120
123
  };
121
124
  readonly STORE: {
122
125
  readonly DASHBOARD: "/store";
@@ -231,6 +234,10 @@ export declare const DEFAULT_ROUTE_MAP: {
231
234
  readonly PRIZE_DRAWS: "/admin/prize-draws";
232
235
  readonly PRIZE_DRAWS_EDIT: (id: string) => string;
233
236
  readonly TEAM: "/admin/team";
237
+ readonly SUPPORT_TICKETS: "/admin/support-tickets";
238
+ readonly SUPPORT_TICKET_BY_ID: (id: string) => string;
239
+ readonly SCAMMERS: "/admin/scammers";
240
+ readonly SCAMMER_BY_ID: (id: string) => string;
234
241
  };
235
242
  readonly DEMO: {
236
243
  readonly SEED: "/demo/seed";
@@ -311,6 +318,7 @@ export declare const ROUTES: {
311
318
  readonly SCAM_DETAIL: (id: string) => string;
312
319
  readonly SCAM_REPORT: "/scams/report";
313
320
  readonly SCAM_TYPES: "/scams/types";
321
+ readonly SCAM_FAQS: "/scams/faqs";
314
322
  };
315
323
  readonly ERRORS: {
316
324
  readonly UNAUTHORIZED: "/unauthorized";
@@ -347,6 +355,8 @@ export declare const ROUTES: {
347
355
  readonly REVIEWS: "/user/reviews";
348
356
  readonly BIDS: "/user/bids";
349
357
  readonly RETURNS: "/user/returns";
358
+ readonly SUPPORT: "/user/support";
359
+ readonly SUPPORT_TICKET: (id: string) => string;
350
360
  };
351
361
  readonly STORE: {
352
362
  readonly DASHBOARD: "/store";
@@ -461,6 +471,10 @@ export declare const ROUTES: {
461
471
  readonly PRIZE_DRAWS: "/admin/prize-draws";
462
472
  readonly PRIZE_DRAWS_EDIT: (id: string) => string;
463
473
  readonly TEAM: "/admin/team";
474
+ readonly SUPPORT_TICKETS: "/admin/support-tickets";
475
+ readonly SUPPORT_TICKET_BY_ID: (id: string) => string;
476
+ readonly SCAMMERS: "/admin/scammers";
477
+ readonly SCAMMER_BY_ID: (id: string) => string;
464
478
  };
465
479
  readonly DEMO: {
466
480
  readonly SEED: "/demo/seed";
@@ -69,6 +69,7 @@ export const DEFAULT_ROUTE_MAP = {
69
69
  SCAM_DETAIL: (id) => `/scams/${id}`,
70
70
  SCAM_REPORT: "/scams/report",
71
71
  SCAM_TYPES: "/scams/types",
72
+ SCAM_FAQS: "/scams/faqs",
72
73
  },
73
74
  ERRORS: {
74
75
  UNAUTHORIZED: "/unauthorized",
@@ -105,6 +106,8 @@ export const DEFAULT_ROUTE_MAP = {
105
106
  REVIEWS: "/user/reviews",
106
107
  BIDS: "/user/bids",
107
108
  RETURNS: "/user/returns",
109
+ SUPPORT: "/user/support",
110
+ SUPPORT_TICKET: (id) => `/user/support/${id}`,
108
111
  },
109
112
  STORE: {
110
113
  DASHBOARD: "/store",
@@ -222,6 +225,10 @@ export const DEFAULT_ROUTE_MAP = {
222
225
  PRIZE_DRAWS: "/admin/prize-draws",
223
226
  PRIZE_DRAWS_EDIT: (id) => `/admin/prize-draws/${id}/edit`,
224
227
  TEAM: "/admin/team",
228
+ SUPPORT_TICKETS: "/admin/support-tickets",
229
+ SUPPORT_TICKET_BY_ID: (id) => `/admin/support-tickets/${id}`,
230
+ SCAMMERS: "/admin/scammers",
231
+ SCAMMER_BY_ID: (id) => `/admin/scammers/${id}`,
225
232
  },
226
233
  DEMO: {
227
234
  SEED: "/demo/seed",
@@ -1347,4 +1347,307 @@ export const faqSeedData = [
1347
1347
  createdAt: daysAgo(2),
1348
1348
  updatedAt: daysAgo(2),
1349
1349
  },
1350
+ // ══════════════════════════════════════════════════════════════════════════
1351
+ // SCAM AWARENESS (12 FAQs)
1352
+ // ══════════════════════════════════════════════════════════════════════════
1353
+ {
1354
+ id: "faq-how-to-spot-fake-seller",
1355
+ question: "How do I spot a fake or scam seller on LetItRip?",
1356
+ answer: html(`<p>Verified LetItRip sellers have a green <strong>Verified</strong> badge on their store page. Before buying, check for:</p><ul><li><strong>No badge or pending badge</strong> — the seller has not completed verification.</li><li><strong>Zero reviews or suspiciously perfect ratings</strong> — be cautious if every review is 5-star and very recent.</li><li><strong>Prices too good to be true</strong> — PSA 10 Charizard Base Set does not sell for ₹1,000.</li><li><strong>Urgency tactics</strong> — "Only 1 left, buy in 5 minutes or it's gone" is a classic pressure technique.</li><li><strong>Requests to pay outside LetItRip</strong> — any seller asking for UPI/bank transfer outside the platform is violating our Terms and likely attempting fraud.</li></ul><p>When in doubt, message the seller through LetItRip's secure messaging, check their store history, and read recent reviews carefully.</p>`),
1357
+ category: "scam_awareness",
1358
+ showOnHomepage: false,
1359
+ showInFooter: false,
1360
+ isPinned: true,
1361
+ order: 1,
1362
+ priority: 10,
1363
+ tags: ["scam", "fake seller", "verification", "trust"],
1364
+ relatedFAQs: ["faq-off-platform-payment-scam", "faq-counterfeit-collectible"],
1365
+ useSiteSettings: true,
1366
+ variables: {},
1367
+ stats: makeStats(3200, 290),
1368
+ seo: {
1369
+ slug: "faq-how-to-spot-fake-seller",
1370
+ metaTitle: "How to spot a fake seller on LetItRip",
1371
+ metaDescription: "Signs of a scam seller: no verification badge, suspiciously perfect reviews, prices too low, off-platform payment requests.",
1372
+ },
1373
+ isActive: true,
1374
+ createdBy: "user-admin-letitrip",
1375
+ createdAt: daysAgo(30),
1376
+ updatedAt: daysAgo(5),
1377
+ },
1378
+ {
1379
+ id: "faq-off-platform-payment-scam",
1380
+ question: "A seller asked me to pay via UPI or bank transfer outside LetItRip — is that safe?",
1381
+ answer: html(`<p><strong>No. Never pay outside LetItRip.</strong> This is the most common scam on collectibles platforms.</p><p>When you pay through LetItRip, Razorpay holds your money in escrow and only releases it to the seller after you confirm delivery (or after 5 days without a dispute). You are protected.</p><p>If you pay via UPI, NEFT, or any method outside the platform:</p><ul><li>LetItRip has <strong>no visibility</strong> into the transaction.</li><li>We <strong>cannot refund</strong> or mediate — we are not party to that transaction.</li><li>You have no buyer protection.</li></ul><p><strong>What to do:</strong> Report the seller immediately via the "Report seller" button on their store page, or email us at trust@letitrip.in. We will investigate and suspend the seller if confirmed.</p><p>Asking buyers to pay off-platform is an <strong>immediate permanent ban</strong> on LetItRip.</p>`),
1382
+ category: "scam_awareness",
1383
+ showOnHomepage: false,
1384
+ showInFooter: false,
1385
+ isPinned: true,
1386
+ order: 2,
1387
+ priority: 10,
1388
+ tags: ["scam", "UPI", "off-platform", "payment", "fraud"],
1389
+ relatedFAQs: ["faq-how-to-spot-fake-seller", "faq-how-to-report-scam"],
1390
+ useSiteSettings: true,
1391
+ variables: {},
1392
+ stats: makeStats(5100, 480),
1393
+ seo: {
1394
+ slug: "faq-off-platform-payment-scam",
1395
+ metaTitle: "Off-platform UPI payment scam — LetItRip",
1396
+ metaDescription: "Never pay via UPI or bank transfer outside LetItRip. Escrow only protects on-platform payments.",
1397
+ },
1398
+ isActive: true,
1399
+ createdBy: "user-admin-letitrip",
1400
+ createdAt: daysAgo(30),
1401
+ updatedAt: daysAgo(5),
1402
+ },
1403
+ {
1404
+ id: "faq-counterfeit-collectible",
1405
+ question: "What if I receive a counterfeit or fake collectible?",
1406
+ answer: html(`<p>LetItRip has a strict <strong>zero-tolerance counterfeit policy</strong>. If you suspect your item is fake:</p><ol><li><strong>Do not confirm delivery</strong> — you have 5 days after the carrier marks delivered to dispute.</li><li>Open a dispute via the Order Detail page: "I have a problem with this order" → "Item is fake or counterfeit".</li><li>Upload clear photos of the item, packaging, and any authentication details (holograms, print quality, card stock, etc.).</li><li>Our Trust team will review within 48 hours.</li></ol><p><strong>If confirmed counterfeit:</strong></p><ul><li>You receive a <strong>full refund</strong> including shipping.</li><li>The seller's listing is taken down immediately.</li><li>The seller's account is flagged for review — repeated violations result in permanent ban.</li></ul><p>For PSA/BGS/CGC graded slabs: we verify the slab cert number against the respective grading company's public database before closing the dispute in your favor.</p>`),
1407
+ category: "scam_awareness",
1408
+ showOnHomepage: false,
1409
+ showInFooter: false,
1410
+ isPinned: false,
1411
+ order: 3,
1412
+ priority: 9,
1413
+ tags: ["counterfeit", "fake", "refund", "dispute", "graded"],
1414
+ relatedFAQs: ["faq-how-to-spot-fake-seller", "faq-how-to-report-scam"],
1415
+ useSiteSettings: true,
1416
+ variables: {},
1417
+ stats: makeStats(2800, 260),
1418
+ seo: {
1419
+ slug: "faq-counterfeit-collectible",
1420
+ metaTitle: "Received a counterfeit collectible? — LetItRip",
1421
+ metaDescription: "Open a dispute within 5 days, upload photos. Confirmed fakes get a full refund and seller is banned.",
1422
+ },
1423
+ isActive: true,
1424
+ createdBy: "user-admin-letitrip",
1425
+ createdAt: daysAgo(28),
1426
+ updatedAt: daysAgo(4),
1427
+ },
1428
+ {
1429
+ id: "faq-how-to-report-scam",
1430
+ question: "How do I report a scammer or suspicious account on LetItRip?",
1431
+ answer: html(`<p>You can report a suspicious account through any of these channels:</p><ul><li><strong>Store page</strong> — click the three-dot menu (⋮) on any store page → "Report this store".</li><li><strong>Order dispute</strong> — if you already placed an order, use the dispute flow in Order Details.</li><li><strong>Scam Registry</strong> — visit <a href="/scams/report">/scams/report</a> to submit a detailed scam report including phone numbers, UPI IDs, and a description of what happened. This helps warn other buyers.</li><li><strong>Email</strong> — trust@letitrip.in for urgent cases.</li></ul><p>When reporting, include:</p><ul><li>The seller's LetItRip store name or URL</li><li>Any phone numbers, UPI IDs, or social media handles used in the scam</li><li>Screenshots of conversations (especially off-platform payment requests)</li><li>Order ID if a transaction was involved</li></ul><p>We review all scam reports within 24–48 hours. Reports go directly to our Trust & Safety team.</p>`),
1432
+ category: "scam_awareness",
1433
+ showOnHomepage: false,
1434
+ showInFooter: false,
1435
+ isPinned: true,
1436
+ order: 4,
1437
+ priority: 9,
1438
+ tags: ["report", "scam", "trust & safety", "fraud"],
1439
+ relatedFAQs: ["faq-off-platform-payment-scam", "faq-how-to-spot-fake-seller"],
1440
+ useSiteSettings: true,
1441
+ variables: {},
1442
+ stats: makeStats(1900, 180),
1443
+ seo: {
1444
+ slug: "faq-how-to-report-scam",
1445
+ metaTitle: "How to report a scammer on LetItRip",
1446
+ metaDescription: "Report scammers via the store page, order dispute, scam registry, or trust@letitrip.in.",
1447
+ },
1448
+ isActive: true,
1449
+ createdBy: "user-admin-letitrip",
1450
+ createdAt: daysAgo(28),
1451
+ updatedAt: daysAgo(4),
1452
+ },
1453
+ {
1454
+ id: "faq-phishing-fake-letitrip",
1455
+ question: "I received a message claiming to be LetItRip asking for my password or OTP — is it real?",
1456
+ answer: html(`<p><strong>No. LetItRip will never ask for your password, OTP, or payment details via WhatsApp, SMS, or email.</strong></p><p>Common phishing tactics targeting collectibles buyers:</p><ul><li>Fake "LetItRip support" WhatsApp numbers asking to "verify your order" via an OTP.</li><li>SMS links to fake login pages that steal your credentials.</li><li>Emails claiming your account will be suspended unless you click a link and re-enter your card number.</li></ul><p><strong>What LetItRip actually sends:</strong></p><ul><li>Order confirmation and shipping emails from noreply@letitrip.in.</li><li>OTPs for login — we never ask you to read the OTP back to anyone.</li><li>We never contact you via personal WhatsApp or unofficial numbers.</li></ul><p>If you received a suspicious message, do not click any links. Report the number/email to trust@letitrip.in and change your LetItRip password immediately if you've already entered credentials on an unknown site.</p>`),
1457
+ category: "scam_awareness",
1458
+ showOnHomepage: false,
1459
+ showInFooter: false,
1460
+ isPinned: false,
1461
+ order: 5,
1462
+ priority: 8,
1463
+ tags: ["phishing", "OTP", "password", "impersonation", "WhatsApp"],
1464
+ relatedFAQs: ["faq-off-platform-payment-scam", "faq-how-to-report-scam"],
1465
+ useSiteSettings: true,
1466
+ variables: {},
1467
+ stats: makeStats(2200, 200),
1468
+ seo: {
1469
+ slug: "faq-phishing-fake-letitrip",
1470
+ metaTitle: "Fake LetItRip messages asking for OTP or password",
1471
+ metaDescription: "LetItRip never asks for OTPs or passwords via WhatsApp or SMS. These are phishing scams.",
1472
+ },
1473
+ isActive: true,
1474
+ createdBy: "user-admin-letitrip",
1475
+ createdAt: daysAgo(25),
1476
+ updatedAt: daysAgo(3),
1477
+ },
1478
+ {
1479
+ id: "faq-advance-payment-scam",
1480
+ question: "A seller asked me to pay an 'advance' or 'booking amount' outside the order system — is that legitimate?",
1481
+ answer: html(`<p><strong>No. LetItRip does not have an "advance" or "booking amount" system.</strong> This is a well-known scam pattern in the Indian collectibles market.</p><p>How it works: a scammer (posing as a seller) messages you about a rare item, asks for a 20–30% advance via UPI to "hold" it, and then disappears — or demands more money before shipping.</p><p><strong>If a seller contacts you outside LetItRip</strong> (Instagram DM, WhatsApp, Telegram) and asks for an advance to hold an item:</p><ul><li>The item may not exist.</li><li>You have no recourse once you transfer money.</li><li>LetItRip's escrow and buyer protection does NOT apply to off-platform transactions.</li></ul><p>Legitimate pre-orders on LetItRip are handled through the Pre-Orders section — all payments go through Razorpay and are fully protected. If a seller is asking you to deviate from this, report them immediately.</p>`),
1482
+ category: "scam_awareness",
1483
+ showOnHomepage: false,
1484
+ showInFooter: false,
1485
+ isPinned: false,
1486
+ order: 6,
1487
+ priority: 8,
1488
+ tags: ["advance payment", "booking amount", "pre-order scam", "fraud"],
1489
+ relatedFAQs: ["faq-off-platform-payment-scam", "faq-how-to-report-scam"],
1490
+ useSiteSettings: true,
1491
+ variables: {},
1492
+ stats: makeStats(1600, 150),
1493
+ seo: {
1494
+ slug: "faq-advance-payment-scam",
1495
+ metaTitle: "Advance payment / booking amount scam — LetItRip",
1496
+ metaDescription: "LetItRip has no advance or booking system. Sellers asking for off-platform advances are scammers.",
1497
+ },
1498
+ isActive: true,
1499
+ createdBy: "user-admin-letitrip",
1500
+ createdAt: daysAgo(25),
1501
+ updatedAt: daysAgo(3),
1502
+ },
1503
+ {
1504
+ id: "faq-graded-slab-scam",
1505
+ question: "How do I verify a PSA/BGS/CGC graded card is genuine?",
1506
+ answer: html(`<p>Graded slabs are a prime target for forgery. Before buying, always verify independently:</p><ul><li><strong>PSA</strong> — visit <a href="https://www.psacard.com/cert" target="_blank" rel="noopener noreferrer">psacard.com/cert</a> and enter the cert number visible on the label.</li><li><strong>BGS (Beckett)</strong> — visit <a href="https://www.beckett.com/grading" target="_blank" rel="noopener noreferrer">beckett.com/grading</a> → Cert Verification.</li><li><strong>CGC</strong> — visit <a href="https://www.cgccards.com/certlookup/" target="_blank" rel="noopener noreferrer">cgccards.com/certlookup</a>.</li></ul><p>Common signs of a fake slab:</p><ul><li>Cert number does not appear in the grading company's database.</li><li>Label font, hologram, or case seam looks slightly off compared to genuine slabs.</li><li>Seller refuses to show a clear close-up of the cert number label.</li></ul><p>Ask the seller to share a close-up photo of the label before buying. If the cert doesn't verify, do not purchase, and report the listing via the "Report listing" button.</p>`),
1507
+ category: "scam_awareness",
1508
+ showOnHomepage: false,
1509
+ showInFooter: false,
1510
+ isPinned: false,
1511
+ order: 7,
1512
+ priority: 7,
1513
+ tags: ["PSA", "BGS", "CGC", "graded", "cert verification", "fake slab"],
1514
+ relatedFAQs: ["faq-counterfeit-collectible", "faq-how-to-spot-fake-seller"],
1515
+ useSiteSettings: true,
1516
+ variables: {},
1517
+ stats: makeStats(3400, 310),
1518
+ seo: {
1519
+ slug: "faq-graded-slab-scam",
1520
+ metaTitle: "Verify PSA / BGS / CGC graded card is genuine — LetItRip",
1521
+ metaDescription: "Always verify the cert number at psacard.com, beckett.com, or cgccards.com before buying a graded slab.",
1522
+ },
1523
+ isActive: true,
1524
+ createdBy: "user-admin-letitrip",
1525
+ createdAt: daysAgo(22),
1526
+ updatedAt: daysAgo(2),
1527
+ },
1528
+ {
1529
+ id: "faq-social-media-seller-scam",
1530
+ question: "I found a LetItRip seller on Instagram. Can I buy from them directly?",
1531
+ answer: html(`<p><strong>Always buy through LetItRip — never through DMs, Instagram, or WhatsApp links.</strong></p><p>Many LetItRip sellers promote their stores on social media, which is perfectly fine. But the moment a seller asks you to complete a transaction outside LetItRip (even if they show you their LetItRip store as "proof"), you lose all buyer protection:</p><ul><li>No escrow — money goes directly to them.</li><li>No dispute mediation — LetItRip cannot intervene.</li><li>No refund guarantee.</li></ul><p>The correct flow: find the seller's official LetItRip store via their bio link or at letitrip.in/stores, and complete the purchase through the platform. If a seller's "LetItRip link" leads to a site that looks different from letitrip.in, it may be a phishing clone — verify the URL is exactly <strong>letitrip.in</strong> or <strong>www.letitrip.in</strong>.</p>`),
1532
+ category: "scam_awareness",
1533
+ showOnHomepage: false,
1534
+ showInFooter: false,
1535
+ isPinned: false,
1536
+ order: 8,
1537
+ priority: 7,
1538
+ tags: ["Instagram", "social media", "DM", "off-platform", "fraud"],
1539
+ relatedFAQs: ["faq-off-platform-payment-scam", "faq-phishing-fake-letitrip"],
1540
+ useSiteSettings: true,
1541
+ variables: {},
1542
+ stats: makeStats(1800, 165),
1543
+ seo: {
1544
+ slug: "faq-social-media-seller-scam",
1545
+ metaTitle: "Buying from LetItRip sellers on Instagram — safe?",
1546
+ metaDescription: "Always complete purchases on letitrip.in. Social media DM sales bypass buyer protection.",
1547
+ },
1548
+ isActive: true,
1549
+ createdBy: "user-admin-letitrip",
1550
+ createdAt: daysAgo(20),
1551
+ updatedAt: daysAgo(2),
1552
+ },
1553
+ {
1554
+ id: "faq-scam-registry",
1555
+ question: "What is the LetItRip Scam Registry?",
1556
+ answer: html(`<p>The <strong>LetItRip Scam Registry</strong> is a publicly searchable database of phone numbers, UPI IDs, and display names associated with confirmed or reported collectibles scams in India.</p><p><strong>What's in the registry:</strong></p><ul><li>Phone numbers and UPI IDs reported by buyers and verified by our Trust team.</li><li>The type of scam (advance payment, fake listing, counterfeit slab, etc.).</li><li>Verification status: <em>pending review</em>, <em>verified</em>, or <em>rejected</em>.</li></ul><p><strong>How to use it:</strong> Before transferring money to any individual for a collectible — even outside LetItRip — search their phone number or UPI ID in the registry at <a href="/scams">/scams</a>. A verified hit is a strong warning to walk away.</p><p><strong>How to submit:</strong> Use the <a href="/scams/report">/scams/report</a> form. Provide as much detail as possible — more evidence means faster verification.</p><p>The registry is maintained by LetItRip's Trust & Safety team and is reviewed daily.</p>`),
1557
+ category: "scam_awareness",
1558
+ showOnHomepage: false,
1559
+ showInFooter: true,
1560
+ isPinned: false,
1561
+ order: 9,
1562
+ priority: 8,
1563
+ tags: ["scam registry", "phone number", "UPI", "trust", "database"],
1564
+ relatedFAQs: ["faq-how-to-report-scam", "faq-how-to-spot-fake-seller"],
1565
+ useSiteSettings: true,
1566
+ variables: {},
1567
+ stats: makeStats(2600, 240),
1568
+ seo: {
1569
+ slug: "faq-scam-registry",
1570
+ metaTitle: "LetItRip Scam Registry — search reported scammers",
1571
+ metaDescription: "Search phone numbers and UPI IDs in the LetItRip Scam Registry to check if a collectibles seller has been reported.",
1572
+ },
1573
+ isActive: true,
1574
+ createdBy: "user-admin-letitrip",
1575
+ createdAt: daysAgo(18),
1576
+ updatedAt: daysAgo(1),
1577
+ },
1578
+ {
1579
+ id: "faq-buyer-protection-limits",
1580
+ question: "Does LetItRip buyer protection cover all purchases?",
1581
+ answer: html(`<p>LetItRip buyer protection covers <strong>all purchases made through the platform</strong> — standard products, auctions, and pre-orders. Here's what's covered and what isn't:</p><p><strong>Covered:</strong></p><ul><li>Item not received within the stated shipping window.</li><li>Item significantly not as described (wrong item, wrong grade, wrong condition).</li><li>Confirmed counterfeit or fake item.</li><li>Seller cancels after payment.</li></ul><p><strong>Not covered:</strong></p><ul><li>Purchases made outside LetItRip (UPI transfers, social media DMs, etc.).</li><li>Prize draw results — the randomness of draws is not a dispute ground.</li><li>Buyer's remorse — changed your mind after delivery.</li><li>Damage caused by the buyer after delivery.</li><li>Disputes raised after the 5-day post-delivery window (unless extended by a pending dispute).</li></ul><p>To ensure full protection, always: pay through LetItRip, inspect your item within 5 days of delivery, and open a dispute <em>before</em> confirming delivery if something is wrong.</p>`),
1582
+ category: "scam_awareness",
1583
+ showOnHomepage: false,
1584
+ showInFooter: false,
1585
+ isPinned: false,
1586
+ order: 10,
1587
+ priority: 7,
1588
+ tags: ["buyer protection", "escrow", "dispute", "refund", "coverage"],
1589
+ relatedFAQs: ["faq-counterfeit-collectible", "faq-off-platform-payment-scam"],
1590
+ useSiteSettings: true,
1591
+ variables: {},
1592
+ stats: makeStats(4100, 380),
1593
+ seo: {
1594
+ slug: "faq-buyer-protection-limits",
1595
+ metaTitle: "What does LetItRip buyer protection cover?",
1596
+ metaDescription: "Buyer protection covers on-platform purchases. Off-platform payments, prize draw results, and buyer's remorse are excluded.",
1597
+ },
1598
+ isActive: true,
1599
+ createdBy: "user-admin-letitrip",
1600
+ createdAt: daysAgo(15),
1601
+ updatedAt: daysAgo(1),
1602
+ },
1603
+ {
1604
+ id: "faq-scam-types-collectibles",
1605
+ question: "What are the most common collectibles scams in India?",
1606
+ answer: html(`<p>Based on reports to our Trust team, these are the most common scam patterns targeting Indian collectibles buyers:</p><ol><li><strong>Advance payment / "hold my item"</strong> — seller asks for partial payment via UPI to hold a rare item, then ghosts or demands more.</li><li><strong>Fake graded slabs</strong> — counterfeit PSA/BGS slabs with non-existent cert numbers, sold at below-market prices.</li><li><strong>Wrong item shipped</strong> — a high-value item is listed, but a cheap substitute is shipped inside the same packaging.</li><li><strong>Photo stolen from legitimate seller</strong> — scammer uses another seller's photos to create a fake listing on social media, collects payment, ships nothing.</li><li><strong>Phishing via "LetItRip support"</strong> — fake support agent asks for OTP to "resolve a payment issue".</li><li><strong>Post-trade regret scam</strong> — buyer receives item, falsely claims it's fake to get a refund AND keep the item.</li><li><strong>Prize draw manipulation claims</strong> — fake screenshots claiming a specific user "won" to pressure sellers into shipping without proper verification.</li></ol><p>The best defense: buy exclusively through LetItRip, pay through Razorpay, and never share OTPs or passwords with anyone.</p>`),
1607
+ category: "scam_awareness",
1608
+ showOnHomepage: false,
1609
+ showInFooter: false,
1610
+ isPinned: false,
1611
+ order: 11,
1612
+ priority: 9,
1613
+ tags: ["scam types", "advance payment", "fake PSA", "India", "collectibles"],
1614
+ relatedFAQs: ["faq-how-to-spot-fake-seller", "faq-graded-slab-scam", "faq-scam-registry"],
1615
+ useSiteSettings: true,
1616
+ variables: {},
1617
+ stats: makeStats(5800, 530),
1618
+ seo: {
1619
+ slug: "faq-scam-types-collectibles",
1620
+ metaTitle: "Common collectibles scams in India — LetItRip guide",
1621
+ metaDescription: "7 common scam patterns: advance payments, fake graded slabs, wrong item shipped, phishing, and more.",
1622
+ },
1623
+ isActive: true,
1624
+ createdBy: "user-admin-letitrip",
1625
+ createdAt: daysAgo(12),
1626
+ updatedAt: daysAgo(1),
1627
+ },
1628
+ {
1629
+ id: "faq-seller-impersonation",
1630
+ question: "Someone is impersonating a legitimate LetItRip seller — what should I do?",
1631
+ answer: html(`<p>Seller impersonation — creating a fake account or social media profile mimicking a real LetItRip store — is one of the most sophisticated scams in the collectibles space.</p><p><strong>How to verify you're talking to the real seller:</strong></p><ul><li>The real seller's profile is at letitrip.in/stores/<em>their-store-slug</em>. Check the URL carefully.</li><li>Message the seller through LetItRip's built-in messaging system (not WhatsApp or Instagram).</li><li>Legitimate sellers will never ask you to move the conversation off-platform.</li></ul><p><strong>If you suspect impersonation:</strong></p><ol><li>Do not send any money.</li><li>Screenshot the impersonator's profile/conversation.</li><li>Report to trust@letitrip.in with subject "Seller impersonation — [store name]".</li><li>Warn the real seller by messaging them via their official LetItRip store page.</li></ol><p>We take impersonation reports seriously and will work with platforms (Instagram, WhatsApp) to have fake accounts removed where possible.</p>`),
1632
+ category: "scam_awareness",
1633
+ showOnHomepage: false,
1634
+ showInFooter: false,
1635
+ isPinned: false,
1636
+ order: 12,
1637
+ priority: 8,
1638
+ tags: ["impersonation", "fake account", "social media", "trust"],
1639
+ relatedFAQs: ["faq-social-media-seller-scam", "faq-how-to-report-scam"],
1640
+ useSiteSettings: true,
1641
+ variables: {},
1642
+ stats: makeStats(1400, 130),
1643
+ seo: {
1644
+ slug: "faq-seller-impersonation",
1645
+ metaTitle: "Seller impersonation scam — LetItRip",
1646
+ metaDescription: "How to detect and report a fake account impersonating a real LetItRip seller.",
1647
+ },
1648
+ isActive: true,
1649
+ createdBy: "user-admin-letitrip",
1650
+ createdAt: daysAgo(10),
1651
+ updatedAt: daysAgo(1),
1652
+ },
1350
1653
  ];