Compare commits
2 Commits
e558e19512
...
89815e7e21
| Author | SHA1 | Date | |
|---|---|---|---|
| 89815e7e21 | |||
| 69bc5675dc |
@@ -35,6 +35,7 @@ pub struct UpdateUserRequest {
|
|||||||
pub smoking: Option<bool>,
|
pub smoking: Option<bool>,
|
||||||
pub alcohol: Option<bool>,
|
pub alcohol: Option<bool>,
|
||||||
pub diet_id: Option<i32>,
|
pub diet_id: Option<i32>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response for a user.
|
/// Response for a user.
|
||||||
@@ -50,6 +51,7 @@ pub struct UserResponse {
|
|||||||
pub smoking: Option<bool>,
|
pub smoking: Option<bool>,
|
||||||
pub alcohol: Option<bool>,
|
pub alcohol: Option<bool>,
|
||||||
pub diet: Option<String>,
|
pub diet: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ pub async fn list_users(
|
|||||||
smoking: u.smoking,
|
smoking: u.smoking,
|
||||||
alcohol: u.alcohol,
|
alcohol: u.alcohol,
|
||||||
diet: u.diet_id.and_then(|id| diet_map.get(&id).cloned()),
|
diet: u.diet_id.and_then(|id| diet_map.get(&id).cloned()),
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
created_at: u.created_at.to_string(),
|
created_at: u.created_at.to_string(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -141,6 +144,7 @@ pub async fn get_user(
|
|||||||
smoking: u.smoking,
|
smoking: u.smoking,
|
||||||
alcohol: u.alcohol,
|
alcohol: u.alcohol,
|
||||||
diet: diet_name,
|
diet: diet_name,
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
created_at: u.created_at.to_string(),
|
created_at: u.created_at.to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -188,6 +192,7 @@ pub async fn create_user(
|
|||||||
smoking: Set(req.smoking),
|
smoking: Set(req.smoking),
|
||||||
alcohol: Set(req.alcohol),
|
alcohol: Set(req.alcohol),
|
||||||
diet_id: Set(req.diet_id),
|
diet_id: Set(req.diet_id),
|
||||||
|
avatar_url: Set(None), // Default to None on create for now, unless we want to support it in CreateUserRequest
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -223,6 +228,7 @@ pub async fn create_user(
|
|||||||
smoking: inserted.smoking,
|
smoking: inserted.smoking,
|
||||||
alcohol: inserted.alcohol,
|
alcohol: inserted.alcohol,
|
||||||
diet: diet_name,
|
diet: diet_name,
|
||||||
|
avatar_url: inserted.avatar_url,
|
||||||
created_at: inserted.created_at.to_string(),
|
created_at: inserted.created_at.to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -270,6 +276,9 @@ pub async fn update_user(
|
|||||||
if req.diet_id.is_some() {
|
if req.diet_id.is_some() {
|
||||||
active.diet_id = Set(req.diet_id);
|
active.diet_id = Set(req.diet_id);
|
||||||
}
|
}
|
||||||
|
if req.avatar_url.is_some() {
|
||||||
|
active.avatar_url = Set(req.avatar_url);
|
||||||
|
}
|
||||||
active.updated_at = Set(now);
|
active.updated_at = Set(now);
|
||||||
|
|
||||||
let updated = active
|
let updated = active
|
||||||
@@ -307,6 +316,7 @@ pub async fn update_user(
|
|||||||
smoking: updated.smoking,
|
smoking: updated.smoking,
|
||||||
alcohol: updated.alcohol,
|
alcohol: updated.alcohol,
|
||||||
diet: diet_name,
|
diet: diet_name,
|
||||||
|
avatar_url: updated.avatar_url,
|
||||||
created_at: updated.created_at.to_string(),
|
created_at: updated.created_at.to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ pub struct Model {
|
|||||||
/// Foreign key to diet types
|
/// Foreign key to diet types
|
||||||
pub diet_id: Option<i32>,
|
pub diet_id: Option<i32>,
|
||||||
|
|
||||||
|
/// URL to profile avatar icon
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
|
||||||
pub created_at: DateTime,
|
pub created_at: DateTime,
|
||||||
pub updated_at: DateTime,
|
pub updated_at: DateTime,
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/public/icons/general/icons8-cancel-50.png
Normal file
|
After Width: | Height: | Size: 849 B |
BIN
frontend/public/icons/general/icons8-document-50.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
frontend/public/icons/general/icons8-idea-50.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/icons/general/icons8-settings-50.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/general/icons8-sun-50.png
Normal file
|
After Width: | Height: | Size: 539 B |
BIN
frontend/public/icons/general/icons8-user-50.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/general/icons8-waxing-crescent-50.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/healthcare/icons8-powerchart-50.png
Normal file
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-2.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-3.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-4.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-5.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-6.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50-7.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/icons/user/icons8-male-user-50.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/icons/user/icons8-user-50-2.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/user/icons8-user-50-3.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/public/icons/user/icons8-user-50-4.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/public/icons/user/icons8-user-50-5.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/public/icons/user/icons8-user-50-7.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/user/icons8-user-50-8.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/icons/user/icons8-user-50.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -6,6 +6,7 @@ interface User {
|
|||||||
username: string
|
username: string
|
||||||
name: string | null
|
name: string | null
|
||||||
role: string
|
role: string
|
||||||
|
avatar_url: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -67,10 +68,10 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
const displayName = user.name || user.username
|
const displayName = user.name || user.username
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/', label: 'Dashboard', icon: '📊' },
|
{ path: '/', label: 'Dashboard', icon: '/icons/healthcare/icons8-powerchart-50.png' },
|
||||||
{ path: '/profile', label: 'Profile', icon: '👤' },
|
{ path: '/profile', label: 'Profile', icon: '/icons/general/icons8-user-50.png' },
|
||||||
{ path: '/insights', label: 'Insights', icon: '💡', disabled: true },
|
{ path: '/insights', label: 'Insights', icon: '/icons/general/icons8-idea-50.png', disabled: true },
|
||||||
{ path: '/sources', label: 'Sources', icon: '📄' },
|
{ path: '/sources', label: 'Sources', icon: '/icons/general/icons8-document-50.png' },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +91,9 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
className={`sidebar-link ${location.pathname === item.path ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
|
className={`sidebar-link ${location.pathname === item.path ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
|
||||||
onClick={e => item.disabled && e.preventDefault()}
|
onClick={e => item.disabled && e.preventDefault()}
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">{item.icon}</span>
|
<span className="sidebar-icon">
|
||||||
|
<img src={item.icon} alt={item.label} />
|
||||||
|
</span>
|
||||||
<span className="sidebar-label">{item.label}</span>
|
<span className="sidebar-label">{item.label}</span>
|
||||||
{item.disabled && <span className="sidebar-badge">Soon</span>}
|
{item.disabled && <span className="sidebar-badge">Soon</span>}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -105,7 +108,9 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
to="/admin"
|
to="/admin"
|
||||||
className={`sidebar-link ${location.pathname === '/admin' ? 'active' : ''}`}
|
className={`sidebar-link ${location.pathname === '/admin' ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">⚙️</span>
|
<span className="sidebar-icon">
|
||||||
|
<img src="/icons/user/icons8-add-user-group-woman-man-50.png" alt="Admin" />
|
||||||
|
</span>
|
||||||
<span className="sidebar-label">Manage Users</span>
|
<span className="sidebar-label">Manage Users</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,11 +118,20 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
|
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
<div className="sidebar-user">
|
<div className="sidebar-user">
|
||||||
<div className="sidebar-user-name">{displayName}</div>
|
<div className="sidebar-avatar">
|
||||||
<div className="sidebar-user-role">
|
{user.avatar_url ? (
|
||||||
<span className={`indicator indicator-${user.role === 'admin' ? 'warning' : 'info'}`}>
|
<img src={user.avatar_url} alt={displayName} className="avatar-img" />
|
||||||
{user.role}
|
) : (
|
||||||
</span>
|
<div className="avatar-placeholder">{displayName[0].toUpperCase()}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="sidebar-user-info">
|
||||||
|
<div className="sidebar-user-name">{displayName}</div>
|
||||||
|
<div className="sidebar-user-role">
|
||||||
|
<span className={`indicator indicator-${user.role === 'admin' ? 'warning' : 'info'}`}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-actions">
|
<div className="sidebar-actions">
|
||||||
@@ -126,14 +140,18 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? '☀️' : '🌙'}
|
{theme === 'dark' ? (
|
||||||
|
<img src="/icons/general/icons8-sun-50.png" alt="Light" className="theme-icon" />
|
||||||
|
) : (
|
||||||
|
<img src="/icons/general/icons8-waxing-crescent-50.png" alt="Dark" className="theme-icon" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="sidebar-btn"
|
className="sidebar-btn"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
title="Logout"
|
title="Logout"
|
||||||
>
|
>
|
||||||
🚪
|
<img src="/icons/general/icons8-cancel-50.png" alt="Logout" className="theme-icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -422,6 +422,100 @@ select.input {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Biomarker List Layout */
|
||||||
|
.biomarker-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-row:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-dot.status-low {
|
||||||
|
background: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-dot.status-normal {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-dot.status-high {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-info {
|
||||||
|
flex: 0 0 240px;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-info .biomarker-name {
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biomarker-info .biomarker-unit {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Biomarker Scale Bar */
|
||||||
|
.biomarker-scale {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-bar {
|
||||||
|
width: 120px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-bar.placeholder {
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-bar.range {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-marker {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: var(--text-primary);
|
||||||
|
border-radius: 2px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* App Layout with Sidebar */
|
/* App Layout with Sidebar */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -549,6 +643,42 @@ select.input {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-info {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-user-name {
|
.sidebar-user-name {
|
||||||
@@ -559,6 +689,43 @@ select.input {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Avatar Picker in Profile */
|
||||||
|
.avatar-picker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, transform 0.1s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option.selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-actions {
|
.sidebar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
@@ -578,6 +745,22 @@ select.input {
|
|||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invert icons in dark mode if they are dark-oriented */
|
||||||
|
[data-theme='dark'] .theme-icon {
|
||||||
|
filter: invert(1) brightness(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Content */
|
/* Main Content */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function DashboardPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ fontSize: '16px', fontWeight: 600 }}>{category.name}</span>
|
<span style={{ fontSize: '16px', fontWeight: 600, textTransform: 'uppercase' }}>{category.name}</span>
|
||||||
<span className="text-secondary text-sm" style={{ marginLeft: 'var(--space-sm)' }}>
|
<span className="text-secondary text-sm" style={{ marginLeft: 'var(--space-sm)' }}>
|
||||||
({categoryBiomarkers.length} biomarkers)
|
({categoryBiomarkers.length} biomarkers)
|
||||||
</span>
|
</span>
|
||||||
@@ -102,15 +102,17 @@ export function DashboardPage() {
|
|||||||
No biomarkers in this category
|
No biomarkers in this category
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-xs)' }}>
|
<div className="biomarker-list">
|
||||||
{categoryBiomarkers.map(biomarker => (
|
{categoryBiomarkers.map(biomarker => (
|
||||||
<div
|
<div key={biomarker.id} className="biomarker-row">
|
||||||
key={biomarker.id}
|
<div className="biomarker-dot" title="No data"></div>
|
||||||
className="biomarker-chip"
|
<div className="biomarker-info">
|
||||||
title={`${biomarker.name} (${biomarker.unit})`}
|
<span className="biomarker-name">{biomarker.name}</span>
|
||||||
>
|
<span className="biomarker-unit">{biomarker.unit}</span>
|
||||||
<span className="biomarker-name">{biomarker.name}</span>
|
</div>
|
||||||
<span className="biomarker-unit">{biomarker.unit}</span>
|
<div className="biomarker-scale">
|
||||||
|
<div className="scale-bar placeholder"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface UserProfile {
|
|||||||
smoking: boolean | null
|
smoking: boolean | null
|
||||||
alcohol: boolean | null
|
alcohol: boolean | null
|
||||||
diet: string | null
|
diet: string | null
|
||||||
|
avatar_url: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
@@ -35,6 +36,12 @@ export function ProfilePage() {
|
|||||||
const [smoking, setSmoking] = useState<boolean | null>(null)
|
const [smoking, setSmoking] = useState<boolean | null>(null)
|
||||||
const [alcohol, setAlcohol] = useState<boolean | null>(null)
|
const [alcohol, setAlcohol] = useState<boolean | null>(null)
|
||||||
const [dietId, setDietId] = useState<number | null>(null)
|
const [dietId, setDietId] = useState<number | null>(null)
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const avatarOptions = [
|
||||||
|
...[1, 2, 3, 4, 5, 6, 7].map(i => `/icons/user/icons8-male-user-50${i === 1 ? '' : `-${i}`}.png`),
|
||||||
|
...['', '-2', '-3', '-4', '-5', '-7', '-8'].map(s => `/icons/user/icons8-user-50${s}.png`),
|
||||||
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch current user and diets (Layout already ensures auth)
|
// Fetch current user and diets (Layout already ensures auth)
|
||||||
@@ -61,9 +68,17 @@ export function ProfilePage() {
|
|||||||
// Find diet ID from name
|
// Find diet ID from name
|
||||||
const diet = dietsData.find((d: Diet) => d.name === profile.diet)
|
const diet = dietsData.find((d: Diet) => d.name === profile.diet)
|
||||||
setDietId(diet?.id || null)
|
setDietId(diet?.id || null)
|
||||||
|
setAvatarUrl(profile.avatar_url)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false))
|
.finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
// Show success message if we just saved
|
||||||
|
if (localStorage.getItem('profileSaved') === 'true') {
|
||||||
|
setMessage({ type: 'success', text: 'Profile updated successfully' })
|
||||||
|
localStorage.removeItem('profileSaved')
|
||||||
|
}
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -86,11 +101,13 @@ export function ProfilePage() {
|
|||||||
smoking,
|
smoking,
|
||||||
alcohol,
|
alcohol,
|
||||||
diet_id: dietId,
|
diet_id: dietId,
|
||||||
|
avatar_url: avatarUrl,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage({ type: 'success', text: 'Profile updated successfully' })
|
localStorage.setItem('profileSaved', 'true')
|
||||||
|
window.location.reload()
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: 'Failed to update profile' })
|
setMessage({ type: 'error', text: 'Failed to update profile' })
|
||||||
}
|
}
|
||||||
@@ -131,6 +148,20 @@ export function ProfilePage() {
|
|||||||
placeholder="Your full name"
|
placeholder="Your full name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Profile Avatar</label>
|
||||||
|
<div className="avatar-picker">
|
||||||
|
{avatarOptions.map(option => (
|
||||||
|
<div
|
||||||
|
key={option}
|
||||||
|
className={`avatar-option ${avatarUrl === option ? 'selected' : ''}`}
|
||||||
|
onClick={() => setAvatarUrl(option)}
|
||||||
|
>
|
||||||
|
<img src={option} alt="Avatar option" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Physical Info */}
|
{/* Physical Info */}
|
||||||
|
|||||||