diff --git a/client/hooks/useAuth.tsx b/client/hooks/useAuth.tsx index 2ae6e5f..587abdf 100644 --- a/client/hooks/useAuth.tsx +++ b/client/hooks/useAuth.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useState, type Dispatch, type SetStateAction, type ReactNode } from 'react'; import { api } from '@/api'; export interface User { @@ -21,7 +21,7 @@ interface MeResponse { interface AuthContextValue { user: User | null | undefined; // undefined = loading singleUserMode: boolean; - setUser: (user: User | null | undefined) => void; + setUser: Dispatch>; logout: () => void | Promise; refresh: () => void; hasNewVersion: boolean; diff --git a/client/pages/ProfilePage.jsx b/client/pages/ProfilePage.tsx similarity index 85% rename from client/pages/ProfilePage.jsx rename to client/pages/ProfilePage.tsx index a72d9b7..10a318d 100644 --- a/client/pages/ProfilePage.jsx +++ b/client/pages/ProfilePage.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, type ComponentType, type ReactNode } from 'react'; import { toast } from 'sonner'; import { User, Mail, KeyRound, ShieldCheck, Loader2, History, Monitor, Smartphone, ChevronRight, Bell, SendHorizontal, ScanLine, TriangleAlert, Copy, Check, Lock, } from 'lucide-react'; import { api } from '@/api'; -import { useAuth } from '@/hooks/useAuth'; +import { errMessage } from '@/lib/utils'; +import { useAuth, type User as AuthUser } from '@/hooks/useAuth'; import { useAutoSave } from '@/hooks/useAutoSave'; import { SaveStatus } from '@/components/ui/save-status'; import { Button } from '@/components/ui/button'; @@ -15,28 +16,66 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; -function asProfile(data) { - return data?.profile || data?.user || data || {}; +type Settings = Record; + +interface Profile { + username?: string; + display_name?: string; + displayName?: string; + name?: string; + role?: string; + last_password_change_at?: string; + password_changed_at?: string; + [key: string]: unknown; } -function displayNameOf(profile) { +interface LoginEntry { + id: number | string; + success?: boolean; + user_agent?: string; + browser?: string; + os?: string; + device_type?: string; + logged_in_at?: string; + is_current_session?: boolean; + ip_address?: string; + location_city?: string; + location_region?: string; + location_country?: string; + location_isp?: string; + device_fingerprint?: string; +} + +function asProfile(data: unknown): Profile { + const d = data as { profile?: Profile; user?: Profile } | Profile | null | undefined; + return ((d as { profile?: Profile })?.profile || (d as { user?: Profile })?.user || (d as Profile) || {}) as Profile; +} + +function displayNameOf(profile: Profile): string { return profile.display_name || profile.displayName || profile.name || ''; } -export function asSettings(data) { - return data?.settings || data?.notifications || data || {}; +export function asSettings(data: unknown): Settings { + const d = data as { settings?: Settings; notifications?: Settings } | Settings | null | undefined; + return ((d as { settings?: Settings })?.settings || (d as { notifications?: Settings })?.notifications || (d as Settings) || {}) as Settings; } -function formatDateTime(value) { +function formatDateTime(value: unknown): string { if (!value) return 'Not recorded'; - const d = new Date(value); + const d = new Date(value as string | number); if (Number.isNaN(d.getTime())) return String(value); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } -function SectionCard({ title, icon: Icon, subtitle, action, children }) { +function SectionCard({ title, icon: Icon, subtitle, action, children }: { + title: ReactNode; + icon: ComponentType<{ className?: string }>; + subtitle?: ReactNode; + action?: ReactNode; + children: ReactNode; +}) { return (
@@ -54,7 +93,7 @@ function SectionCard({ title, icon: Icon, subtitle, action, children }) { ); } -function FieldRow({ label, value }) { +function FieldRow({ label, value }: { label: ReactNode; value?: ReactNode }) { return (

{label}

@@ -63,7 +102,13 @@ function FieldRow({ label, value }) { ); } -function CheckRow({ id, label, checked, onChange, disabled }) { +function CheckRow({ id, label, checked, onChange, disabled }: { + id: string; + label: ReactNode; + checked?: boolean; + onChange?: (v: boolean) => void; + disabled?: boolean; +}) { return (