Files
resolutionflow/frontend/src/pages/account/ProfileSettingsPage.tsx
Michael Chihlas f3fbc5142c refactor: replace hardcoded rgba/hex colors with Tailwind tokens
- rgba(255,255,255,0.xx) → bg-white/[0.xx], border-white/[0.xx]
- rgba(6,182,212,0.3) → border-primary/30 (focus states)
- #0a0a0a → bg-background
- Inline style hex colors → var(--color-primary), var(--color-brand-gradient-to)
- 28 files updated, zero hardcoded rgba() patterns remaining

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:35:51 -05:00

185 lines
6.8 KiB
TypeScript

import { useState } from 'react'
import { Link } from 'react-router-dom'
import { User as UserIcon, Loader2, AlertCircle, Check } from 'lucide-react'
import { authApi } from '@/api/auth'
import { useAuthStore } from '@/store/authStore'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import type { UserUpdate } from '@/types'
const inputClass = cn(
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)
export function ProfileSettingsPage() {
const user = useAuthStore((s) => s.user)
const fetchUser = useAuthStore((s) => s.fetchUser)
const [name, setName] = useState(user?.name ?? '')
const [email, setEmail] = useState(user?.email ?? '')
const [phone, setPhone] = useState(user?.phone ?? '')
const [jobTitle, setJobTitle] = useState(user?.job_title ?? '')
const [timezone, setTimezone] = useState(user?.timezone ?? 'UTC')
const [currentPassword, setCurrentPassword] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const emailChanged = email !== user?.email
const hasChanges =
emailChanged ||
name !== user?.name ||
phone !== (user?.phone ?? '') ||
jobTitle !== (user?.job_title ?? '') ||
timezone !== (user?.timezone ?? 'UTC')
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
if (!hasChanges) return
setIsSaving(true)
setError(null)
try {
const payload: UserUpdate = {}
if (name !== user?.name) payload.name = name.trim()
if (emailChanged) {
payload.email = email.trim()
payload.current_password = currentPassword
}
if (phone !== (user?.phone ?? '')) payload.phone = phone.trim() || null
if (jobTitle !== (user?.job_title ?? '')) payload.job_title = jobTitle.trim() || null
if (timezone !== (user?.timezone ?? 'UTC')) payload.timezone = timezone
await authApi.updateProfile(payload)
await fetchUser()
setCurrentPassword('')
toast.success('Profile updated')
} catch (err) {
const axiosErr = err as { response?: { data?: { detail?: string } } }
setError(axiosErr.response?.data?.detail ?? 'Failed to update profile')
} finally {
setIsSaving(false)
}
}
return (
<div>
<div className="mb-8">
<div className="flex items-center gap-3">
<UserIcon className="h-8 w-8 text-muted-foreground" />
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">Profile Settings</h1>
</div>
<p className="mt-2 text-muted-foreground">
Update your name, email, and personal details
</p>
</div>
<div className="max-w-xl">
<form onSubmit={handleSave} className="glass-card-static p-6 space-y-5">
{/* Name */}
<div>
<label htmlFor="profile-name" className="block text-sm font-medium text-foreground">Name</label>
<input id="profile-name" type="text" value={name} onChange={(e) => setName(e.target.value)} required className={inputClass} />
</div>
{/* Email */}
<div>
<label htmlFor="profile-email" className="block text-sm font-medium text-foreground">Email</label>
<input id="profile-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className={inputClass} />
</div>
{/* Password confirmation for email change */}
{emailChanged && (
<div>
<label htmlFor="profile-password" className="block text-sm font-medium text-foreground">Current Password</label>
<p className="text-xs text-muted-foreground">Required to change your email address</p>
<input id="profile-password" type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required className={inputClass} />
</div>
)}
{/* Phone */}
<div>
<label htmlFor="profile-phone" className="block text-sm font-medium text-foreground">Phone</label>
<input id="profile-phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Optional" className={inputClass} />
</div>
{/* Job Title */}
<div>
<label htmlFor="profile-job-title" className="block text-sm font-medium text-foreground">Job Title</label>
<input id="profile-job-title" type="text" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} placeholder="e.g. Network Engineer" className={inputClass} />
</div>
{/* Timezone */}
<div>
<label htmlFor="profile-timezone" className="block text-sm font-medium text-foreground">Timezone</label>
<select id="profile-timezone" value={timezone} onChange={(e) => setTimezone(e.target.value)} className={inputClass}>
{COMMON_TIMEZONES.map((tz) => (
<option key={tz} value={tz}>{tz}</option>
))}
</select>
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-rose-500">
<AlertCircle className="h-4 w-4 shrink-0" />
{error}
</div>
)}
<div className="flex items-center gap-3 pt-2">
<button
type="submit"
disabled={isSaving || !hasChanges}
className={cn(
'inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-brand-dark',
'shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97]',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
Save Changes
</button>
<Link
to="/change-password"
className={cn(
'inline-flex items-center rounded-[10px] px-4 py-2 text-sm font-medium',
'bg-white/[0.04] border border-brand-border text-foreground',
'hover:border-white/[0.12]'
)}
>
Change Password
</Link>
</div>
</form>
</div>
</div>
)
}
const COMMON_TIMEZONES = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'Pacific/Honolulu',
'America/Toronto',
'America/Vancouver',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Amsterdam',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Kolkata',
'Asia/Dubai',
'Australia/Sydney',
'Australia/Melbourne',
'Pacific/Auckland',
]
export default ProfileSettingsPage