From 8c5d3b93d5c7fbfe612a3d56223ae89e81827c35 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 7 Jan 2026 15:14:31 +0100 Subject: [PATCH] feat: improve mobile navigation with tap-to-reset and history filtering --- .../admin/mobile/components/MobileShell.tsx | 12 +++++-- .../js/admin/mobile/hooks/useMobileNav.ts | 16 ++++----- resources/js/admin/mobile/lib/tabHistory.ts | 34 +++++++++++++------ 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/resources/js/admin/mobile/components/MobileShell.tsx b/resources/js/admin/mobile/components/MobileShell.tsx index b396f19..e6cfed8 100644 --- a/resources/js/admin/mobile/components/MobileShell.tsx +++ b/resources/js/admin/mobile/components/MobileShell.tsx @@ -32,7 +32,7 @@ type MobileShellProps = { export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) { const { events, activeEvent, hasMultipleEvents, hasEvents, selectEvent } = useEventContext(); - const { go } = useMobileNav(activeEvent?.slug); + const { go } = useMobileNav(activeEvent?.slug, activeTab); const navigate = useNavigate(); const location = useLocation(); const { t, i18n } = useTranslation('mobile'); @@ -86,7 +86,15 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head React.useEffect(() => { const path = `${location.pathname}${location.search}${location.hash}`; - setTabHistory(activeTab, path); + + // Blacklist transient paths from being saved in tab history + const isBlacklisted = + location.pathname.includes('/billing/shop') || + location.pathname.includes('/welcome'); + + if (!isBlacklisted) { + setTabHistory(activeTab, path); + } }, [activeTab, location.hash, location.pathname, location.search]); const refreshQueuedActions = React.useCallback(() => { diff --git a/resources/js/admin/mobile/hooks/useMobileNav.ts b/resources/js/admin/mobile/hooks/useMobileNav.ts index 292b831..fede01b 100644 --- a/resources/js/admin/mobile/hooks/useMobileNav.ts +++ b/resources/js/admin/mobile/hooks/useMobileNav.ts @@ -2,10 +2,10 @@ import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useEventContext } from '../../context/EventContext'; import { NavKey } from '../components/BottomNav'; -import { resolveTabTarget } from '../lib/tabHistory'; +import { resolveTabTarget, resolveDefaultTarget } from '../lib/tabHistory'; import { adminPath } from '../../constants'; -export function useMobileNav(currentSlug?: string | null) { +export function useMobileNav(currentSlug?: string | null, activeTab?: NavKey) { const navigate = useNavigate(); const location = useLocation(); const { activeEvent } = useEventContext(); @@ -13,18 +13,16 @@ export function useMobileNav(currentSlug?: string | null) { const go = React.useCallback( (key: NavKey) => { - const target = resolveTabTarget(key, slug); - - // Tap-to-reset: If we are already at the target, and it is the home tab, - // and we are not at the dashboard root, then go to dashboard. - if (key === 'home' && location.pathname === target && target !== adminPath('/mobile/dashboard')) { - navigate(adminPath('/mobile/dashboard')); + // Tap-to-reset: If the user taps the tab they are already on, reset to root. + if (key === activeTab) { + navigate(resolveDefaultTarget(key, slug)); return; } + const target = resolveTabTarget(key, slug); navigate(target); }, - [navigate, location.pathname, slug] + [navigate, activeTab, slug] ); return { go, slug }; diff --git a/resources/js/admin/mobile/lib/tabHistory.ts b/resources/js/admin/mobile/lib/tabHistory.ts index 3d85d27..fb18e11 100644 --- a/resources/js/admin/mobile/lib/tabHistory.ts +++ b/resources/js/admin/mobile/lib/tabHistory.ts @@ -1,24 +1,35 @@ import { adminPath } from '../../constants'; import type { NavKey } from '../components/BottomNav'; -const STORAGE_KEY = 'admin-mobile-tab-history-v1'; +const STORAGE_KEY = 'admin-mobile-tab-history-v2'; +const EXPIRY_MS = 1000 * 60 * 60 * 2; // 2 hours -type TabHistory = Partial>; +type TabHistory = { + paths: Partial>; + updatedAt: number; +}; function readHistory(): TabHistory { if (typeof window === 'undefined') { - return {}; + return { paths: {}, updatedAt: 0 }; } try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) { - return {}; + return { paths: {}, updatedAt: 0 }; } const parsed = JSON.parse(raw) as TabHistory; - return parsed ?? {}; + + // Check for expiry + if (Date.now() - parsed.updatedAt > EXPIRY_MS) { + window.localStorage.removeItem(STORAGE_KEY); + return { paths: {}, updatedAt: 0 }; + } + + return parsed ?? { paths: {}, updatedAt: 0 }; } catch { - return {}; + return { paths: {}, updatedAt: 0 }; } } @@ -36,15 +47,16 @@ function writeHistory(history: TabHistory): void { export function setTabHistory(key: NavKey, path: string): void { const history = readHistory(); - history[key] = path; + history.paths[key] = path; + history.updatedAt = Date.now(); writeHistory(history); } -export function getTabHistory(): TabHistory { - return readHistory(); +export function getTabHistory(): Partial> { + return readHistory().paths; } -function resolveDefaultTarget(key: NavKey, slug?: string | null): string { +export function resolveDefaultTarget(key: NavKey, slug?: string | null): string { if (key === 'tasks') { return slug ? adminPath(`/mobile/events/${slug}/tasks`) : adminPath('/mobile/tasks'); } @@ -81,7 +93,7 @@ function resolveEventScopedTarget(path: string, slug: string | null | undefined, export function resolveTabTarget(key: NavKey, slug?: string | null): string { const history = readHistory(); - const stored = history[key]; + const stored = history.paths[key]; const fallback = resolveDefaultTarget(key, slug); if (!stored) {