feat: Add optional display name to user profile with corresponding API and UI updates

This commit is contained in:
2025-12-19 18:07:01 +05:30
parent f2f2d1dec7
commit d981ff37fb
4 changed files with 44 additions and 5 deletions

View File

@@ -28,6 +28,7 @@ pub struct CreateUserRequest {
/// Request to update a user. /// Request to update a user.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateUserRequest { pub struct UpdateUserRequest {
pub name: Option<String>,
pub height_cm: Option<f32>, pub height_cm: Option<f32>,
pub blood_type: Option<String>, pub blood_type: Option<String>,
pub birthdate: Option<String>, pub birthdate: Option<String>,
@@ -41,6 +42,7 @@ pub struct UpdateUserRequest {
pub struct UserResponse { pub struct UserResponse {
pub id: i32, pub id: i32,
pub username: String, pub username: String,
pub name: Option<String>,
pub role: String, pub role: String,
pub height_cm: Option<f32>, pub height_cm: Option<f32>,
pub blood_type: Option<String>, pub blood_type: Option<String>,
@@ -85,6 +87,7 @@ pub async fn list_users(
.map(|u| UserResponse { .map(|u| UserResponse {
id: u.id, id: u.id,
username: u.username, username: u.username,
name: u.name,
role: role_map.get(&u.role_id).cloned().unwrap_or_default(), role: role_map.get(&u.role_id).cloned().unwrap_or_default(),
height_cm: u.height_cm, height_cm: u.height_cm,
blood_type: u.blood_type, blood_type: u.blood_type,
@@ -130,6 +133,7 @@ pub async fn get_user(
Ok(Json(UserResponse { Ok(Json(UserResponse {
id: u.id, id: u.id,
username: u.username, username: u.username,
name: u.name,
role: role_name, role: role_name,
height_cm: u.height_cm, height_cm: u.height_cm,
blood_type: u.blood_type, blood_type: u.blood_type,
@@ -211,6 +215,7 @@ pub async fn create_user(
Ok(Json(UserResponse { Ok(Json(UserResponse {
id: inserted.id, id: inserted.id,
username: inserted.username, username: inserted.username,
name: inserted.name,
role: role_name, role: role_name,
height_cm: inserted.height_cm, height_cm: inserted.height_cm,
blood_type: inserted.blood_type, blood_type: inserted.blood_type,
@@ -246,6 +251,9 @@ pub async fn update_user(
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
let mut active: user::ActiveModel = existing.into(); let mut active: user::ActiveModel = existing.into();
if req.name.is_some() {
active.name = Set(req.name);
}
if req.height_cm.is_some() { if req.height_cm.is_some() {
active.height_cm = Set(req.height_cm); active.height_cm = Set(req.height_cm);
} }
@@ -291,6 +299,7 @@ pub async fn update_user(
Ok(Json(UserResponse { Ok(Json(UserResponse {
id: updated.id, id: updated.id,
username: updated.username, username: updated.username,
name: updated.name,
role: role_name, role: role_name,
height_cm: updated.height_cm, height_cm: updated.height_cm,
blood_type: updated.blood_type, blood_type: updated.blood_type,

View File

@@ -18,6 +18,9 @@ pub struct Model {
/// Foreign key to roles table /// Foreign key to roles table
pub role_id: i32, pub role_id: i32,
/// Display name (optional, separate from username)
pub name: Option<String>,
// Profile fields // Profile fields
/// Height in centimeters /// Height in centimeters
pub height_cm: Option<f32>, pub height_cm: Option<f32>,

View File

@@ -4,6 +4,7 @@ import { useNavigate, Link } from 'react-router-dom'
interface User { interface User {
id: number id: number
username: string username: string
name: string | null
role: string role: string
} }
@@ -13,13 +14,21 @@ export function Dashboard() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
// First get auth info, then fetch full user profile for name
fetch('/api/auth/me', { credentials: 'include' }) fetch('/api/auth/me', { credentials: 'include' })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.user) { if (!data.user) {
setUser(data.user)
} else {
navigate('/login') 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')) .catch(() => navigate('/login'))
@@ -49,6 +58,9 @@ export function Dashboard() {
localStorage.setItem('theme', newTheme) localStorage.setItem('theme', newTheme)
} }
// Display name or username fallback
const displayName = user.name || user.username
return ( return (
<div style={{ padding: 'var(--space-lg)' }}> <div style={{ padding: 'var(--space-lg)' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}> <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 className="card" style={{ marginBottom: 'var(--space-lg)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div> <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"> <p className="text-secondary text-sm">
Role: <span className="indicator indicator-info">{user.role}</span> Role: <span className="indicator indicator-info">{user.role}</span>
</p> </p>

View File

@@ -10,6 +10,7 @@ interface Diet {
interface UserProfile { interface UserProfile {
id: number id: number
username: string username: string
name: string | null
role: string role: string
height_cm: number | null height_cm: number | null
blood_type: string | null blood_type: string | null
@@ -29,6 +30,7 @@ export function ProfilePage() {
// Form state // Form state
const [userId, setUserId] = useState<number | null>(null) const [userId, setUserId] = useState<number | null>(null)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [name, setName] = useState('')
const [heightCm, setHeightCm] = useState('') const [heightCm, setHeightCm] = useState('')
const [bloodType, setBloodType] = useState('') const [bloodType, setBloodType] = useState('')
const [birthdate, setBirthdate] = useState('') const [birthdate, setBirthdate] = useState('')
@@ -55,6 +57,7 @@ export function ProfilePage() {
.then((profile: UserProfile) => { .then((profile: UserProfile) => {
setUserId(profile.id) setUserId(profile.id)
setUsername(profile.username) setUsername(profile.username)
setName(profile.name || '')
setHeightCm(profile.height_cm?.toString() || '') setHeightCm(profile.height_cm?.toString() || '')
setBloodType(profile.blood_type || '') setBloodType(profile.blood_type || '')
setBirthdate(profile.birthdate || '') setBirthdate(profile.birthdate || '')
@@ -82,6 +85,7 @@ export function ProfilePage() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
name: name || null,
height_cm: heightCm ? parseFloat(heightCm) : null, height_cm: heightCm ? parseFloat(heightCm) : null,
blood_type: bloodType || null, blood_type: bloodType || null,
birthdate: birthdate || null, birthdate: birthdate || null,
@@ -115,13 +119,24 @@ export function ProfilePage() {
</header> </header>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{/* Username (read-only) */} {/* Account Info */}
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
<h3 style={{ marginBottom: 'var(--space-md)' }}>Account</h3> <h3 style={{ marginBottom: 'var(--space-md)' }}>Account</h3>
<div className="form-group"> <div className="form-group">
<label>Username</label> <label>Username</label>
<input type="text" className="input" value={username} disabled /> <input type="text" className="input" value={username} disabled />
</div> </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> </div>
{/* Physical Info */} {/* Physical Info */}