Add contextual help links to admin pages
This commit is contained in:
@@ -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')} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,8 +1070,11 @@ 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">
|
||||||
<MobileCard>
|
<XStack justifyContent="flex-end">
|
||||||
|
<ContextHelpLink slug="live-ops-control" />
|
||||||
|
</XStack>
|
||||||
|
<MobileCard>
|
||||||
<Accordion type="single" collapsible>
|
<Accordion type="single" collapsible>
|
||||||
<Accordion.Item value="upload-settings">
|
<Accordion.Item value="upload-settings">
|
||||||
<Accordion.Trigger
|
<Accordion.Trigger
|
||||||
@@ -1332,7 +1336,8 @@ export default function MobileEventControlRoomPage() {
|
|||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
</YStack>
|
||||||
)}
|
)}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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', () => ({
|
||||||
|
|||||||
42
resources/js/admin/mobile/components/ContextHelpLink.tsx
Normal file
42
resources/js/admin/mobile/components/ContextHelpLink.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user