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.
|
/// 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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user