feat: improve mobile navigation with tap-to-reset and history filtering

This commit is contained in:
Codex Agent
2026-01-07 15:14:31 +01:00
parent 8e1031fff0
commit 5866a0826c
3 changed files with 40 additions and 22 deletions

View File

@@ -32,7 +32,7 @@ type MobileShellProps = {
export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) { export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) {
const { events, activeEvent, hasMultipleEvents, hasEvents, selectEvent } = useEventContext(); const { events, activeEvent, hasMultipleEvents, hasEvents, selectEvent } = useEventContext();
const { go } = useMobileNav(activeEvent?.slug); const { go } = useMobileNav(activeEvent?.slug, activeTab);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { t, i18n } = useTranslation('mobile'); const { t, i18n } = useTranslation('mobile');
@@ -86,7 +86,15 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
React.useEffect(() => { React.useEffect(() => {
const path = `${location.pathname}${location.search}${location.hash}`; const path = `${location.pathname}${location.search}${location.hash}`;
// 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); setTabHistory(activeTab, path);
}
}, [activeTab, location.hash, location.pathname, location.search]); }, [activeTab, location.hash, location.pathname, location.search]);
const refreshQueuedActions = React.useCallback(() => { const refreshQueuedActions = React.useCallback(() => {

View File

@@ -2,10 +2,10 @@ import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useEventContext } from '../../context/EventContext'; import { useEventContext } from '../../context/EventContext';
import { NavKey } from '../components/BottomNav'; import { NavKey } from '../components/BottomNav';
import { resolveTabTarget } from '../lib/tabHistory'; import { resolveTabTarget, resolveDefaultTarget } from '../lib/tabHistory';
import { adminPath } from '../../constants'; import { adminPath } from '../../constants';
export function useMobileNav(currentSlug?: string | null) { export function useMobileNav(currentSlug?: string | null, activeTab?: NavKey) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { activeEvent } = useEventContext(); const { activeEvent } = useEventContext();
@@ -13,18 +13,16 @@ export function useMobileNav(currentSlug?: string | null) {
const go = React.useCallback( const go = React.useCallback(
(key: NavKey) => { (key: NavKey) => {
const target = resolveTabTarget(key, slug); // Tap-to-reset: If the user taps the tab they are already on, reset to root.
if (key === activeTab) {
// Tap-to-reset: If we are already at the target, and it is the home tab, navigate(resolveDefaultTarget(key, slug));
// 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'));
return; return;
} }
const target = resolveTabTarget(key, slug);
navigate(target); navigate(target);
}, },
[navigate, location.pathname, slug] [navigate, activeTab, slug]
); );
return { go, slug }; return { go, slug };

View File

@@ -1,24 +1,35 @@
import { adminPath } from '../../constants'; import { adminPath } from '../../constants';
import type { NavKey } from '../components/BottomNav'; 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<Record<NavKey, string>>; type TabHistory = {
paths: Partial<Record<NavKey, string>>;
updatedAt: number;
};
function readHistory(): TabHistory { function readHistory(): TabHistory {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return {}; return { paths: {}, updatedAt: 0 };
} }
try { try {
const raw = window.localStorage.getItem(STORAGE_KEY); const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) { if (!raw) {
return {}; return { paths: {}, updatedAt: 0 };
} }
const parsed = JSON.parse(raw) as TabHistory; 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 { } catch {
return {}; return { paths: {}, updatedAt: 0 };
} }
} }
@@ -36,15 +47,16 @@ function writeHistory(history: TabHistory): void {
export function setTabHistory(key: NavKey, path: string): void { export function setTabHistory(key: NavKey, path: string): void {
const history = readHistory(); const history = readHistory();
history[key] = path; history.paths[key] = path;
history.updatedAt = Date.now();
writeHistory(history); writeHistory(history);
} }
export function getTabHistory(): TabHistory { export function getTabHistory(): Partial<Record<NavKey, string>> {
return readHistory(); return readHistory().paths;
} }
function resolveDefaultTarget(key: NavKey, slug?: string | null): string { export function resolveDefaultTarget(key: NavKey, slug?: string | null): string {
if (key === 'tasks') { if (key === 'tasks') {
return slug ? adminPath(`/mobile/events/${slug}/tasks`) : adminPath('/mobile/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 { export function resolveTabTarget(key: NavKey, slug?: string | null): string {
const history = readHistory(); const history = readHistory();
const stored = history[key]; const stored = history.paths[key];
const fallback = resolveDefaultTarget(key, slug); const fallback = resolveDefaultTarget(key, slug);
if (!stored) { if (!stored) {