feat(profile): add user profile management with first and last name fields
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, FormEvent } from 'react';
|
||||
|
||||
type Profile = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
};
|
||||
|
||||
export default function ProfileClient() {
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/profile')
|
||||
.then((r) => r.json())
|
||||
.then((data: Profile) => {
|
||||
setProfile(data);
|
||||
setForm({
|
||||
firstName: data.firstName ?? '',
|
||||
lastName: data.lastName ?? '',
|
||||
email: data.email,
|
||||
});
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch('/api/profile', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
firstName: form.firstName || null,
|
||||
lastName: form.lastName || null,
|
||||
email: form.email,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.message ?? 'Kunde inte spara ändringar');
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
} catch {
|
||||
setError('Något gick fel');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #ddd',
|
||||
fontSize: '1rem',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
marginBottom: 6,
|
||||
fontWeight: 500,
|
||||
fontSize: '0.9rem',
|
||||
color: '#444',
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <p style={{ color: '#666' }}>Laddar profil...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
{/* Initialer/avatar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: '50%',
|
||||
background: '#2563eb',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 700,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{profile?.username?.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '1.1rem' }}>{profile?.username}</div>
|
||||
<div style={{ color: '#666', fontSize: '0.9rem' }}>{profile?.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div>
|
||||
<label htmlFor="firstName" style={labelStyle}>Förnamn</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
value={form.firstName}
|
||||
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
||||
style={inputStyle}
|
||||
maxLength={100}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" style={labelStyle}>Efternamn</label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
value={form.lastName}
|
||||
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
|
||||
style={inputStyle}
|
||||
maxLength={100}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" style={labelStyle}>E-post</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
||||
required
|
||||
style={inputStyle}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Användarnamn</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profile?.username ?? ''}
|
||||
disabled
|
||||
style={{ ...inputStyle, background: '#f5f5f5', color: '#888', cursor: 'not-allowed' }}
|
||||
/>
|
||||
<p style={{ fontSize: '0.8rem', color: '#999', margin: '4px 0 0' }}>
|
||||
Användarnamn kan inte ändras
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p style={{ color: '#dc2626', margin: 0 }}>{error}</p>}
|
||||
{success && <p style={{ color: '#16a34a', margin: 0 }}>Ändringarna sparades!</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: '10px',
|
||||
background: '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontSize: '1rem',
|
||||
cursor: saving ? 'not-allowed' : 'pointer',
|
||||
opacity: saving ? 0.7 : 1,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{saving ? 'Sparar...' : 'Spara ändringar'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user