Compare commits
2 Commits
03ee16bb87
...
53a90fec33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53a90fec33 | ||
|
|
e1a2850768 |
@@ -17,6 +17,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-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-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-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-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-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)"}
|
{"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)"}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
fotospiel-app-5veo
|
fotospiel-app-43mp
|
||||||
|
|||||||
114
resources/js/admin/mobile/HelpArticlePage.tsx
Normal file
114
resources/js/admin/mobile/HelpArticlePage.tsx
Normal file
@@ -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 (
|
||||||
|
<MobileShell activeTab="profile" title={article?.title ?? t('common.help', 'Help')} onBack={back}>
|
||||||
|
{isLoading ? (
|
||||||
|
<YStack space="$2">
|
||||||
|
<SkeletonCard height={120} />
|
||||||
|
<SkeletonCard height={160} />
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isError ? (
|
||||||
|
<MobileCard>
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||||
|
{t('dashboard:help.error', 'Help could not be loaded.')}
|
||||||
|
</Text>
|
||||||
|
<CTAButton
|
||||||
|
label={t('common.retry', 'Erneut versuchen')}
|
||||||
|
onPress={() => refetch()}
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isLoading && article ? (
|
||||||
|
<YStack space="$3">
|
||||||
|
<MobileCard>
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
|
||||||
|
{article.title}
|
||||||
|
</Text>
|
||||||
|
{article.updated_at ? (
|
||||||
|
<Text fontSize="$xs" color={theme.muted}>
|
||||||
|
{t('help.article.updated', 'Aktualisiert')}:{' '}
|
||||||
|
{new Date(article.updated_at).toLocaleDateString(locale, {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="prose prose-sm max-w-none dark:prose-invert [&_table]:w-full [&_table]:text-sm [&_:where(p,ul,ol,li)]:text-foreground [&_:where(h1,h2,h3,h4,h5,h6)]:text-foreground"
|
||||||
|
dangerouslySetInnerHTML={{ __html: article.body_html ?? article.body_markdown ?? '' }}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
|
||||||
|
{article.related && article.related.length > 0 ? (
|
||||||
|
<MobileCard>
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||||
|
{t('help.article.relatedTitle', 'Weitere Artikel')}
|
||||||
|
</Text>
|
||||||
|
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
|
||||||
|
{article.related.map((rel) => (
|
||||||
|
<YGroup.Item key={rel.slug}>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
onPress={() => navigate(adminPath(`/mobile/help/${encodeURIComponent(rel.slug)}`))}
|
||||||
|
title={
|
||||||
|
<Text fontSize="$sm" color={theme.textStrong}>
|
||||||
|
{rel.title ?? rel.slug}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
iconAfter={<ChevronRight size={16} color={theme.muted} />}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
))}
|
||||||
|
</YGroup>
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
) : null}
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
|
</MobileShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
resources/js/admin/mobile/HelpCenterPage.tsx
Normal file
158
resources/js/admin/mobile/HelpCenterPage.tsx
Normal file
@@ -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 (
|
||||||
|
<MobileShell activeTab="profile" title={t('common.help', 'Help')} onBack={back}>
|
||||||
|
{isLoading ? (
|
||||||
|
<YStack space="$2">
|
||||||
|
<SkeletonCard height={120} />
|
||||||
|
<SkeletonCard height={120} />
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isError ? (
|
||||||
|
<MobileCard>
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||||
|
{t('dashboard:help.error', 'Help could not be loaded.')}
|
||||||
|
</Text>
|
||||||
|
<CTAButton
|
||||||
|
label={t('common.retry', 'Erneut versuchen')}
|
||||||
|
onPress={() => refetch()}
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isLoading && !isError ? (
|
||||||
|
<YStack space="$3">
|
||||||
|
<HelpSection
|
||||||
|
title={t('dashboard:help.title', 'FAQ')}
|
||||||
|
icon={HelpCircle}
|
||||||
|
items={faqArticles}
|
||||||
|
emptyLabel={t('dashboard:help.description', 'Noch keine FAQ-Artikel verfügbar.')}
|
||||||
|
onSelect={(slug) => navigate(adminPath(`/mobile/help/${encodeURIComponent(slug)}`))}
|
||||||
|
/>
|
||||||
|
<HelpSection
|
||||||
|
title={t('dashboard:help.documentationTitle', 'Guides & Dokus')}
|
||||||
|
items={guideArticles}
|
||||||
|
emptyLabel={t('dashboard:help.description', 'Noch keine Help-Artikel verfügbar.')}
|
||||||
|
onSelect={(slug) => navigate(adminPath(`/mobile/help/${encodeURIComponent(slug)}`))}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
|
</MobileShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<MobileCard padding="$0">
|
||||||
|
<YStack padding="$3" space="$2">
|
||||||
|
<XStack alignItems="center" space="$2">
|
||||||
|
{IconCmp ? (
|
||||||
|
<XStack
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
borderRadius={10}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={theme.surfaceMuted}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
>
|
||||||
|
<IconCmp size={14} color={theme.textStrong} />
|
||||||
|
</XStack>
|
||||||
|
) : null}
|
||||||
|
<Text fontSize="$md" fontWeight="800" color={theme.textStrong}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<Text fontSize="$sm" color={theme.muted}>
|
||||||
|
{emptyLabel}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<YGroup.Item key={item.slug}>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
onPress={() => onSelect(item.slug)}
|
||||||
|
title={
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
subTitle={
|
||||||
|
item.summary ? (
|
||||||
|
<Text fontSize="$xs" color={theme.muted}>
|
||||||
|
{item.summary}
|
||||||
|
</Text>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
iconAfter={<ChevronRight size={16} color={theme.muted} />}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
))}
|
||||||
|
</YGroup>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { Avatar } from '@tamagui/avatar';
|
||||||
import { Card } from '@tamagui/card';
|
import { Card } from '@tamagui/card';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
@@ -13,7 +13,7 @@ import { CTAButton } from './components/Primitives';
|
|||||||
import { MobileSelect } from './components/FormControls';
|
import { MobileSelect } from './components/FormControls';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { fetchTenantProfile } from '../api';
|
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 i18n from '../i18n';
|
||||||
import { useAppearance } from '@/hooks/use-appearance';
|
import { useAppearance } from '@/hooks/use-appearance';
|
||||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||||
@@ -180,6 +180,21 @@ export default function MobileProfilePage() {
|
|||||||
</YGroup.Item>
|
</YGroup.Item>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
<YGroup.Item>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
title={
|
||||||
|
<Text fontSize="$sm" color={textColor}>
|
||||||
|
{t('common.help', 'Help')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
iconAfter={<HelpCircle size={18} color={subtle} />}
|
||||||
|
onPress={() => navigate(ADMIN_FAQ_PATH)}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
</YGroup>
|
</YGroup>
|
||||||
</YStack>
|
</YStack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
86
resources/js/admin/mobile/__tests__/HelpArticlePage.test.tsx
Normal file
86
resources/js/admin/mobile/__tests__/HelpArticlePage.test.tsx
Normal file
@@ -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: '<p>Welcome</p>',
|
||||||
|
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 }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/group', () => ({
|
||||||
|
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
||||||
|
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/list-item', () => ({
|
||||||
|
ListItem: ({ title }: { title?: React.ReactNode }) => <div>{title}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/Primitives', () => ({
|
||||||
|
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
|
||||||
|
SkeletonCard: () => <div>Loading...</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import MobileHelpArticlePage from '../HelpArticlePage';
|
||||||
|
|
||||||
|
describe('MobileHelpArticlePage', () => {
|
||||||
|
it('renders article title', async () => {
|
||||||
|
render(<MobileHelpArticlePage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Dashboard overview')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
95
resources/js/admin/mobile/__tests__/HelpCenterPage.test.tsx
Normal file
95
resources/js/admin/mobile/__tests__/HelpCenterPage.test.tsx
Normal file
@@ -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 }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 }) => <div>{children}</div>,
|
||||||
|
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/group', () => ({
|
||||||
|
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
||||||
|
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/list-item', () => ({
|
||||||
|
ListItem: ({ title, subTitle }: { title?: React.ReactNode; subTitle?: React.ReactNode }) => (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{subTitle}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('tamagui', () => ({
|
||||||
|
Separator: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/Primitives', () => ({
|
||||||
|
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
|
||||||
|
SkeletonCard: () => <div>Loading...</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import MobileHelpCenterPage from '../HelpCenterPage';
|
||||||
|
|
||||||
|
describe('MobileHelpCenterPage', () => {
|
||||||
|
it('renders FAQ and guide articles', async () => {
|
||||||
|
render(<MobileHelpCenterPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('FAQ & Troubleshooting')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Dashboard overview')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ import { countQueuedPhotoActions } from '../lib/queueStatus';
|
|||||||
import { useAdminTheme } from '../theme';
|
import { useAdminTheme } from '../theme';
|
||||||
import { useAuth } from '../../auth/context';
|
import { useAuth } from '../../auth/context';
|
||||||
import { EventSwitcherSheet } from './EventSwitcherSheet';
|
import { EventSwitcherSheet } from './EventSwitcherSheet';
|
||||||
|
import { UserMenuSheet } from './UserMenuSheet';
|
||||||
|
|
||||||
type MobileShellProps = {
|
type MobileShellProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -55,6 +56,7 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
|||||||
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
|
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
|
||||||
const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0);
|
const [queuedPhotoCount, setQueuedPhotoCount] = React.useState(0);
|
||||||
const [switcherOpen, setSwitcherOpen] = React.useState(false);
|
const [switcherOpen, setSwitcherOpen] = React.useState(false);
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
|
||||||
|
|
||||||
const effectiveEvents = events.length ? events : fallbackEvents;
|
const effectiveEvents = events.length ? events : fallbackEvents;
|
||||||
const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null);
|
const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null);
|
||||||
@@ -267,7 +269,7 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
|||||||
</HeaderActionButton>
|
</HeaderActionButton>
|
||||||
|
|
||||||
{/* User Avatar */}
|
{/* User Avatar */}
|
||||||
<Pressable onPress={() => navigate(adminPath('/mobile/profile'))}>
|
<Pressable onPress={() => setUserMenuOpen(true)} aria-label={t('mobileProfile.title', 'Profile')}>
|
||||||
<XStack
|
<XStack
|
||||||
width={36} height={36} borderRadius={18}
|
width={36} height={36} borderRadius={18}
|
||||||
backgroundColor={actionSurface}
|
backgroundColor={actionSurface}
|
||||||
@@ -369,6 +371,13 @@ export function MobileShell({ title, children, activeTab, onBack, headerActions
|
|||||||
events={effectiveEvents}
|
events={effectiveEvents}
|
||||||
activeSlug={effectiveActive?.slug ?? null}
|
activeSlug={effectiveActive?.slug ?? null}
|
||||||
/>
|
/>
|
||||||
|
<UserMenuSheet
|
||||||
|
open={userMenuOpen}
|
||||||
|
onClose={() => setUserMenuOpen(false)}
|
||||||
|
user={user}
|
||||||
|
isMember={isMember}
|
||||||
|
navigate={navigate}
|
||||||
|
/>
|
||||||
</YStack>
|
</YStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
266
resources/js/admin/mobile/components/UserMenuSheet.tsx
Normal file
266
resources/js/admin/mobile/components/UserMenuSheet.tsx
Normal file
@@ -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<string>(() => (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 (
|
||||||
|
<YStack
|
||||||
|
position="fixed"
|
||||||
|
top={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
zIndex={100000}
|
||||||
|
pointerEvents={open ? 'auto' : 'none'}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={onClose}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
backgroundColor: theme.overlay,
|
||||||
|
opacity: open ? 1 : 0,
|
||||||
|
transition: 'opacity 180ms ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<YStack
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
bottom={0}
|
||||||
|
right={0}
|
||||||
|
width="86vw"
|
||||||
|
maxWidth={MENU_WIDTH}
|
||||||
|
backgroundColor={theme.surface}
|
||||||
|
borderLeftWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
padding="$4"
|
||||||
|
space="$3"
|
||||||
|
style={{
|
||||||
|
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
||||||
|
transition: 'transform 220ms ease',
|
||||||
|
boxShadow: `-12px 0 24px ${theme.glassShadow ?? theme.shadow}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
|
<Text fontSize="$md" fontWeight="800" color={theme.textStrong}>
|
||||||
|
{t('mobileProfile.title', 'Profil')}
|
||||||
|
</Text>
|
||||||
|
<Pressable onPress={onClose} aria-label={t('common.close', 'Close')}>
|
||||||
|
<XStack
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
borderRadius={10}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={theme.surfaceMuted}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
>
|
||||||
|
<X size={16} color={theme.muted} />
|
||||||
|
</XStack>
|
||||||
|
</Pressable>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<XStack alignItems="center" space="$3">
|
||||||
|
<XStack
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
borderRadius={16}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={theme.accentSoft}
|
||||||
|
>
|
||||||
|
<Text fontSize="$lg" fontWeight="800" color={theme.primary}>
|
||||||
|
{user?.name?.charAt(0)?.toUpperCase() ?? 'U'}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
<YStack>
|
||||||
|
<Text fontSize="$sm" fontWeight="800" color={theme.textStrong}>
|
||||||
|
{user?.name ?? t('events.members.roles.guest', 'Guest')}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$xs" color={theme.muted}>
|
||||||
|
{user?.email ?? ''}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
||||||
|
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||||
|
{t('mobileProfile.settings', 'Einstellungen')}
|
||||||
|
</Text>
|
||||||
|
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<YGroup.Item key={item.label}>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
onPress={() => handleNavigate(item.path)}
|
||||||
|
title={
|
||||||
|
<XStack alignItems="center" space="$2">
|
||||||
|
<XStack
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
borderRadius={10}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={theme.surfaceMuted}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
>
|
||||||
|
<item.icon size={14} color={theme.textStrong} />
|
||||||
|
</XStack>
|
||||||
|
<Text fontSize="$sm" color={theme.textStrong}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
iconAfter={<ChevronRight size={16} color={theme.muted} />}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
))}
|
||||||
|
</YGroup>
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||||
|
{t('settings.appearance.title', 'Darstellung')}
|
||||||
|
</Text>
|
||||||
|
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
|
||||||
|
<YGroup.Item>
|
||||||
|
<ListItem
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
title={
|
||||||
|
<XStack space="$2" alignItems="center">
|
||||||
|
<Text fontSize="$sm" color={theme.textStrong}>
|
||||||
|
{t('mobileProfile.language', 'Sprache')}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
iconAfter={
|
||||||
|
<MobileSelect
|
||||||
|
value={language}
|
||||||
|
onChange={(event) => {
|
||||||
|
const lng = event.target.value;
|
||||||
|
setLanguage(lng);
|
||||||
|
void i18n.changeLanguage(lng);
|
||||||
|
}}
|
||||||
|
compact
|
||||||
|
style={{ minWidth: 120 }}
|
||||||
|
>
|
||||||
|
<option value="de">{t('mobileProfile.languageDe', 'Deutsch')}</option>
|
||||||
|
<option value="en">{t('mobileProfile.languageEn', 'English')}</option>
|
||||||
|
</MobileSelect>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
<YGroup.Item>
|
||||||
|
<ListItem
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
title={
|
||||||
|
<XStack space="$2" alignItems="center">
|
||||||
|
<Text fontSize="$sm" color={theme.textStrong}>
|
||||||
|
{t('mobileProfile.theme', 'Dark Mode')}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
iconAfter={
|
||||||
|
<Switch
|
||||||
|
size="$2"
|
||||||
|
checked={isDark}
|
||||||
|
onCheckedChange={(next) => updateAppearance(next ? 'dark' : 'light')}
|
||||||
|
aria-label={t('mobileProfile.theme', 'Dark Mode')}
|
||||||
|
>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
</YGroup>
|
||||||
|
{appearance === 'system' ? (
|
||||||
|
<Text fontSize="$xs" color={theme.muted}>
|
||||||
|
{t('mobileProfile.themeSystem', 'System')}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,11 +37,24 @@ vi.mock('@tamagui/react-native-web-lite', () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('tamagui', () => ({
|
||||||
|
Separator: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
||||||
|
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
Tab: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../BottomNav', () => ({
|
vi.mock('../BottomNav', () => ({
|
||||||
BottomNav: () => <div data-testid="bottom-nav" />,
|
BottomNav: () => <div data-testid="bottom-nav" />,
|
||||||
NavKey: {},
|
NavKey: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../UserMenuSheet', () => ({
|
||||||
|
UserMenuSheet: () => <div data-testid="user-menu-sheet" />,
|
||||||
|
}));
|
||||||
|
|
||||||
const eventContext = {
|
const eventContext = {
|
||||||
events: [],
|
events: [],
|
||||||
activeEvent: { slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
activeEvent: { slug: 'event-1', name: 'Test Event', event_date: '2024-01-01', status: 'active', settings: {} },
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const MobileSettingsPage = React.lazy(() => import('./mobile/SettingsPage'));
|
|||||||
const MobileDataExportsPage = React.lazy(() => import('./mobile/DataExportsPage'));
|
const MobileDataExportsPage = React.lazy(() => import('./mobile/DataExportsPage'));
|
||||||
const MobileLoginPage = React.lazy(() => import('./mobile/LoginPage'));
|
const MobileLoginPage = React.lazy(() => import('./mobile/LoginPage'));
|
||||||
const MobilePublicHelpPage = React.lazy(() => import('./mobile/PublicHelpPage'));
|
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 MobileForgotPasswordPage = React.lazy(() => import('./mobile/ForgotPasswordPage'));
|
||||||
const MobileResetPasswordPage = React.lazy(() => import('./mobile/ResetPasswordPage'));
|
const MobileResetPasswordPage = React.lazy(() => import('./mobile/ResetPasswordPage'));
|
||||||
const MobileDashboardPage = React.lazy(() => import('./mobile/DashboardPage'));
|
const MobileDashboardPage = React.lazy(() => import('./mobile/DashboardPage'));
|
||||||
@@ -214,6 +216,8 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'mobile/notifications/:notificationId', element: <MobileNotificationsPage /> },
|
{ path: 'mobile/notifications/:notificationId', element: <MobileNotificationsPage /> },
|
||||||
{ path: 'mobile/profile', element: <MobileProfilePage /> },
|
{ path: 'mobile/profile', element: <MobileProfilePage /> },
|
||||||
{ path: 'mobile/profile/account', element: <MobileProfileAccountPage /> },
|
{ path: 'mobile/profile/account', element: <MobileProfileAccountPage /> },
|
||||||
|
{ path: 'mobile/help', element: <MobileHelpCenterPage /> },
|
||||||
|
{ path: 'mobile/help/:slug', element: <MobileHelpArticlePage /> },
|
||||||
{ path: 'mobile/billing', element: <RequireAdminAccess><MobileBillingPage /></RequireAdminAccess> },
|
{ path: 'mobile/billing', element: <RequireAdminAccess><MobileBillingPage /></RequireAdminAccess> },
|
||||||
{ path: 'mobile/billing/shop', element: <RequireAdminAccess><MobilePackageShopPage /></RequireAdminAccess> },
|
{ path: 'mobile/billing/shop', element: <RequireAdminAccess><MobilePackageShopPage /></RequireAdminAccess> },
|
||||||
{ path: 'mobile/settings', element: <RequireAdminAccess><MobileSettingsPage /></RequireAdminAccess> },
|
{ path: 'mobile/settings', element: <RequireAdminAccess><MobileSettingsPage /></RequireAdminAccess> },
|
||||||
|
|||||||
Reference in New Issue
Block a user