feat: improve mobile navigation with tap-to-reset and history filtering
This commit is contained in:
@@ -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}`;
|
||||||
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]);
|
}, [activeTab, location.hash, location.pathname, location.search]);
|
||||||
|
|
||||||
const refreshQueuedActions = React.useCallback(() => {
|
const refreshQueuedActions = React.useCallback(() => {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user