diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index fcc8f80..e078803 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,3 @@ -{"id":"--stealth-d39","title":"Superadmin control surface spec and access matrix","description":"Define the minimal superadmin control surface, permissions, and mapping to tenant/guest responsibilities. Document scope and non-goals.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-01-01T14:16:06.994379577+01:00","updated_at":"2026-01-01T17:23:28.230936323+01:00","close_reason":"Duplicate of fotospiel-app-ihd after beads re-init","deleted_at":"2026-01-01T17:23:28.230936323+01:00","deleted_by":"soeren","delete_reason":"Remove stray stealth issue id","original_type":"task"} {"id":"fotospiel-app-097","title":"Tenant announcements / release notes","description":"Broadcast announcements to tenants/admins with targeting and scheduling.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-01-01T14:20:21.68206312+01:00","updated_at":"2026-01-02T14:18:31.676816348+01:00","closed_at":"2026-01-02T14:18:31.676816348+01:00","close_reason":"Closed"} {"id":"fotospiel-app-0h0","title":"SEC-BILL-02 Signature freshness + retry policies for Paddle webhooks","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:53:37.618780852+01:00","created_by":"soeren","updated_at":"2026-01-01T15:53:37.618780852+01:00"} {"id":"fotospiel-app-0rb","title":"Tenant admin onboarding: inline checkout integration in welcome flow","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:22.434997456+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:28.026795975+01:00","closed_at":"2026-01-01T16:08:28.026795975+01:00","close_reason":"Completed in codebase (verified)"} @@ -17,6 +16,7 @@ {"id":"fotospiel-app-38f","title":"Paddle catalog sync: surface last sync error/log context in admin","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:59:14.865414785+01:00","created_by":"soeren","updated_at":"2026-01-02T21:16:09.109922491+01:00","closed_at":"2026-01-02T21:16:09.109922491+01:00","close_reason":"Completed"} {"id":"fotospiel-app-3ut","title":"SEC-API-03 Synthetic monitoring + alert config","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:52:46.793875724+01:00","created_by":"soeren","updated_at":"2026-01-01T15:52:46.793875724+01:00"} {"id":"fotospiel-app-3xa","title":"Security review: event admin code audit (policies, PKCE, file handling)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:05:20.115675149+01:00","created_by":"soeren","updated_at":"2026-01-01T16:05:20.115675149+01:00"} +{"id":"fotospiel-app-43mp","title":"Help-System für Event Admin PWA planen","status":"open","priority":2,"issue_type":"task","owner":"codex-agent@example.com","created_at":"2026-01-23T08:21:47.812129626+01:00","created_by":"Codex Agent","updated_at":"2026-01-23T08:21:47.812129626+01:00"} {"id":"fotospiel-app-4ar","title":"SEC-BILL-03 Failed capture notifications + ledger hook","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:54:33.266516715+01:00","created_by":"soeren","updated_at":"2026-01-01T15:54:33.266516715+01:00"} {"id":"fotospiel-app-4en","title":"Add translations for Mobile Package Shop","description":"The new MobilePackageShopPage.tsx uses translation keys like 'shop.title', 'shop.legal.agb', etc. Ensure these are added to the management.json files for de and en.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-06T18:05:50.469751088+01:00","created_by":"soeren","updated_at":"2026-01-06T18:14:19.984343737+01:00","closed_at":"2026-01-06T18:14:19.984346372+01:00"} {"id":"fotospiel-app-4i4","title":"Security review: map roles/data","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:02:58.370301875+01:00","created_by":"soeren","updated_at":"2026-01-01T16:03:03.997327414+01:00","closed_at":"2026-01-01T16:03:03.997327414+01:00","close_reason":"Completed in codebase (verified)"} diff --git a/.beads/last-touched b/.beads/last-touched index fa12790..9ce75b2 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -fotospiel-app-5veo +fotospiel-app-43mp diff --git a/resources/js/admin/mobile/HelpArticlePage.tsx b/resources/js/admin/mobile/HelpArticlePage.tsx new file mode 100644 index 0000000..7a94221 --- /dev/null +++ b/resources/js/admin/mobile/HelpArticlePage.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { ChevronRight } from 'lucide-react'; +import { YStack } from '@tamagui/stacks'; +import { SizableText as Text } from '@tamagui/text'; +import { YGroup } from '@tamagui/group'; +import { ListItem } from '@tamagui/list-item'; + +import { MobileShell } from './components/MobileShell'; +import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives'; +import { useAdminTheme } from './theme'; +import { useBackNavigation } from './hooks/useBackNavigation'; +import { adminPath, ADMIN_FAQ_PATH } from '../constants'; +import { fetchHelpCenterArticle, type HelpCenterArticle } from '../api'; + +export default function MobileHelpArticlePage() { + const { slug } = useParams<{ slug: string }>(); + const { t, i18n } = useTranslation(['management', 'dashboard']); + const theme = useAdminTheme(); + const back = useBackNavigation(ADMIN_FAQ_PATH); + const navigate = useNavigate(); + const locale = i18n.language; + + const { data, isLoading, isError, refetch } = useQuery({ + queryKey: ['mobile', 'help-article', slug, locale], + enabled: Boolean(slug), + queryFn: () => fetchHelpCenterArticle(slug ?? '', locale), + }); + + const article: HelpCenterArticle | null = data ?? null; + + return ( + + {isLoading ? ( + + + + + ) : null} + + {isError ? ( + + + + {t('dashboard:help.error', 'Help could not be loaded.')} + + refetch()} + fullWidth={false} + /> + + + ) : null} + + {!isLoading && article ? ( + + + + + {article.title} + + {article.updated_at ? ( + + {t('help.article.updated', 'Aktualisiert')}:{' '} + {new Date(article.updated_at).toLocaleDateString(locale, { + day: '2-digit', + month: 'short', + year: 'numeric', + })} + + ) : null} +
+ + + + {article.related && article.related.length > 0 ? ( + + + + {t('help.article.relatedTitle', 'Weitere Artikel')} + + + {article.related.map((rel) => ( + + navigate(adminPath(`/mobile/help/${encodeURIComponent(rel.slug)}`))} + title={ + + {rel.title ?? rel.slug} + + } + iconAfter={} + /> + + ))} + + + + ) : null} + + ) : null} + + ); +} diff --git a/resources/js/admin/mobile/HelpCenterPage.tsx b/resources/js/admin/mobile/HelpCenterPage.tsx new file mode 100644 index 0000000..828408e --- /dev/null +++ b/resources/js/admin/mobile/HelpCenterPage.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { ChevronRight, HelpCircle } from 'lucide-react'; +import { YStack, XStack } from '@tamagui/stacks'; +import { SizableText as Text } from '@tamagui/text'; +import { YGroup } from '@tamagui/group'; +import { ListItem } from '@tamagui/list-item'; + +import { MobileShell } from './components/MobileShell'; +import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives'; +import { useAdminTheme } from './theme'; +import { useBackNavigation } from './hooks/useBackNavigation'; +import { adminPath, ADMIN_PROFILE_PATH } from '../constants'; +import { fetchHelpCenterArticles, type HelpCenterArticleSummary } from '../api'; + +const FAQ_SLUGS = new Set(['admin-issue-resolution']); + +function isFaqArticle(article: HelpCenterArticleSummary): boolean { + const title = article.title?.toLowerCase() ?? ''; + return FAQ_SLUGS.has(article.slug) || article.slug.startsWith('faq-') || title.includes('faq'); +} + +export default function MobileHelpCenterPage() { + const navigate = useNavigate(); + const { t, i18n } = useTranslation(['management', 'dashboard']); + const theme = useAdminTheme(); + const back = useBackNavigation(ADMIN_PROFILE_PATH); + const locale = i18n.language; + + const { data, isLoading, isError, refetch } = useQuery({ + queryKey: ['mobile', 'help-center', locale], + queryFn: () => fetchHelpCenterArticles(locale), + }); + + const articles = Array.isArray(data) ? data : []; + const faqArticles = articles.filter(isFaqArticle); + const guideArticles = articles.filter((article) => !isFaqArticle(article)); + + return ( + + {isLoading ? ( + + + + + ) : null} + + {isError ? ( + + + + {t('dashboard:help.error', 'Help could not be loaded.')} + + refetch()} + fullWidth={false} + /> + + + ) : null} + + {!isLoading && !isError ? ( + + navigate(adminPath(`/mobile/help/${encodeURIComponent(slug)}`))} + /> + navigate(adminPath(`/mobile/help/${encodeURIComponent(slug)}`))} + /> + + ) : null} + + ); +} + +function HelpSection({ + title, + items, + emptyLabel, + icon: IconCmp, + onSelect, +}: { + title: string; + items: HelpCenterArticleSummary[]; + emptyLabel: string; + icon?: React.ComponentType<{ size?: number; color?: string }>; + onSelect: (slug: string) => void; +}) { + const theme = useAdminTheme(); + + return ( + + + + {IconCmp ? ( + + + + ) : null} + + {title} + + + {items.length === 0 ? ( + + {emptyLabel} + + ) : ( + + {items.map((item) => ( + + onSelect(item.slug)} + title={ + + {item.title} + + } + subTitle={ + item.summary ? ( + + {item.summary} + + ) : undefined + } + iconAfter={} + /> + + ))} + + )} + + + ); +} diff --git a/resources/js/admin/mobile/ProfilePage.tsx b/resources/js/admin/mobile/ProfilePage.tsx index 8d3b775..82c14f0 100644 --- a/resources/js/admin/mobile/ProfilePage.tsx +++ b/resources/js/admin/mobile/ProfilePage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { User, Settings, Globe, Moon, Download, LogOut } from 'lucide-react'; +import { User, Settings, Globe, Moon, Download, LogOut, HelpCircle } from 'lucide-react'; import { Avatar } from '@tamagui/avatar'; import { Card } from '@tamagui/card'; import { YStack, XStack } from '@tamagui/stacks'; @@ -13,7 +13,7 @@ import { CTAButton } from './components/Primitives'; import { MobileSelect } from './components/FormControls'; import { useAuth } from '../auth/context'; import { fetchTenantProfile } from '../api'; -import { adminPath, ADMIN_DATA_EXPORTS_PATH, ADMIN_PROFILE_ACCOUNT_PATH } from '../constants'; +import { adminPath, ADMIN_DATA_EXPORTS_PATH, ADMIN_PROFILE_ACCOUNT_PATH, ADMIN_FAQ_PATH } from '../constants'; import i18n from '../i18n'; import { useAppearance } from '@/hooks/use-appearance'; import { useBackNavigation } from './hooks/useBackNavigation'; @@ -180,6 +180,21 @@ export default function MobileProfilePage() { ) : null} + + + {t('common.help', 'Help')} + + } + iconAfter={} + onPress={() => navigate(ADMIN_FAQ_PATH)} + /> + diff --git a/resources/js/admin/mobile/__tests__/HelpArticlePage.test.tsx b/resources/js/admin/mobile/__tests__/HelpArticlePage.test.tsx new file mode 100644 index 0000000..81044c6 --- /dev/null +++ b/resources/js/admin/mobile/__tests__/HelpArticlePage.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +const article = { + slug: 'tenant-dashboard-overview', + title: 'Dashboard overview', + summary: 'Learn the dashboard.', + body_html: '

Welcome

', + related: [], +}; + +vi.mock('@tanstack/react-query', () => ({ + useQuery: () => ({ + data: article, + isLoading: false, + isError: false, + refetch: vi.fn(), + }), +})); + +vi.mock('react-router-dom', () => ({ + useParams: () => ({ slug: 'tenant-dashboard-overview' }), + useNavigate: () => vi.fn(), +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (_key: string, fallback?: string) => fallback ?? _key, + i18n: { language: 'de' }, + }), + initReactI18next: { + type: '3rdParty', + init: () => undefined, + }, +})); + +vi.mock('../components/MobileShell', () => ({ + MobileShell: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('../hooks/useBackNavigation', () => ({ + useBackNavigation: () => vi.fn(), +})); + +vi.mock('../theme', () => ({ + useAdminTheme: () => ({ + textStrong: '#111827', + muted: '#6b7280', + border: '#e5e7eb', + }), +})); + +vi.mock('@tamagui/stacks', () => ({ + YStack: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('@tamagui/text', () => ({ + SizableText: ({ children }: { children: React.ReactNode }) => {children}, +})); + +vi.mock('@tamagui/group', () => ({ + YGroup: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { + Item: ({ children }: { children: React.ReactNode }) =>
{children}
, + }), +})); + +vi.mock('@tamagui/list-item', () => ({ + ListItem: ({ title }: { title?: React.ReactNode }) =>
{title}
, +})); + +vi.mock('../components/Primitives', () => ({ + MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, + CTAButton: ({ label }: { label: string }) => , + SkeletonCard: () =>
Loading...
, +})); + +import MobileHelpArticlePage from '../HelpArticlePage'; + +describe('MobileHelpArticlePage', () => { + it('renders article title', async () => { + render(); + + expect(screen.getByText('Dashboard overview')).toBeInTheDocument(); + }); +}); diff --git a/resources/js/admin/mobile/__tests__/HelpCenterPage.test.tsx b/resources/js/admin/mobile/__tests__/HelpCenterPage.test.tsx new file mode 100644 index 0000000..8f5aa13 --- /dev/null +++ b/resources/js/admin/mobile/__tests__/HelpCenterPage.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +const articles = [ + { slug: 'admin-issue-resolution', title: 'FAQ & Troubleshooting', summary: 'Common issues.' }, + { slug: 'tenant-dashboard-overview', title: 'Dashboard overview', summary: 'Learn the dashboard.' }, +]; + +vi.mock('@tanstack/react-query', () => ({ + useQuery: () => ({ + data: articles, + isLoading: false, + isError: false, + refetch: vi.fn(), + }), +})); + +vi.mock('react-router-dom', () => ({ + useNavigate: () => vi.fn(), +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (_key: string, fallback?: string) => fallback ?? _key, + i18n: { language: 'de' }, + }), + initReactI18next: { + type: '3rdParty', + init: () => undefined, + }, +})); + +vi.mock('../components/MobileShell', () => ({ + MobileShell: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('../hooks/useBackNavigation', () => ({ + useBackNavigation: () => vi.fn(), +})); + +vi.mock('../theme', () => ({ + useAdminTheme: () => ({ + textStrong: '#111827', + muted: '#6b7280', + border: '#e5e7eb', + surfaceMuted: '#f3f4f6', + shadow: 'rgba(0,0,0,0.12)', + }), +})); + +vi.mock('@tamagui/stacks', () => ({ + YStack: ({ children }: { children: React.ReactNode }) =>
{children}
, + XStack: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('@tamagui/text', () => ({ + SizableText: ({ children }: { children: React.ReactNode }) => {children}, +})); + +vi.mock('@tamagui/group', () => ({ + YGroup: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { + Item: ({ children }: { children: React.ReactNode }) =>
{children}
, + }), +})); + +vi.mock('@tamagui/list-item', () => ({ + ListItem: ({ title, subTitle }: { title?: React.ReactNode; subTitle?: React.ReactNode }) => ( +
+ {title} + {subTitle} +
+ ), +})); + +vi.mock('tamagui', () => ({ + Separator: ({ children }: { children?: React.ReactNode }) =>
{children}
, +})); + +vi.mock('../components/Primitives', () => ({ + MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, + CTAButton: ({ label }: { label: string }) => , + SkeletonCard: () =>
Loading...
, +})); + +import MobileHelpCenterPage from '../HelpCenterPage'; + +describe('MobileHelpCenterPage', () => { + it('renders FAQ and guide articles', async () => { + render(); + + expect(screen.getByText('FAQ & Troubleshooting')).toBeInTheDocument(); + expect(screen.getByText('Dashboard overview')).toBeInTheDocument(); + }); +}); diff --git a/resources/js/admin/mobile/components/MobileShell.tsx b/resources/js/admin/mobile/components/MobileShell.tsx index bdac2bb..5b2c6f6 100644 --- a/resources/js/admin/mobile/components/MobileShell.tsx +++ b/resources/js/admin/mobile/components/MobileShell.tsx @@ -21,6 +21,7 @@ import { countQueuedPhotoActions } from '../lib/queueStatus'; import { useAdminTheme } from '../theme'; import { useAuth } from '../../auth/context'; import { EventSwitcherSheet } from './EventSwitcherSheet'; +import { UserMenuSheet } from './UserMenuSheet'; type MobileShellProps = { title?: string; @@ -55,6 +56,7 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions const [attemptedFetch, setAttemptedFetch] = React.useState(false); const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0); const [switcherOpen, setSwitcherOpen] = React.useState(false); + const [userMenuOpen, setUserMenuOpen] = React.useState(false); const effectiveEvents = events.length ? events : fallbackEvents; const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null); @@ -267,7 +269,7 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions {/* User Avatar */} - navigate(adminPath('/mobile/profile'))}> + setUserMenuOpen(true)} aria-label={t('mobileProfile.title', 'Profile')}> + setUserMenuOpen(false)} + user={user} + isMember={isMember} + navigate={navigate} + /> ); } diff --git a/resources/js/admin/mobile/components/UserMenuSheet.tsx b/resources/js/admin/mobile/components/UserMenuSheet.tsx new file mode 100644 index 0000000..b184a17 --- /dev/null +++ b/resources/js/admin/mobile/components/UserMenuSheet.tsx @@ -0,0 +1,266 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ChevronRight, CreditCard, FileText, HelpCircle, User, X } from 'lucide-react'; +import { XStack, YStack } from '@tamagui/stacks'; +import { SizableText as Text } from '@tamagui/text'; +import { Pressable } from '@tamagui/react-native-web-lite'; +import { ListItem } from '@tamagui/list-item'; +import { YGroup } from '@tamagui/group'; +import { Switch } from '@tamagui/switch'; +import { Separator } from 'tamagui'; + +import { useAppearance } from '@/hooks/use-appearance'; +import { ADMIN_BILLING_PATH, ADMIN_DATA_EXPORTS_PATH, ADMIN_FAQ_PATH, ADMIN_PROFILE_ACCOUNT_PATH, adminPath } from '../../constants'; +import { useAdminTheme } from '../theme'; +import { MobileSelect } from './FormControls'; + +type UserMenuSheetProps = { + open: boolean; + onClose: () => void; + user?: { name?: string | null; email?: string | null; avatar_url?: string | null }; + isMember: boolean; + navigate: (path: string) => void; +}; + +const MENU_WIDTH = 320; + +export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserMenuSheetProps) { + const { t, i18n } = useTranslation('management'); + const theme = useAdminTheme(); + const { appearance, resolved, updateAppearance } = useAppearance(); + const [language, setLanguage] = React.useState(() => (i18n.language?.startsWith('en') ? 'en' : 'de')); + + React.useEffect(() => { + setLanguage(i18n.language?.startsWith('en') ? 'en' : 'de'); + }, [i18n.language]); + + const isDark = resolved === 'dark'; + + const handleNavigate = (path: string) => { + onClose(); + navigate(path); + }; + + const menuItems = [ + { + label: t('mobileProfile.account', 'Account bearbeiten'), + icon: User, + path: ADMIN_PROFILE_ACCOUNT_PATH, + show: true, + }, + { + label: t('billing.sections.packages.title', 'Pakete'), + icon: CreditCard, + path: adminPath('/mobile/billing#packages'), + show: !isMember, + }, + { + label: t('billing.sections.invoices.title', 'Rechnungen & Zahlungen'), + icon: CreditCard, + path: adminPath('/mobile/billing#invoices'), + show: !isMember, + }, + { + label: t('dataExports.title', 'Datenexporte'), + icon: FileText, + path: ADMIN_DATA_EXPORTS_PATH, + show: !isMember, + }, + { + label: t('common.help', 'Help'), + icon: HelpCircle, + path: ADMIN_FAQ_PATH, + show: true, + }, + ].filter((item) => item.show); + + return ( + + + + + + + {t('mobileProfile.title', 'Profil')} + + + + + + + + + + + + {user?.name?.charAt(0)?.toUpperCase() ?? 'U'} + + + + + {user?.name ?? t('events.members.roles.guest', 'Guest')} + + + {user?.email ?? ''} + + + + + + + + + {t('mobileProfile.settings', 'Einstellungen')} + + + {menuItems.map((item) => ( + + handleNavigate(item.path)} + title={ + + + + + + {item.label} + + + } + iconAfter={} + /> + + ))} + + + + + + {t('settings.appearance.title', 'Darstellung')} + + + + + + {t('mobileProfile.language', 'Sprache')} + + + } + iconAfter={ + { + const lng = event.target.value; + setLanguage(lng); + void i18n.changeLanguage(lng); + }} + compact + style={{ minWidth: 120 }} + > + + + + } + /> + + + + + {t('mobileProfile.theme', 'Dark Mode')} + + + } + iconAfter={ + updateAppearance(next ? 'dark' : 'light')} + aria-label={t('mobileProfile.theme', 'Dark Mode')} + > + + + } + /> + + + {appearance === 'system' ? ( + + {t('mobileProfile.themeSystem', 'System')} + + ) : null} + + + + ); +} diff --git a/resources/js/admin/mobile/components/__tests__/MobileShell.test.tsx b/resources/js/admin/mobile/components/__tests__/MobileShell.test.tsx index 8bb8e5a..1055066 100644 --- a/resources/js/admin/mobile/components/__tests__/MobileShell.test.tsx +++ b/resources/js/admin/mobile/components/__tests__/MobileShell.test.tsx @@ -37,11 +37,24 @@ vi.mock('@tamagui/react-native-web-lite', () => ({ ), })); +vi.mock('tamagui', () => ({ + Separator: ({ children }: { children?: React.ReactNode }) =>
{children}
, + Tabs: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { + List: ({ children }: { children: React.ReactNode }) =>
{children}
, + Tab: ({ children }: { children: React.ReactNode }) =>
{children}
, + Content: ({ children }: { children: React.ReactNode }) =>
{children}
, + }), +})); + vi.mock('../BottomNav', () => ({ BottomNav: () =>
, NavKey: {}, })); +vi.mock('../UserMenuSheet', () => ({ + UserMenuSheet: () =>
, +})); + const eventContext = { events: [], activeEvent: { slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} }, diff --git a/resources/js/admin/router.tsx b/resources/js/admin/router.tsx index 83f3d7e..64fd4a1 100644 --- a/resources/js/admin/router.tsx +++ b/resources/js/admin/router.tsx @@ -41,6 +41,8 @@ const MobileSettingsPage = React.lazy(() => import('./mobile/SettingsPage')); const MobileDataExportsPage = React.lazy(() => import('./mobile/DataExportsPage')); const MobileLoginPage = React.lazy(() => import('./mobile/LoginPage')); const MobilePublicHelpPage = React.lazy(() => import('./mobile/PublicHelpPage')); +const MobileHelpCenterPage = React.lazy(() => import('./mobile/HelpCenterPage')); +const MobileHelpArticlePage = React.lazy(() => import('./mobile/HelpArticlePage')); const MobileForgotPasswordPage = React.lazy(() => import('./mobile/ForgotPasswordPage')); const MobileResetPasswordPage = React.lazy(() => import('./mobile/ResetPasswordPage')); const MobileDashboardPage = React.lazy(() => import('./mobile/DashboardPage')); @@ -214,6 +216,8 @@ export const router = createBrowserRouter([ { path: 'mobile/notifications/:notificationId', element: }, { path: 'mobile/profile', element: }, { path: 'mobile/profile/account', element: }, + { path: 'mobile/help', element: }, + { path: 'mobile/help/:slug', element: }, { path: 'mobile/billing', element: }, { path: 'mobile/billing/shop', element: }, { path: 'mobile/settings', element: },