feat(settings): default landing page (plan Tier 6)
- New per-user 'default_landing_page' setting (tracker default / calendar / summary / spending), registered in the whitelist + defaults. - Implemented as a post-login redirect (keeps /=Tracker so the Home link and direct nav are unaffected). Precedence: admin → deep-link they were bounced from (state.from) → default landing → /. Resolved by fetching settings in the login success path, with a safe / fallback so a fetch failure never blocks login. - Settings: Select in the Regional section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
01030fe7d0
commit
7e5e012434
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { errMessage } from '@/lib/utils';
|
||||
|
|
@ -22,8 +22,13 @@ interface AuthModeInfo {
|
|||
oidc_provider_name?: string;
|
||||
}
|
||||
|
||||
const LANDING_ROUTES: Record<string, string> = {
|
||||
tracker: '/', calendar: '/calendar', summary: '/summary', spending: '/spending',
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { setUser, refresh } = useAuth();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
|
|
@ -49,6 +54,24 @@ export default function LoginPage() {
|
|||
|
||||
const destFor = (user?: User | null) => (user?.is_default_admin ? '/admin' : '/');
|
||||
|
||||
// Where to send the user after signing in. Precedence: admin console →
|
||||
// explicit deep-link they were bounced from → their default landing page → /.
|
||||
const resolveDest = async (user?: User | null): Promise<string> => {
|
||||
if (user?.is_default_admin) return '/admin';
|
||||
const from = (location.state as { from?: { pathname?: string } } | null)?.from?.pathname;
|
||||
if (from && from !== '/login') return from;
|
||||
try {
|
||||
const s = (await api.settings()) as Record<string, unknown>;
|
||||
const landing = typeof s.default_landing_page === 'string' ? LANDING_ROUTES[s.default_landing_page] : undefined;
|
||||
return landing || '/';
|
||||
} catch {
|
||||
return '/'; // never block login on a settings-fetch failure
|
||||
}
|
||||
};
|
||||
const navigateAfterAuth = async (user?: User | null) => {
|
||||
navigate(await resolveDest(user), { replace: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
api.authMode()
|
||||
.then(raw => {
|
||||
|
|
@ -77,7 +100,7 @@ export default function LoginPage() {
|
|||
setShowPrivacy(true);
|
||||
setShowChangePw(false);
|
||||
} else {
|
||||
navigate(destFor(user), { replace: true });
|
||||
void navigateAfterAuth(user);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -148,7 +171,7 @@ export default function LoginPage() {
|
|||
if (pendingUser?.first_login) {
|
||||
setShowPrivacy(true);
|
||||
} else {
|
||||
navigate(destFor(pendingUser), { replace: true });
|
||||
void navigateAfterAuth(pendingUser);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(errMessage(err, 'Failed to change password.'));
|
||||
|
|
@ -164,7 +187,7 @@ export default function LoginPage() {
|
|||
|
||||
refresh();
|
||||
setShowPrivacy(false);
|
||||
navigate(destFor(pendingUser), { replace: true });
|
||||
void navigateAfterAuth(pendingUser);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ interface SettingsState {
|
|||
currency: string;
|
||||
date_format: string;
|
||||
week_start: string;
|
||||
default_landing_page: string;
|
||||
grace_period_days: number | string;
|
||||
due_soon_days: number | string;
|
||||
drift_threshold_pct: string;
|
||||
|
|
@ -335,6 +336,7 @@ export default function SettingsPage() {
|
|||
currency: 'USD',
|
||||
date_format: 'MM/DD/YYYY',
|
||||
week_start: '0',
|
||||
default_landing_page: 'tracker',
|
||||
grace_period_days: 5,
|
||||
due_soon_days: 3,
|
||||
drift_threshold_pct: '5',
|
||||
|
|
@ -368,6 +370,7 @@ export default function SettingsPage() {
|
|||
currency: s.currency,
|
||||
date_format: s.date_format,
|
||||
week_start: s.week_start,
|
||||
default_landing_page: s.default_landing_page,
|
||||
grace_period_days: s.grace_period_days,
|
||||
due_soon_days: s.due_soon_days,
|
||||
drift_threshold_pct: s.drift_threshold_pct,
|
||||
|
|
@ -494,6 +497,20 @@ export default function SettingsPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow label="Default landing page" description="Where you land right after signing in.">
|
||||
<Select value={settings.default_landing_page} onValueChange={(v) => set('default_landing_page', v)}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="tracker">Tracker</SelectItem>
|
||||
<SelectItem value="calendar">Calendar</SelectItem>
|
||||
<SelectItem value="summary">Summary</SelectItem>
|
||||
<SelectItem value="spending">Spending</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
</SectionCard>
|
||||
|
||||
{/* Tracker Layout */}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const USER_SETTING_KEYS = [
|
|||
'currency',
|
||||
'date_format',
|
||||
'week_start',
|
||||
'default_landing_page',
|
||||
'grace_period_days',
|
||||
'due_soon_days',
|
||||
'notify_days_before',
|
||||
|
|
@ -32,6 +33,7 @@ const USER_SETTING_KEYS = [
|
|||
|
||||
const USER_SETTING_DEFAULTS = {
|
||||
week_start: '0',
|
||||
default_landing_page: 'tracker',
|
||||
due_soon_days: '3',
|
||||
search_bars_collapsed: 'false',
|
||||
bank_auto_categorize_merchants: 'true',
|
||||
|
|
|
|||
Loading…
Reference in New Issue