diff --git a/client/index.css b/client/index.css index a0c2cb6..a3cc945 100644 --- a/client/index.css +++ b/client/index.css @@ -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 */ diff --git a/client/pages/ProfilePage.jsx b/client/pages/ProfilePage.jsx index a806d4d..ee8ceec 100644 --- a/client/pages/ProfilePage.jsx +++ b/client/pages/ProfilePage.jsx @@ -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() {

Profile

-

Manage your account, notification preferences, and password.

+

Manage your account, security, and privacy. Notification preferences live in Settings.

@@ -968,10 +970,7 @@ export default function ProfilePage() {
-
- {!loading && } - {!loading && api.profileSettings().then(setSettings).catch(() => {})} />} -
+ {/* Notification preferences moved to Settings → Notifications */}
{!loading && }
diff --git a/client/pages/SettingsPage.jsx b/client/pages/SettingsPage.jsx index 74cfc00..745f3a9 100644 --- a/client/pages/SettingsPage.jsx +++ b/client/pages/SettingsPage.jsx @@ -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 ( +
+ + +
+ ); +} + // ─── 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 */}

Settings

-

Manage your display and billing preferences

+

Manage your display, billing, and notification preferences

{/* Appearance */} @@ -400,6 +426,16 @@ export default function SettingsPage() { aria-label="Show Summary cards" /> + + set('tracker_show_safe_to_spend', String(checked))} + aria-label="Show Safe to Spend" + /> +
+ {/* Notifications — email + push reminder preferences (save independently) */} +
+ +
+
diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 6daa11e..45f71a7 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -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 && ( setEditStartingOpen(true)} diff --git a/services/userSettings.js b/services/userSettings.js index 942ac3f..fd5c04d 100644 --- a/services/userSettings.js +++ b/services/userSettings.js @@ -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"]',