Add contextual help links to admin pages

This commit is contained in:
Codex Agent
2026-01-23 09:18:46 +01:00
parent 53a90fec33
commit 35f28fd48d
8 changed files with 80 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import toast from 'react-hot-toast';
import { adminPath } from '../constants'; import { adminPath } from '../constants';
import { useBackNavigation } from './hooks/useBackNavigation'; import { useBackNavigation } from './hooks/useBackNavigation';
import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme';
import { ContextHelpLink } from './components/ContextHelpLink';
import { extractBrandingForm, type BrandingFormValues } from '../lib/brandingForm'; import { extractBrandingForm, type BrandingFormValues } from '../lib/brandingForm';
import { DEFAULT_EVENT_BRANDING } from '@/guest/context/EventBrandingContext'; import { DEFAULT_EVENT_BRANDING } from '@/guest/context/EventBrandingContext';
import { getContrastingTextColor, relativeLuminance } from '@/guest/lib/color'; import { getContrastingTextColor, relativeLuminance } from '@/guest/lib/color';
@@ -560,6 +561,10 @@ export default function MobileBrandingPage() {
</MobileCard> </MobileCard>
) : null} ) : null}
<XStack justifyContent="flex-end">
<ContextHelpLink slug="event-prep-checklist" />
</XStack>
<MobileCard space="$2"> <MobileCard space="$2">
<XStack space="$2"> <XStack space="$2">
<TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} /> <TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} />

View File

@@ -29,6 +29,7 @@ import { useEventReadiness } from './hooks/useEventReadiness';
import { SetupChecklist } from './components/SetupChecklist'; import { SetupChecklist } from './components/SetupChecklist';
import { KpiStrip, PillBadge } from './components/Primitives'; import { KpiStrip, PillBadge } from './components/Primitives';
import { getApiErrorMessage } from '../lib/apiError'; import { getApiErrorMessage } from '../lib/apiError';
import { ContextHelpLink } from './components/ContextHelpLink';
// --- HELPERS --- // --- HELPERS ---
@@ -212,6 +213,7 @@ export default function MobileDashboardPage() {
title={t('dashboard:overview.title', 'At a glance')} title={t('dashboard:overview.title', 'At a glance')}
showSeparator={false} showSeparator={false}
compact compact
action={<ContextHelpLink slug="tenant-dashboard-overview" />}
/> />
</YStack> </YStack>
<Separator backgroundColor={theme.border} opacity={0.6} /> <Separator backgroundColor={theme.border} opacity={0.6} />

View File

@@ -45,6 +45,7 @@ import { useAdminTheme } from './theme';
import { useOnlineStatus } from './hooks/useOnlineStatus'; import { useOnlineStatus } from './hooks/useOnlineStatus';
import { useAuth } from '../auth/context'; import { useAuth } from '../auth/context';
import { withAlpha } from './components/colors'; import { withAlpha } from './components/colors';
import { ContextHelpLink } from './components/ContextHelpLink';
import { import {
enqueuePhotoAction, enqueuePhotoAction,
loadPhotoQueue, loadPhotoQueue,
@@ -279,7 +280,7 @@ export default function MobileEventControlRoomPage() {
const isMember = user?.role === 'member'; const isMember = user?.role === 'member';
const slug = slugParam ?? activeEvent?.slug ?? null; const slug = slugParam ?? activeEvent?.slug ?? null;
const online = useOnlineStatus(); const online = useOnlineStatus();
const { textStrong, text, muted, border, primary, surfaceMuted, surface } = useAdminTheme(); const { textStrong, text, muted, border, primary, danger, accent, surfaceMuted, surface } = useAdminTheme();
const [activeTab, setActiveTab] = React.useState<'moderation' | 'live'>('moderation'); const [activeTab, setActiveTab] = React.useState<'moderation' | 'live'>('moderation');
const [moderationPhotos, setModerationPhotos] = React.useState<TenantPhoto[]>([]); const [moderationPhotos, setModerationPhotos] = React.useState<TenantPhoto[]>([]);
@@ -1069,7 +1070,10 @@ export default function MobileEventControlRoomPage() {
value={activeTab} value={activeTab}
onValueChange={(val) => setActiveTab(val as 'moderation' | 'live')} onValueChange={(val) => setActiveTab(val as 'moderation' | 'live')}
header={( header={(
<YStack space="$2">
<XStack justifyContent="flex-end">
<ContextHelpLink slug="live-ops-control" />
</XStack>
<MobileCard> <MobileCard>
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<Accordion.Item value="upload-settings"> <Accordion.Item value="upload-settings">
@@ -1333,6 +1337,7 @@ export default function MobileEventControlRoomPage() {
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>
</MobileCard> </MobileCard>
</YStack>
)} )}
tabs={[ tabs={[
{ {

View File

@@ -17,6 +17,7 @@ import { resolveEventDisplayName } from '../lib/events';
import { adminPath } from '../constants'; import { adminPath } from '../constants';
import { useBackNavigation } from './hooks/useBackNavigation'; import { useBackNavigation } from './hooks/useBackNavigation';
import { useAdminTheme } from './theme'; import { useAdminTheme } from './theme';
import { ContextHelpLink } from './components/ContextHelpLink';
type LiveShowFormState = { type LiveShowFormState = {
moderation_mode: NonNullable<LiveShowSettings['moderation_mode']>; moderation_mode: NonNullable<LiveShowSettings['moderation_mode']>;
@@ -279,6 +280,10 @@ export default function MobileEventLiveShowSettingsPage() {
</MobileCard> </MobileCard>
) : null} ) : null}
<XStack justifyContent="flex-end">
<ContextHelpLink slug="live-show-setup" />
</XStack>
{loading ? ( {loading ? (
<YStack space="$2"> <YStack space="$2">
{Array.from({ length: 3 }).map((_, idx) => ( {Array.from({ length: 3 }).map((_, idx) => (

View File

@@ -51,6 +51,7 @@ import { withAlpha } from './components/colors';
import { useAdminTheme } from './theme'; import { useAdminTheme } from './theme';
import { resolveEngagementMode } from '../lib/events'; import { resolveEngagementMode } from '../lib/events';
import { useAuth } from '../auth/context'; import { useAuth } from '../auth/context';
import { ContextHelpLink } from './components/ContextHelpLink';
function allowPermission(permissions: string[], permission: string): boolean { function allowPermission(permissions: string[], permission: string): boolean {
if (permissions.includes('*') || permissions.includes(permission)) { if (permissions.includes('*') || permissions.includes(permission)) {
@@ -1162,6 +1163,9 @@ export default function MobileEventTasksPage() {
alignItems="stretch" alignItems="stretch"
width="100%" width="100%"
> >
<XStack justifyContent="flex-end" marginBottom="$2">
<ContextHelpLink slug="event-prep-checklist" />
</XStack>
<Tabs.List <Tabs.List
borderRadius={16} borderRadius={16}
borderWidth={1} borderWidth={1}

View File

@@ -23,6 +23,7 @@ import { ADMIN_BASE_PATH, adminPath } from '../constants';
import { resolveLayoutForFormat } from './qr/utils'; import { resolveLayoutForFormat } from './qr/utils';
import { useBackNavigation } from './hooks/useBackNavigation'; import { useBackNavigation } from './hooks/useBackNavigation';
import { useAdminTheme } from './theme'; import { useAdminTheme } from './theme';
import { ContextHelpLink } from './components/ContextHelpLink';
export default function MobileQrPrintPage() { export default function MobileQrPrintPage() {
const { slug: slugParam } = useParams<{ slug?: string }>(); const { slug: slugParam } = useParams<{ slug?: string }>();
@@ -101,6 +102,10 @@ export default function MobileQrPrintPage() {
</MobileCard> </MobileCard>
) : null} ) : null}
<XStack justifyContent="flex-end">
<ContextHelpLink slug="event-prep-checklist" />
</XStack>
<MobileCard space="$3" alignItems="center"> <MobileCard space="$3" alignItems="center">
<Text fontSize="$md" fontWeight="800" color={textStrong}> <Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.qr.heroTitle', 'Entrance QR Code')} {t('events.qr.heroTitle', 'Entrance QR Code')}

View File

@@ -65,6 +65,14 @@ vi.mock('../components/Primitives', () => ({
</button> </button>
), ),
SkeletonCard: () => <div>Loading...</div>, SkeletonCard: () => <div>Loading...</div>,
ContentTabs: ({ header, tabs }: { header?: React.ReactNode; tabs: Array<{ value: string; content: React.ReactNode }> }) => (
<div>
{header}
{tabs.map((tab) => (
<div key={tab.value}>{tab.content}</div>
))}
</div>
),
})); }));
vi.mock('../components/FormControls', () => ({ vi.mock('../components/FormControls', () => ({

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { HelpCircle } from 'lucide-react';
import { XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { adminPath } from '../../constants';
import { useAdminTheme } from '../theme';
type ContextHelpLinkProps = {
slug: string;
label?: string;
};
export function ContextHelpLink({ slug, label = 'Help' }: ContextHelpLinkProps) {
const navigate = useNavigate();
const { border, primary, surfaceMuted, textStrong } = useAdminTheme();
return (
<Pressable
onPress={() => navigate(adminPath(`/mobile/help/${encodeURIComponent(slug)}`))}
aria-label={label}
>
<XStack
alignItems="center"
space="$1.5"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<HelpCircle size={14} color={primary} />
<Text fontSize="$xs" fontWeight="700" color={textStrong}>
{label}
</Text>
</XStack>
</Pressable>
);
}