Navigation now feels more “app‑like” with
stateful tabs and reliable back behavior, and a full onboarding flow is wired in with conditional package selection
(skips when an active package exists).
What changed
- Added per‑tab history + back navigation fallback to make tab switching/Back feel native (resources/js/admin/mobile/
lib/tabHistory.ts, resources/js/admin/mobile/hooks/useBackNavigation.ts, resources/js/admin/mobile/hooks/
useMobileNav.ts, resources/js/admin/mobile/components/MobileShell.tsx + updates across mobile pages).
- Implemented onboarding flow pages + shared shell, and wired new routes/prefetch (resources/js/admin/mobile/welcome/
WelcomeLandingPage.tsx, resources/js/admin/mobile/welcome/WelcomePackagesPage.tsx, resources/js/admin/mobile/
welcome/WelcomeSummaryPage.tsx, resources/js/admin/mobile/welcome/WelcomeEventPage.tsx, resources/js/admin/mobile/
components/OnboardingShell.tsx, resources/js/admin/router.tsx, resources/js/admin/mobile/prefetch.ts).
- Conditional package step: packages page redirects to event setup if activePackage exists; selection stored locally
for summary (resources/js/admin/mobile/lib/onboardingSelection.ts, resources/js/admin/mobile/welcome/
WelcomePackagesPage.tsx).
- Added a “Start welcome journey” CTA in the empty dashboard state (resources/js/admin/mobile/DashboardPage.tsx).
- Added translations for onboarding shell + selected package + dashboard CTA (resources/js/admin/i18n/locales/en/
onboarding.json, resources/js/admin/i18n/locales/de/onboarding.json, resources/js/admin/i18n/locales/en/
management.json, resources/js/admin/i18n/locales/de/management.json).
- Tests for new helpers/hooks (resources/js/admin/mobile/lib/tabHistory.test.ts, resources/js/admin/mobile/lib/
onboardingSelection.test.ts, resources/js/admin/mobile/hooks/useBackNavigation.test.tsx).
This commit is contained in:
21
resources/js/admin/mobile/lib/onboardingSelection.test.ts
Normal file
21
resources/js/admin/mobile/lib/onboardingSelection.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getSelectedPackageId, setSelectedPackageId } from './onboardingSelection';
|
||||
|
||||
describe('onboardingSelection', () => {
|
||||
beforeEach(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('stores and returns the selected package id', () => {
|
||||
setSelectedPackageId(12);
|
||||
expect(getSelectedPackageId()).toBe(12);
|
||||
});
|
||||
|
||||
it('clears the selection when set to null', () => {
|
||||
setSelectedPackageId(3);
|
||||
setSelectedPackageId(null);
|
||||
expect(getSelectedPackageId()).toBeNull();
|
||||
});
|
||||
});
|
||||
34
resources/js/admin/mobile/lib/onboardingSelection.ts
Normal file
34
resources/js/admin/mobile/lib/onboardingSelection.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
const STORAGE_KEY = 'admin-onboarding-package-v1';
|
||||
|
||||
export function getSelectedPackageId(): number | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const value = Number(raw);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setSelectedPackageId(id: number | null): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!id) {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(STORAGE_KEY, String(id));
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
36
resources/js/admin/mobile/lib/tabHistory.test.ts
Normal file
36
resources/js/admin/mobile/lib/tabHistory.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getTabHistory, resolveTabTarget, setTabHistory } from './tabHistory';
|
||||
import { adminPath } from '../../constants';
|
||||
|
||||
describe('tabHistory', () => {
|
||||
beforeEach(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('stores tab history entries', () => {
|
||||
setTabHistory('home', adminPath('/mobile/dashboard'));
|
||||
setTabHistory('tasks', adminPath('/mobile/tasks'));
|
||||
const history = getTabHistory();
|
||||
expect(history.home).toBe(adminPath('/mobile/dashboard'));
|
||||
expect(history.tasks).toBe(adminPath('/mobile/tasks'));
|
||||
});
|
||||
|
||||
it('returns fallback when no history exists', () => {
|
||||
const target = resolveTabTarget('uploads', null);
|
||||
expect(target).toBe(adminPath('/mobile/uploads'));
|
||||
});
|
||||
|
||||
it('reuses stored event route when slug matches', () => {
|
||||
setTabHistory('uploads', adminPath('/mobile/events/summer/photos'));
|
||||
const target = resolveTabTarget('uploads', 'summer');
|
||||
expect(target).toBe(adminPath('/mobile/events/summer/photos'));
|
||||
});
|
||||
|
||||
it('falls back to active slug when stored slug differs', () => {
|
||||
setTabHistory('tasks', adminPath('/mobile/events/winter/tasks'));
|
||||
const target = resolveTabTarget('tasks', 'summer');
|
||||
expect(target).toBe(adminPath('/mobile/events/summer/tasks'));
|
||||
});
|
||||
});
|
||||
92
resources/js/admin/mobile/lib/tabHistory.ts
Normal file
92
resources/js/admin/mobile/lib/tabHistory.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { adminPath } from '../../constants';
|
||||
import type { NavKey } from '../components/BottomNav';
|
||||
|
||||
const STORAGE_KEY = 'admin-mobile-tab-history-v1';
|
||||
|
||||
type TabHistory = Partial<Record<NavKey, string>>;
|
||||
|
||||
function readHistory(): TabHistory {
|
||||
if (typeof window === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(raw) as TabHistory;
|
||||
return parsed ?? {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeHistory(history: TabHistory): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
export function setTabHistory(key: NavKey, path: string): void {
|
||||
const history = readHistory();
|
||||
history[key] = path;
|
||||
writeHistory(history);
|
||||
}
|
||||
|
||||
export function getTabHistory(): TabHistory {
|
||||
return readHistory();
|
||||
}
|
||||
|
||||
function resolveDefaultTarget(key: NavKey, slug?: string | null): string {
|
||||
if (key === 'tasks') {
|
||||
return slug ? adminPath(`/mobile/events/${slug}/tasks`) : adminPath('/mobile/tasks');
|
||||
}
|
||||
if (key === 'uploads') {
|
||||
return slug ? adminPath(`/mobile/events/${slug}/photos`) : adminPath('/mobile/uploads');
|
||||
}
|
||||
if (key === 'profile') {
|
||||
return adminPath('/mobile/profile');
|
||||
}
|
||||
return adminPath('/mobile/dashboard');
|
||||
}
|
||||
|
||||
function resolveEventScopedTarget(path: string, slug: string | null | undefined, key: NavKey): string {
|
||||
if (!slug) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (key !== 'tasks' && key !== 'uploads') {
|
||||
return path;
|
||||
}
|
||||
|
||||
const match = path.match(/\/event-admin\/mobile\/events\/([^/]+)\/(tasks|photos)(?:\/.*)?$/);
|
||||
if (!match) {
|
||||
return resolveDefaultTarget(key, slug);
|
||||
}
|
||||
|
||||
const storedSlug = match[1];
|
||||
if (storedSlug === slug) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return resolveDefaultTarget(key, slug);
|
||||
}
|
||||
|
||||
export function resolveTabTarget(key: NavKey, slug?: string | null): string {
|
||||
const history = readHistory();
|
||||
const stored = history[key];
|
||||
const fallback = resolveDefaultTarget(key, slug);
|
||||
|
||||
if (!stored) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return resolveEventScopedTarget(stored, slug, key);
|
||||
}
|
||||
Reference in New Issue
Block a user