refactor(ts): SettingsPage → TypeScript (auto-save settings + tracker layout toggles)

TS flagged a dead prop: onSaved was passed to NotificationPreferences (which only
takes settings) — removed it. Notification components imported from ProfilePage
(still .jsx) type as any until that page converts.
This commit is contained in:
null 2026-07-04 22:29:16 -05:00
parent b912fd74be
commit 18d0c0bdb9
1 changed files with 42 additions and 21 deletions

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, type ComponentType, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { AlertCircle, Moon, RefreshCw, Sun, Users } from 'lucide-react';
import { api } from '@/api';
import { cn } from '@/lib/utils';
import { cn, errMessage } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { CalendarFeedManager } from '@/components/CalendarFeedManager';
@ -18,7 +18,7 @@ import { SaveStatus } from '@/components/ui/save-status';
import { NotificationPreferences, PushNotifications, asSettings } from '@/pages/ProfilePage';
export const LINK_IMPORT_PREF_KEY = 'link_import_ask';
export function getLinkImportPref() {
export function getLinkImportPref(): boolean {
return localStorage.getItem(LINK_IMPORT_PREF_KEY) !== 'false';
}
@ -26,12 +26,12 @@ export function getLinkImportPref() {
// preferences; Profile keeps identity, security, and privacy) ──────────────
function NotificationsSection() {
const [settings, setSettings] = useState(null);
const [settings, setSettings] = useState<Record<string, unknown> | null>(null);
const load = useCallback(() => {
api.profileSettings()
.then((data) => setSettings(asSettings(data)))
.catch((err) => toast.error(err.message || 'Failed to load notification settings.'));
.catch((err) => toast.error(errMessage(err, 'Failed to load notification settings.')));
}, []);
useEffect(() => { load(); }, [load]);
@ -39,7 +39,7 @@ function NotificationsSection() {
if (!settings) return null;
return (
<div className="space-y-4 mb-4">
<NotificationPreferences settings={settings} onSaved={setSettings} />
<NotificationPreferences settings={settings} />
<PushNotifications settings={settings} onSaved={load} />
</div>
);
@ -47,7 +47,7 @@ function NotificationsSection() {
// ─── Card wrapper ─────────────────────────────────────────────────────────────
function SectionCard({ title, children }) {
function SectionCard({ title, children }: { title: ReactNode; children: ReactNode }) {
return (
<div className="table-surface mb-4">
<div className="px-6 py-4 border-b border-border/50">
@ -62,7 +62,7 @@ function SectionCard({ title, children }) {
// ─── Setting Row ──────────────────────────────────────────────────────────────
function SettingRow({ label, description, children }) {
function SettingRow({ label, description, children }: { label: ReactNode; description?: ReactNode; children: ReactNode }) {
return (
<div className="px-4 py-4 flex flex-col gap-3 sm:px-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex-1 min-w-0 sm:mr-8">
@ -80,7 +80,13 @@ function SettingRow({ label, description, children }) {
// ─── Theme Card ───────────────────────────────────────────────────────────────
function ThemeCard({ value, label, icon: Icon, currentTheme, onSelect }) {
function ThemeCard({ value, label, icon: Icon, currentTheme, onSelect }: {
value: string;
label: string;
icon: ComponentType<{ className?: string }>;
currentTheme: string;
onSelect: (v: string) => void;
}) {
const selected = currentTheme === value;
return (
<button
@ -132,7 +138,7 @@ function LoginModeRecoverySection() {
refresh();
navigate('/login', { replace: true });
} catch (err) {
toast.error(err.message || 'Failed to restore multi-user mode.');
toast.error(errMessage(err, 'Failed to restore multi-user mode.'));
} finally {
setRestoring(false);
}
@ -246,7 +252,7 @@ function SettingsSkeleton() {
function LinkImportToggle() {
const [enabled, setEnabled] = useState(getLinkImportPref);
function toggle(next) {
function toggle(next: boolean) {
localStorage.setItem(LINK_IMPORT_PREF_KEY, String(next));
setEnabled(next);
toast.success(next
@ -264,15 +270,30 @@ function LinkImportToggle() {
);
}
function settingsBool(value, fallback = true) {
function settingsBool(value: unknown, fallback = true): boolean {
if (value === undefined || value === null || value === '') return fallback;
return value === true || value === 'true';
}
interface SettingsState {
currency: string;
date_format: string;
grace_period_days: number | string;
drift_threshold_pct: string;
tracker_show_bank_projection_banner: string;
tracker_bank_projection_banner_snoozed_until: string;
tracker_show_search_sort: string;
tracker_show_summary_cards: string;
tracker_show_safe_to_spend: string;
tracker_show_overdue_command_center: string;
tracker_show_drift_insights: string;
[key: string]: unknown;
}
// ─── SettingsPage ─────────────────────────────────────────────────────────────
export default function SettingsPage() {
const DEFAULTS = {
const DEFAULTS: SettingsState = {
currency: 'USD',
date_format: 'MM/DD/YYYY',
grace_period_days: 3,
@ -286,22 +307,22 @@ export default function SettingsPage() {
tracker_show_drift_insights: 'true',
};
const [settings, setSettings] = useState(DEFAULTS);
const [settings, setSettings] = useState<SettingsState>(DEFAULTS);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
const [loadError, setLoadError] = useState<string | null>(null);
const loadSettings = useCallback(() => {
setLoading(true);
setLoadError(null);
api.settings()
.then((d) => setSettings({ ...DEFAULTS, ...d }))
.catch((err) => setLoadError(err.message || 'Failed to load settings'))
.then((d) => setSettings({ ...DEFAULTS, ...(d as Partial<SettingsState>) }))
.catch((err) => setLoadError(errMessage(err, 'Failed to load settings')))
.finally(() => setLoading(false));
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { loadSettings(); }, [loadSettings]);
const buildPayload = (s) => ({
const buildPayload = (s: SettingsState) => ({
currency: s.currency,
date_format: s.date_format,
grace_period_days: s.grace_period_days,
@ -320,17 +341,17 @@ export default function SettingsPage() {
// don't save half-typed numbers.
const { status: saveStatus, schedule } = useAutoSave(
(payload) => api.saveSettings(payload).catch((err) => {
toast.error(err.message || 'Failed to save settings.');
toast.error(errMessage(err, 'Failed to save settings.'));
throw err;
}),
);
const set = (k, v, delay) => setSettings((p) => {
const set = (k: string, v: unknown, delay?: number) => setSettings((p) => {
const next = { ...p, [k]: v };
schedule(buildPayload(next), delay);
return next;
});
const setTyped = (k, v) => set(k, v, 900); // for keystroke-driven inputs
const setTyped = (k: string, v: unknown) => set(k, v, 900); // for keystroke-driven inputs
if (loading) {
return (