feat: Add optional display name to user profile with corresponding API and UI updates
This commit is contained in:
@@ -28,6 +28,7 @@ pub struct CreateUserRequest {
|
||||
/// Request to update a user.
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUserRequest {
|
||||
pub name: Option<String>,
|
||||
pub height_cm: Option<f32>,
|
||||
pub blood_type: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
@@ -41,6 +42,7 @@ pub struct UpdateUserRequest {
|
||||
pub struct UserResponse {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub name: Option<String>,
|
||||
pub role: String,
|
||||
pub height_cm: Option<f32>,
|
||||
pub blood_type: Option<String>,
|
||||
@@ -85,6 +87,7 @@ pub async fn list_users(
|
||||
.map(|u| UserResponse {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
role: role_map.get(&u.role_id).cloned().unwrap_or_default(),
|
||||
height_cm: u.height_cm,
|
||||
blood_type: u.blood_type,
|
||||
@@ -130,6 +133,7 @@ pub async fn get_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
role: role_name,
|
||||
height_cm: u.height_cm,
|
||||
blood_type: u.blood_type,
|
||||
@@ -211,6 +215,7 @@ pub async fn create_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: inserted.id,
|
||||
username: inserted.username,
|
||||
name: inserted.name,
|
||||
role: role_name,
|
||||
height_cm: inserted.height_cm,
|
||||
blood_type: inserted.blood_type,
|
||||
@@ -246,6 +251,9 @@ pub async fn update_user(
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let mut active: user::ActiveModel = existing.into();
|
||||
if req.name.is_some() {
|
||||
active.name = Set(req.name);
|
||||
}
|
||||
if req.height_cm.is_some() {
|
||||
active.height_cm = Set(req.height_cm);
|
||||
}
|
||||
@@ -291,6 +299,7 @@ pub async fn update_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: updated.id,
|
||||
username: updated.username,
|
||||
name: updated.name,
|
||||
role: role_name,
|
||||
height_cm: updated.height_cm,
|
||||
blood_type: updated.blood_type,
|
||||
|
||||
@@ -18,6 +18,9 @@ pub struct Model {
|
||||
/// Foreign key to roles table
|
||||
pub role_id: i32,
|
||||
|
||||
/// Display name (optional, separate from username)
|
||||
pub name: Option<String>,
|
||||
|
||||
// Profile fields
|
||||
/// Height in centimeters
|
||||
pub height_cm: Option<f32>,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNavigate, Link } from 'react-router-dom'
|
||||
interface User {
|
||||
id: number
|
||||
username: string
|
||||
name: string | null
|
||||
role: string
|
||||
}
|
||||
|
||||
@@ -13,13 +14,21 @@ export function Dashboard() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// First get auth info, then fetch full user profile for name
|
||||
fetch('/api/auth/me', { credentials: 'include' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.user) {
|
||||
setUser(data.user)
|
||||
} else {
|
||||
if (!data.user) {
|
||||
navigate('/login')
|
||||
return null
|
||||
}
|
||||
// Fetch full user profile to get name
|
||||
return fetch(`/api/users/${data.user.id}`, { credentials: 'include' })
|
||||
.then(res => res.json())
|
||||
})
|
||||
.then((profile) => {
|
||||
if (profile) {
|
||||
setUser(profile)
|
||||
}
|
||||
})
|
||||
.catch(() => navigate('/login'))
|
||||
@@ -49,6 +58,9 @@ export function Dashboard() {
|
||||
localStorage.setItem('theme', newTheme)
|
||||
}
|
||||
|
||||
// Display name or username fallback
|
||||
const displayName = user.name || user.username
|
||||
|
||||
return (
|
||||
<div style={{ padding: 'var(--space-lg)' }}>
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}>
|
||||
@@ -70,7 +82,7 @@ export function Dashboard() {
|
||||
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: 'var(--space-sm)' }}>Welcome, {user.username}</h3>
|
||||
<h3 style={{ marginBottom: 'var(--space-sm)' }}>Welcome, {displayName}</h3>
|
||||
<p className="text-secondary text-sm">
|
||||
Role: <span className="indicator indicator-info">{user.role}</span>
|
||||
</p>
|
||||
|
||||
@@ -10,6 +10,7 @@ interface Diet {
|
||||
interface UserProfile {
|
||||
id: number
|
||||
username: string
|
||||
name: string | null
|
||||
role: string
|
||||
height_cm: number | null
|
||||
blood_type: string | null
|
||||
@@ -29,6 +30,7 @@ export function ProfilePage() {
|
||||
// Form state
|
||||
const [userId, setUserId] = useState<number | null>(null)
|
||||
const [username, setUsername] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [heightCm, setHeightCm] = useState('')
|
||||
const [bloodType, setBloodType] = useState('')
|
||||
const [birthdate, setBirthdate] = useState('')
|
||||
@@ -55,6 +57,7 @@ export function ProfilePage() {
|
||||
.then((profile: UserProfile) => {
|
||||
setUserId(profile.id)
|
||||
setUsername(profile.username)
|
||||
setName(profile.name || '')
|
||||
setHeightCm(profile.height_cm?.toString() || '')
|
||||
setBloodType(profile.blood_type || '')
|
||||
setBirthdate(profile.birthdate || '')
|
||||
@@ -82,6 +85,7 @@ export function ProfilePage() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
name: name || null,
|
||||
height_cm: heightCm ? parseFloat(heightCm) : null,
|
||||
blood_type: bloodType || null,
|
||||
birthdate: birthdate || null,
|
||||
@@ -115,13 +119,24 @@ export function ProfilePage() {
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Username (read-only) */}
|
||||
{/* Account Info */}
|
||||
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
|
||||
<h3 style={{ marginBottom: 'var(--space-md)' }}>Account</h3>
|
||||
<div className="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" className="input" value={username} disabled />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Display Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
className="input"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your full name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Physical Info */}
|
||||
|
||||
Reference in New Issue
Block a user