feat(settings): safe-to-spend toggle, move notifications from Profile to Settings, fix dark-mode readability
This commit is contained in:
parent
dc49eb9633
commit
8ef794a94a
|
|
@ -80,7 +80,9 @@
|
|||
--secondary: 0.275 0.018 245;
|
||||
--secondary-foreground: 0.94 0.007 245;
|
||||
--muted: 0.275 0.016 245;
|
||||
--muted-foreground: 0.76 0.012 245;
|
||||
/* Secondary text. Raised 0.76 → 0.85 — at 0.76 labels and hints sat too
|
||||
close to the card background (L 0.235) and strained the eyes. */
|
||||
--muted-foreground: 0.85 0.012 245;
|
||||
--accent: 0.32 0.045 158;
|
||||
--accent-foreground: 0.965 0.006 245;
|
||||
--destructive: 0.66 0.18 26;
|
||||
|
|
@ -172,20 +174,22 @@
|
|||
}
|
||||
|
||||
|
||||
/* Faded muted-text variants: keep them readable in dark mode — Tailwind's
|
||||
raw /40–/70 alphas dissolve into the background. */
|
||||
.dark .text-muted-foreground\/40 {
|
||||
color: oklch(var(--muted-foreground) / 0.72);
|
||||
color: oklch(var(--muted-foreground) / 0.8);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/50 {
|
||||
color: oklch(var(--muted-foreground) / 0.78);
|
||||
color: oklch(var(--muted-foreground) / 0.85);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/60 {
|
||||
color: oklch(var(--muted-foreground) / 0.84);
|
||||
color: oklch(var(--muted-foreground) / 0.9);
|
||||
}
|
||||
|
||||
.dark .text-muted-foreground\/70 {
|
||||
color: oklch(var(--muted-foreground) / 0.9);
|
||||
color: oklch(var(--muted-foreground) / 0.95);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function displayNameOf(profile) {
|
|||
return profile.display_name || profile.displayName || profile.name || '';
|
||||
}
|
||||
|
||||
function asSettings(data) {
|
||||
export function asSettings(data) {
|
||||
return data?.settings || data?.notifications || data || {};
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +360,9 @@ function EditProfile({ profile, onSaved }) {
|
|||
);
|
||||
}
|
||||
|
||||
function NotificationPreferences({ settings, onSaved }) {
|
||||
// Exported: rendered on the Settings page ("Notifications" section). Lives here
|
||||
// because it shares asSettings/CheckRow/SectionCard with the rest of this file.
|
||||
export function NotificationPreferences({ settings, onSaved }) {
|
||||
const [form, setForm] = useState(settings);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
|
|
@ -427,7 +429,8 @@ const PUSH_CHANNELS = [
|
|||
{ value: 'telegram', label: 'Telegram', urlLabel: 'Server URL / n/a', urlHint: 'Leave blank for api.telegram.org', tokenLabel: 'Bot token', chatIdLabel: 'Chat ID' },
|
||||
];
|
||||
|
||||
function PushNotifications({ settings, onSaved }) {
|
||||
// Exported: rendered on the Settings page ("Notifications" section).
|
||||
export function PushNotifications({ settings, onSaved }) {
|
||||
const [enabled, setEnabled] = useState(!!settings.notify_push_enabled);
|
||||
const [channel, setChannel] = useState(settings.push_channel || 'ntfy');
|
||||
const [url, setUrl] = useState(settings.push_url || '');
|
||||
|
|
@ -898,7 +901,6 @@ function ProfileNav() {
|
|||
const items = [
|
||||
['#account', 'Account'],
|
||||
['#security', 'Security'],
|
||||
['#notifications', 'Notifications'],
|
||||
['#privacy', 'Privacy'],
|
||||
];
|
||||
return (
|
||||
|
|
@ -950,7 +952,7 @@ export default function ProfilePage() {
|
|||
<div className="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Profile</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Manage your account, notification preferences, and password.</p>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Manage your account, security, and privacy. Notification preferences live in Settings.</p>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-emerald-500" />
|
||||
|
|
@ -968,10 +970,7 @@ export default function ProfilePage() {
|
|||
<div id="security" className="scroll-mt-6">
|
||||
<ChangePassword />
|
||||
</div>
|
||||
<div id="notifications" className="scroll-mt-6 space-y-4">
|
||||
{!loading && <NotificationPreferences settings={settings} onSaved={setSettings} />}
|
||||
{!loading && <PushNotifications settings={settings} onSaved={() => api.profileSettings().then(setSettings).catch(() => {})} />}
|
||||
</div>
|
||||
{/* Notification preferences moved to Settings → Notifications */}
|
||||
<div id="privacy" className="scroll-mt-6">
|
||||
{!loading && <PrivacySettings settings={settings} onSaved={setSettings} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,36 @@ import {
|
|||
import { Switch } from '@/components/ui/switch';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { NotificationPreferences, PushNotifications, asSettings } from '@/pages/ProfilePage';
|
||||
|
||||
export const LINK_IMPORT_PREF_KEY = 'link_import_ask';
|
||||
export function getLinkImportPref() {
|
||||
return localStorage.getItem(LINK_IMPORT_PREF_KEY) !== 'false';
|
||||
}
|
||||
|
||||
// ─── Notifications (moved here from Profile — these are app-behavior
|
||||
// preferences; Profile keeps identity, security, and privacy) ──────────────
|
||||
|
||||
function NotificationsSection() {
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
const load = useCallback(() => {
|
||||
api.profileSettings()
|
||||
.then((data) => setSettings(asSettings(data)))
|
||||
.catch((err) => toast.error(err.message || 'Failed to load notification settings.'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
if (!settings) return null;
|
||||
return (
|
||||
<div className="space-y-4 mb-4">
|
||||
<NotificationPreferences settings={settings} onSaved={setSettings} />
|
||||
<PushNotifications settings={settings} onSaved={load} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Card wrapper ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SectionCard({ title, children }) {
|
||||
|
|
@ -255,6 +279,7 @@ export default function SettingsPage() {
|
|||
tracker_bank_projection_banner_snoozed_until: '',
|
||||
tracker_show_search_sort: 'true',
|
||||
tracker_show_summary_cards: 'true',
|
||||
tracker_show_safe_to_spend: 'true',
|
||||
tracker_show_overdue_command_center: 'true',
|
||||
tracker_show_drift_insights: 'true',
|
||||
};
|
||||
|
|
@ -289,6 +314,7 @@ export default function SettingsPage() {
|
|||
tracker_bank_projection_banner_snoozed_until: settings.tracker_bank_projection_banner_snoozed_until || '',
|
||||
tracker_show_search_sort: settings.tracker_show_search_sort,
|
||||
tracker_show_summary_cards: settings.tracker_show_summary_cards,
|
||||
tracker_show_safe_to_spend: settings.tracker_show_safe_to_spend,
|
||||
tracker_show_overdue_command_center: settings.tracker_show_overdue_command_center,
|
||||
tracker_show_drift_insights: settings.tracker_show_drift_insights,
|
||||
});
|
||||
|
|
@ -329,7 +355,7 @@ export default function SettingsPage() {
|
|||
{/* Page header — flat on background */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Manage your display and billing preferences</p>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Manage your display, billing, and notification preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Appearance */}
|
||||
|
|
@ -400,6 +426,16 @@ export default function SettingsPage() {
|
|||
aria-label="Show Summary cards"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="Safe to Spend"
|
||||
description="Show the safe-to-spend projection card with the payday countdown and upcoming bills."
|
||||
>
|
||||
<Switch
|
||||
checked={settingsBool(settings.tracker_show_safe_to_spend)}
|
||||
onCheckedChange={(checked) => set('tracker_show_safe_to_spend', String(checked))}
|
||||
aria-label="Show Safe to Spend"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="Overdue Command Center"
|
||||
description="Show the quick-action panel for overdue bills."
|
||||
|
|
@ -467,6 +503,11 @@ export default function SettingsPage() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Notifications — email + push reminder preferences (save independently) */}
|
||||
<div id="notifications" className="mt-6 scroll-mt-24">
|
||||
<NotificationsSection />
|
||||
</div>
|
||||
|
||||
<div id="calendar-feed" className="mt-6 scroll-mt-24">
|
||||
<SectionCard title="Calendar Feed">
|
||||
<CalendarFeedManager />
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ export default function TrackerPage() {
|
|||
const showSearchSort = settingEnabled(trackerSettings.tracker_show_search_sort);
|
||||
const showSummaryCards = settingEnabled(trackerSettings.tracker_show_summary_cards);
|
||||
const showOverdueCommandCenter = settingEnabled(trackerSettings.tracker_show_overdue_command_center);
|
||||
const showSafeToSpend = settingEnabled(trackerSettings.tracker_show_safe_to_spend);
|
||||
const showDriftInsights = settingEnabled(trackerSettings.tracker_show_drift_insights);
|
||||
const showBankProjectionBanner = settingEnabled(trackerSettings.tracker_show_bank_projection_banner) &&
|
||||
(!bannerSnoozedUntil || bannerSnoozedUntil <= today);
|
||||
|
|
@ -848,7 +849,7 @@ export default function TrackerPage() {
|
|||
) : null}
|
||||
|
||||
{/* ── Safe to Spend ── */}
|
||||
{!isError && !loading && isCurrentMonth && cashflow && (
|
||||
{!isError && !loading && showSafeToSpend && isCurrentMonth && cashflow && (
|
||||
<CashFlowCard
|
||||
cashflow={cashflow}
|
||||
onSetStartingAmounts={() => setEditStartingOpen(true)}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const USER_SETTING_KEYS = [
|
|||
'tracker_bank_projection_banner_snoozed_until',
|
||||
'tracker_show_search_sort',
|
||||
'tracker_show_summary_cards',
|
||||
'tracker_show_safe_to_spend',
|
||||
'tracker_show_overdue_command_center',
|
||||
'tracker_show_drift_insights',
|
||||
'tracker_table_columns',
|
||||
|
|
@ -28,6 +29,7 @@ const USER_SETTING_DEFAULTS = {
|
|||
tracker_bank_projection_banner_snoozed_until: '',
|
||||
tracker_show_search_sort: 'true',
|
||||
tracker_show_summary_cards: 'true',
|
||||
tracker_show_safe_to_spend: 'true',
|
||||
tracker_show_overdue_command_center: 'true',
|
||||
tracker_show_drift_insights: 'true',
|
||||
tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]',
|
||||
|
|
|
|||
Loading…
Reference in New Issue