Initial commit

This commit is contained in:
2026-07-22 14:08:50 +01:00
commit 16e7684f3b
32 changed files with 6508 additions and 0 deletions

92
src/pages/ProfilePage.tsx Normal file
View File

@@ -0,0 +1,92 @@
import { AtSign, KeyRound, Save, ShieldCheck, UserRound } from 'lucide-react'
import { FormEvent, useState } from 'react'
import { Spinner } from '../components/ui'
import { useAuth } from '../context/AuthContext'
import { useToast } from '../context/ToastContext'
import { api } from '../lib/api'
import { displayName, getInitials } from '../lib/format'
export function ProfilePage() {
const { user, refreshProfile } = useAuth()
const { showToast } = useToast()
const [firstName, setFirstName] = useState(user?.firstName ?? '')
const [lastName, setLastName] = useState(user?.lastName ?? '')
const [email, setEmail] = useState(user?.email ?? '')
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [savingProfile, setSavingProfile] = useState(false)
const [savingPassword, setSavingPassword] = useState(false)
const [profileError, setProfileError] = useState('')
const [passwordError, setPasswordError] = useState('')
const handleProfile = async (event: FormEvent) => {
event.preventDefault()
setSavingProfile(true)
setProfileError('')
try {
await api.updateProfile({ email: email.trim(), firstName: firstName.trim(), lastName: lastName.trim() })
await refreshProfile()
showToast('success', 'Profile saved', 'Your account details are up to date.')
} catch (caught) {
setProfileError(caught instanceof Error ? caught.message : 'Your profile could not be saved.')
} finally {
setSavingProfile(false)
}
}
const handlePassword = async (event: FormEvent) => {
event.preventDefault()
setSavingPassword(true)
setPasswordError('')
try {
await api.updateProfile({ currentPassword, newPassword })
setCurrentPassword('')
setNewPassword('')
showToast('success', 'Password updated', 'Use your new password the next time you sign in.')
} catch (caught) {
setPasswordError(caught instanceof Error ? caught.message : 'Your password could not be changed.')
} finally {
setSavingPassword(false)
}
}
const isAdmin = user?.roles.some((role) => role === 'Site Admin' || role === 'Admin')
return (
<div className="page page--profile">
<header className="page-header"><div><p className="eyebrow">Your Keeply account</p><h1>Profile & settings</h1><p>Keep your personal details and sign-in information current.</p></div></header>
<div className="profile-layout">
<aside className="profile-summary-card">
<span className="avatar avatar--profile">{getInitials(user?.firstName, user?.lastName, user?.email)}</span>
<h2>{displayName(user?.firstName, user?.lastName, user?.email)}</h2>
<p>{user?.email}</p>
<span className={`role-label ${isAdmin ? 'role-label--admin' : ''}`}>{isAdmin ? <ShieldCheck size={14} /> : <UserRound size={14} />}{isAdmin ? 'Site administrator' : 'Pantry member'}</span>
<div className="profile-summary-card__meta"><span>Account ID</span><code>{user?.id}</code></div>
</aside>
<div className="settings-stack">
<section className="settings-card">
<header><span><UserRound size={20} /></span><div><h2>Personal details</h2><p>Used across your household and pantry.</p></div></header>
<form onSubmit={(event) => void handleProfile(event)}>
<div className="settings-card__body">
<div className="form-row"><label className="field"><span>First name</span><input value={firstName} onChange={(event) => setFirstName(event.target.value)} required /></label><label className="field"><span>Last name</span><input value={lastName} onChange={(event) => setLastName(event.target.value)} required /></label></div>
<label className="field"><span>Email address</span><div className="input-with-icon"><AtSign size={17} /><input type="email" value={email} onChange={(event) => setEmail(event.target.value)} required /></div></label>
{profileError && <div className="form-error" role="alert">{profileError}</div>}
</div>
<footer><button className="button button--primary" disabled={savingProfile}>{savingProfile ? <Spinner /> : <Save size={17} />}Save details</button></footer>
</form>
</section>
<section className="settings-card">
<header><span><KeyRound size={20} /></span><div><h2>Change password</h2><p>Choose at least 8 characters with upper, lower and a number.</p></div></header>
<form onSubmit={(event) => void handlePassword(event)}>
<div className="settings-card__body"><label className="field"><span>Current password</span><input type="password" value={currentPassword} onChange={(event) => setCurrentPassword(event.target.value)} autoComplete="current-password" required /></label><label className="field"><span>New password</span><input type="password" value={newPassword} onChange={(event) => setNewPassword(event.target.value)} autoComplete="new-password" minLength={8} required /></label>{passwordError && <div className="form-error" role="alert">{passwordError}</div>}</div>
<footer><button className="button button--secondary" disabled={savingPassword}>{savingPassword && <Spinner />}Update password</button></footer>
</form>
</section>
</div>
</div>
</div>
)
}