diff --git a/client/App.jsx b/client/App.tsx similarity index 97% rename from client/App.jsx rename to client/App.tsx index e78c76f..19ef91a 100644 --- a/client/App.jsx +++ b/client/App.tsx @@ -1,5 +1,4 @@ - -import { lazy, Suspense, useId } from 'react'; +import { lazy, Suspense, useId, type ReactNode } from 'react'; import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import Layout from '@/components/layout/Layout'; @@ -61,7 +60,7 @@ const PayoffPage = lazy(() => import('@/pages/PayoffPage')); const SpendingPage = lazy(() => import('@/pages/SpendingPage')); const BankTransactionsPage = lazy(() => import('@/pages/BankTransactionsPage')); -function RequireAuth({ children, role }) { +function RequireAuth({ children, role }: { children: ReactNode; role?: string }): ReactNode { const { user, singleUserMode } = useAuth(); const location = useLocation(); @@ -96,7 +95,7 @@ function RequireAuth({ children, role }) { return children; } -function AdminShell({ children }) { +function AdminShell({ children }: { children: ReactNode }) { const location = useLocation(); return ( @@ -220,7 +219,7 @@ export default function App() { } > - }>} /> + }>} /> }>} /> }>} /> }>} /> diff --git a/client/components/layout/BrandBlock.jsx b/client/components/layout/BrandBlock.tsx similarity index 94% rename from client/components/layout/BrandBlock.jsx rename to client/components/layout/BrandBlock.tsx index 7324aed..57d7881 100644 --- a/client/components/layout/BrandBlock.jsx +++ b/client/components/layout/BrandBlock.tsx @@ -1,10 +1,9 @@ import React, { useMemo } from 'react'; import { NavLink } from 'react-router-dom'; -import { cn } from '@/lib/utils'; -export const BrandBlock = React.memo(function BrandBlock({ adminMode = false }) { +export const BrandBlock = React.memo(function BrandBlock({ adminMode = false }: { adminMode?: boolean }) { const logoSrc = useMemo(() => '/img/logo.png', []); - + return ( { api.simplefinStatus() - .then(d => setEnabled(!!d.enabled)) + .then(d => setEnabled(!!(d as { enabled?: boolean }).enabled)) .catch(err => console.error('[Layout] simplefinStatus check failed', err)); }, []); @@ -23,7 +23,7 @@ function SimplefinBadge() { ); } -export default function Layout({ mainContentId }) { +export default function Layout({ mainContentId }: { mainContentId?: string }) { const location = useLocation(); return ( diff --git a/client/components/layout/NavPill.jsx b/client/components/layout/NavPill.tsx similarity index 86% rename from client/components/layout/NavPill.jsx rename to client/components/layout/NavPill.tsx index 6aa98cb..dfd05e9 100644 --- a/client/components/layout/NavPill.jsx +++ b/client/components/layout/NavPill.tsx @@ -1,9 +1,24 @@ import React, { useMemo } from 'react'; import { NavLink } from 'react-router-dom'; +import { type LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -export const NavPill = React.memo(function NavPill({ item, onNavigate, badge, badgeNames = [] }) { +export interface NavItem { + icon: LucideIcon; + to: string; + end?: boolean; + label: string; +} + +interface NavPillProps { + item: NavItem; + onNavigate?: () => void; + badge?: number; + badgeNames?: string[]; +} + +export const NavPill = React.memo(function NavPill({ item, onNavigate, badge = 0, badgeNames = [] }: NavPillProps) { const Icon = useMemo(() => item.icon, [item.icon]); const to = useMemo(() => item.to, [item.to]); const end = useMemo(() => item.end, [item.end]); diff --git a/client/components/layout/Sidebar.jsx b/client/components/layout/Sidebar.tsx similarity index 89% rename from client/components/layout/Sidebar.jsx rename to client/components/layout/Sidebar.tsx index a271073..129e9ac 100644 --- a/client/components/layout/Sidebar.jsx +++ b/client/components/layout/Sidebar.tsx @@ -3,7 +3,7 @@ import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { Activity, BarChart3, Calculator, CalendarDays, ChevronDown, ClipboardCheck, ClipboardList, Database, Info, LayoutGrid, LogOut, Map, Menu, Receipt, Search, Settings, ShieldCheck, Tag, TrendingDown, User, X, - Landmark, Repeat, ShoppingCart, + Landmark, Repeat, ShoppingCart, type LucideIcon, } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; @@ -23,19 +23,28 @@ import { import { NavPill } from './NavPill'; import { BrandBlock } from './BrandBlock'; -const userNavItems = [ +interface SidebarNavItem { + to: string; + icon: LucideIcon; + label: string; + end?: boolean; + simplefinOnly?: boolean; + accountToolsOnly?: boolean; +} + +const userNavItems: SidebarNavItem[] = [ { to: '/calendar', icon: CalendarDays, label: 'Calendar' }, { to: '/analytics', icon: BarChart3, label: 'Analytics' }, ]; -const adminNavItems = [ +const adminNavItems: SidebarNavItem[] = [ { to: '/', icon: LayoutGrid, label: 'Tracker', end: true }, { to: '/admin', icon: ShieldCheck, label: 'Admin Panel', end: true }, { to: '/admin/status', icon: Activity, label: 'System Status' }, { to: '/roadmap', icon: Map, label: 'Roadmap' }, ]; -const trackerItems = [ +const trackerItems: SidebarNavItem[] = [ { to: '/', icon: LayoutGrid, label: 'Overview', end: true }, { to: '/summary', icon: ClipboardList, label: 'Summary' }, { to: '/bills', icon: Receipt, label: 'Bills' }, @@ -49,7 +58,14 @@ const trackerItems = [ { to: '/data', icon: Database, label: 'Data', accountToolsOnly: true }, ]; -function TrackerMenu({ onNavigate, badge, badgeNames = [], items = trackerItems }) { +interface TrackerMenuProps { + onNavigate?: () => void; + badge?: number; + badgeNames?: string[]; + items?: SidebarNavItem[]; +} + +function TrackerMenu({ onNavigate, badge = 0, badgeNames = [], items = trackerItems }: TrackerMenuProps) { const location = useLocation(); const navigate = useNavigate(); const isTrackerActive = useMemo(() => items.some(item => ( @@ -110,18 +126,18 @@ function TrackerMenu({ onNavigate, badge, badgeNames = [], items = trackerItems ); } -function UserMenu({ adminMode = false }) { +function UserMenu({ adminMode = false }: { adminMode?: boolean }) { const { user, logout } = useAuth(); const navigate = useNavigate(); - const name = useMemo(() => - user?.display_name || user?.displayName || user?.name || user?.username || (adminMode ? 'Admin' : 'Profile'), + const name = useMemo(() => + user?.display_name || user?.displayName || user?.name || user?.username || (adminMode ? 'Admin' : 'Profile'), [user, adminMode] ); const accountToolsAllowed = useMemo(() => !user?.is_default_admin, [user]); const userRole = useMemo(() => user?.role, [user]); const handleLogout = async () => { - try { await logout(); } catch {} + try { await logout(); } catch { /* ignore */ } navigate('/login', { replace: true }); }; @@ -188,14 +204,15 @@ function UserMenu({ adminMode = false }) { ); } -export default function Sidebar({ adminMode = false }) { +export default function Sidebar({ adminMode = false }: { adminMode?: boolean; mainContentId?: string }) { const [mobileOpen, setMobileOpen] = useState(false); const { user } = useAuth(); const items = useMemo(() => adminMode ? adminNavItems : userNavItems, [adminMode]); const [simplefinReady, setSimplefinReady] = useState(false); const { data: overdueData } = useOverdueCount(); - const overdueCount = (!adminMode && overdueData?.count) ? overdueData.count : 0; - const overdueNames = (!adminMode && overdueData?.names) ? overdueData.names : []; + const od = overdueData as { count?: number; names?: string[] } | undefined; + const overdueCount = !adminMode ? (od?.count ?? 0) : 0; + const overdueNames = !adminMode ? (od?.names ?? []) : []; const accountToolsAllowed = !user?.is_default_admin; const trackerMenuItems = useMemo( () => trackerItems.filter(item => ( @@ -210,7 +227,8 @@ export default function Sidebar({ adminMode = false }) { let cancelled = false; api.simplefinStatus() .then(status => { - if (!cancelled) setSimplefinReady(Boolean(status?.enabled && status?.has_connections)); + const s = status as { enabled?: boolean; has_connections?: boolean }; + if (!cancelled) setSimplefinReady(Boolean(s?.enabled && s?.has_connections)); }) .catch(() => { if (!cancelled) setSimplefinReady(false); diff --git a/client/hooks/useAuth.jsx b/client/hooks/useAuth.jsx deleted file mode 100644 index e2fd33d..0000000 --- a/client/hooks/useAuth.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react'; -import { api } from '@/api'; - -const AuthContext = createContext(null); - -export function AuthProvider({ children }) { - const [user, setUser] = useState(undefined); // undefined = loading - const [singleUserMode, setSUM] = useState(false); - const [hasNewVersion, setHasNewVersion] = useState(false); - - function applyMeResponse(d) { - setUser(d.user); - setSUM(d.single_user_mode || false); - setHasNewVersion(!!d.has_new_version); - } - - useEffect(() => { - api.authMode().then(d => { - if (d.auth_mode === 'single') setSUM(true); - }).catch(err => console.error('[useAuth] authMode check failed', err)); - - api.me().then(applyMeResponse).catch(() => setUser(null)); - }, []); - - const logout = async () => { - await api.logout(); - setUser(null); - setSUM(false); - setHasNewVersion(false); - }; - - const refresh = () => { - api.me().then(applyMeResponse).catch(() => { - setUser(null); - setSUM(false); - }); - }; - - return ( - - {children} - - ); -} - -export function useAuth() { - return useContext(AuthContext) || { - user: null, setUser: () => {}, logout: () => {}, refresh: () => {}, - singleUserMode: false, hasNewVersion: false, setHasNewVersion: () => {}, - }; -} diff --git a/client/hooks/useAuth.tsx b/client/hooks/useAuth.tsx new file mode 100644 index 0000000..2ae6e5f --- /dev/null +++ b/client/hooks/useAuth.tsx @@ -0,0 +1,82 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; +import { api } from '@/api'; + +export interface User { + id: number; + username?: string; + role?: string; + display_name?: string; + displayName?: string; + name?: string; + is_default_admin?: boolean; + [key: string]: unknown; +} + +interface MeResponse { + user?: User | null; + single_user_mode?: boolean; + has_new_version?: boolean; +} + +interface AuthContextValue { + user: User | null | undefined; // undefined = loading + singleUserMode: boolean; + setUser: (user: User | null | undefined) => void; + logout: () => void | Promise; + refresh: () => void; + hasNewVersion: boolean; + setHasNewVersion: (v: boolean) => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(undefined); // undefined = loading + const [singleUserMode, setSUM] = useState(false); + const [hasNewVersion, setHasNewVersion] = useState(false); + + function applyMeResponse(raw: unknown) { + const d = raw as MeResponse; + setUser(d.user); + setSUM(d.single_user_mode || false); + setHasNewVersion(!!d.has_new_version); + } + + useEffect(() => { + api.authMode().then(d => { + if ((d as { auth_mode?: string }).auth_mode === 'single') setSUM(true); + }).catch(err => console.error('[useAuth] authMode check failed', err)); + + api.me().then(applyMeResponse).catch(() => setUser(null)); + }, []); + + const logout = async () => { + await api.logout(); + setUser(null); + setSUM(false); + setHasNewVersion(false); + }; + + const refresh = () => { + api.me().then(applyMeResponse).catch(() => { + setUser(null); + setSUM(false); + }); + }; + + return ( + + {children} + + ); +} + +export function useAuth(): AuthContextValue { + return useContext(AuthContext) || { + user: null, setUser: () => {}, logout: () => {}, refresh: () => {}, + singleUserMode: false, hasNewVersion: false, setHasNewVersion: () => {}, + }; +} diff --git a/client/main.jsx b/client/main.tsx similarity index 90% rename from client/main.jsx rename to client/main.tsx index 5b65e64..11d7325 100644 --- a/client/main.jsx +++ b/client/main.tsx @@ -8,7 +8,7 @@ import { ThemeProvider } from './contexts/ThemeContext'; import 'sonner/dist/styles.css'; import './index.css'; -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/index.html b/index.html index 7110bd8..39c907e 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,6 @@
- +