diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs index 020f395..3c68875 100644 --- a/backend/src/handlers/users.rs +++ b/backend/src/handlers/users.rs @@ -35,6 +35,7 @@ pub struct UpdateUserRequest { pub smoking: Option, pub alcohol: Option, pub diet_id: Option, + pub avatar_url: Option, } /// Response for a user. @@ -50,6 +51,7 @@ pub struct UserResponse { pub smoking: Option, pub alcohol: Option, pub diet: Option, + pub avatar_url: Option, pub created_at: String, } @@ -95,6 +97,7 @@ pub async fn list_users( smoking: u.smoking, alcohol: u.alcohol, diet: u.diet_id.and_then(|id| diet_map.get(&id).cloned()), + avatar_url: u.avatar_url, created_at: u.created_at.to_string(), }) .collect(); @@ -141,6 +144,7 @@ pub async fn get_user( smoking: u.smoking, alcohol: u.alcohol, diet: diet_name, + avatar_url: u.avatar_url, created_at: u.created_at.to_string(), })) } @@ -188,6 +192,7 @@ pub async fn create_user( smoking: Set(req.smoking), alcohol: Set(req.alcohol), 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), updated_at: Set(now), ..Default::default() @@ -223,6 +228,7 @@ pub async fn create_user( smoking: inserted.smoking, alcohol: inserted.alcohol, diet: diet_name, + avatar_url: inserted.avatar_url, created_at: inserted.created_at.to_string(), })) } @@ -270,6 +276,9 @@ pub async fn update_user( if req.diet_id.is_some() { 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); let updated = active @@ -307,6 +316,7 @@ pub async fn update_user( smoking: updated.smoking, alcohol: updated.alcohol, diet: diet_name, + avatar_url: updated.avatar_url, created_at: updated.created_at.to_string(), })) } diff --git a/backend/src/models/user/user.rs b/backend/src/models/user/user.rs index 3d7790f..73df11c 100644 --- a/backend/src/models/user/user.rs +++ b/backend/src/models/user/user.rs @@ -41,6 +41,9 @@ pub struct Model { /// Foreign key to diet types pub diet_id: Option, + /// URL to profile avatar icon + pub avatar_url: Option, + pub created_at: DateTime, pub updated_at: DateTime, } diff --git a/frontend/public/icons/general/icons8-cancel-50.png b/frontend/public/icons/general/icons8-cancel-50.png new file mode 100644 index 0000000..30e49ee Binary files /dev/null and b/frontend/public/icons/general/icons8-cancel-50.png differ diff --git a/frontend/public/icons/general/icons8-document-50.png b/frontend/public/icons/general/icons8-document-50.png new file mode 100644 index 0000000..12db81d Binary files /dev/null and b/frontend/public/icons/general/icons8-document-50.png differ diff --git a/frontend/public/icons/general/icons8-idea-50.png b/frontend/public/icons/general/icons8-idea-50.png new file mode 100644 index 0000000..ad29b4c Binary files /dev/null and b/frontend/public/icons/general/icons8-idea-50.png differ diff --git a/frontend/public/icons/general/icons8-settings-50.png b/frontend/public/icons/general/icons8-settings-50.png new file mode 100644 index 0000000..baad32e Binary files /dev/null and b/frontend/public/icons/general/icons8-settings-50.png differ diff --git a/frontend/public/icons/general/icons8-sun-50.png b/frontend/public/icons/general/icons8-sun-50.png new file mode 100644 index 0000000..41657b5 Binary files /dev/null and b/frontend/public/icons/general/icons8-sun-50.png differ diff --git a/frontend/public/icons/general/icons8-user-50.png b/frontend/public/icons/general/icons8-user-50.png new file mode 100644 index 0000000..b9f33e1 Binary files /dev/null and b/frontend/public/icons/general/icons8-user-50.png differ diff --git a/frontend/public/icons/general/icons8-waxing-crescent-50.png b/frontend/public/icons/general/icons8-waxing-crescent-50.png new file mode 100644 index 0000000..1d137d1 Binary files /dev/null and b/frontend/public/icons/general/icons8-waxing-crescent-50.png differ diff --git a/frontend/public/icons/healthcare/icons8-powerchart-50.png b/frontend/public/icons/healthcare/icons8-powerchart-50.png new file mode 100644 index 0000000..3d3fcae Binary files /dev/null and b/frontend/public/icons/healthcare/icons8-powerchart-50.png differ diff --git a/frontend/public/icons/user/icons8-add-user-group-woman-man-50.png b/frontend/public/icons/user/icons8-add-user-group-woman-man-50.png new file mode 100644 index 0000000..8ac32e8 Binary files /dev/null and b/frontend/public/icons/user/icons8-add-user-group-woman-man-50.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-2.png b/frontend/public/icons/user/icons8-male-user-50-2.png new file mode 100644 index 0000000..030cee9 Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-2.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-3.png b/frontend/public/icons/user/icons8-male-user-50-3.png new file mode 100644 index 0000000..ed869c1 Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-3.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-4.png b/frontend/public/icons/user/icons8-male-user-50-4.png new file mode 100644 index 0000000..9d2f325 Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-4.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-5.png b/frontend/public/icons/user/icons8-male-user-50-5.png new file mode 100644 index 0000000..882c07e Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-5.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-6.png b/frontend/public/icons/user/icons8-male-user-50-6.png new file mode 100644 index 0000000..9f40bf8 Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-6.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50-7.png b/frontend/public/icons/user/icons8-male-user-50-7.png new file mode 100644 index 0000000..599bb4e Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50-7.png differ diff --git a/frontend/public/icons/user/icons8-male-user-50.png b/frontend/public/icons/user/icons8-male-user-50.png new file mode 100644 index 0000000..177d6e9 Binary files /dev/null and b/frontend/public/icons/user/icons8-male-user-50.png differ diff --git a/frontend/public/icons/user/icons8-user-50-2.png b/frontend/public/icons/user/icons8-user-50-2.png new file mode 100644 index 0000000..20739bb Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-2.png differ diff --git a/frontend/public/icons/user/icons8-user-50-3.png b/frontend/public/icons/user/icons8-user-50-3.png new file mode 100644 index 0000000..7c0ca48 Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-3.png differ diff --git a/frontend/public/icons/user/icons8-user-50-4.png b/frontend/public/icons/user/icons8-user-50-4.png new file mode 100644 index 0000000..baf677a Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-4.png differ diff --git a/frontend/public/icons/user/icons8-user-50-5.png b/frontend/public/icons/user/icons8-user-50-5.png new file mode 100644 index 0000000..f5f0a80 Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-5.png differ diff --git a/frontend/public/icons/user/icons8-user-50-7.png b/frontend/public/icons/user/icons8-user-50-7.png new file mode 100644 index 0000000..b9f33e1 Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-7.png differ diff --git a/frontend/public/icons/user/icons8-user-50-8.png b/frontend/public/icons/user/icons8-user-50-8.png new file mode 100644 index 0000000..9b900c5 Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50-8.png differ diff --git a/frontend/public/icons/user/icons8-user-50.png b/frontend/public/icons/user/icons8-user-50.png new file mode 100644 index 0000000..3238516 Binary files /dev/null and b/frontend/public/icons/user/icons8-user-50.png differ diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2eaafb7..f61de7e 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -6,6 +6,7 @@ interface User { username: string name: string | null role: string + avatar_url: string | null } interface LayoutProps { @@ -67,10 +68,10 @@ export function Layout({ children }: LayoutProps) { const displayName = user.name || user.username const navItems = [ - { path: '/', label: 'Dashboard', icon: '📊' }, - { path: '/profile', label: 'Profile', icon: '👤' }, - { path: '/insights', label: 'Insights', icon: '💡', disabled: true }, - { path: '/sources', label: 'Sources', icon: '📄' }, + { path: '/', label: 'Dashboard', icon: '/icons/healthcare/icons8-powerchart-50.png' }, + { path: '/profile', label: 'Profile', icon: '/icons/general/icons8-user-50.png' }, + { path: '/insights', label: 'Insights', icon: '/icons/general/icons8-idea-50.png', disabled: true }, + { path: '/sources', label: 'Sources', icon: '/icons/general/icons8-document-50.png' }, ] return ( @@ -90,7 +91,9 @@ export function Layout({ children }: LayoutProps) { className={`sidebar-link ${location.pathname === item.path ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`} onClick={e => item.disabled && e.preventDefault()} > - {item.icon} + + {item.label} + {item.label} {item.disabled && Soon} @@ -105,7 +108,9 @@ export function Layout({ children }: LayoutProps) { to="/admin" className={`sidebar-link ${location.pathname === '/admin' ? 'active' : ''}`} > - ⚙️ + + Admin + Manage Users @@ -113,11 +118,20 @@ export function Layout({ children }: LayoutProps) {
-
{displayName}
-
- - {user.role} - +
+ {user.avatar_url ? ( + {displayName} + ) : ( +
{displayName[0].toUpperCase()}
+ )} +
+
+
{displayName}
+
+ + {user.role} + +
@@ -126,14 +140,18 @@ export function Layout({ children }: LayoutProps) { onClick={toggleTheme} title={theme === 'dark' ? 'Light mode' : 'Dark mode'} > - {theme === 'dark' ? '☀️' : '🌙'} + {theme === 'dark' ? ( + Light + ) : ( + Dark + )}
diff --git a/frontend/src/index.css b/frontend/src/index.css index d62e6d6..ad1d61a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -549,6 +549,42 @@ select.input { display: flex; align-items: center; 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 { @@ -559,6 +595,43 @@ select.input { 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 { display: flex; gap: var(--space-xs); @@ -578,6 +651,22 @@ select.input { 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 { flex: 1; diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 4c70117..ff95192 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -17,6 +17,7 @@ interface UserProfile { smoking: boolean | null alcohol: boolean | null diet: string | null + avatar_url: string | null } export function ProfilePage() { @@ -35,6 +36,12 @@ export function ProfilePage() { const [smoking, setSmoking] = useState(null) const [alcohol, setAlcohol] = useState(null) const [dietId, setDietId] = useState(null) + const [avatarUrl, setAvatarUrl] = useState(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(() => { // Fetch current user and diets (Layout already ensures auth) @@ -61,9 +68,17 @@ export function ProfilePage() { // Find diet ID from name const diet = dietsData.find((d: Diet) => d.name === profile.diet) 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) => { @@ -86,11 +101,13 @@ export function ProfilePage() { smoking, alcohol, diet_id: dietId, + avatar_url: avatarUrl, }), }) if (res.ok) { - setMessage({ type: 'success', text: 'Profile updated successfully' }) + localStorage.setItem('profileSaved', 'true') + window.location.reload() } else { setMessage({ type: 'error', text: 'Failed to update profile' }) } @@ -131,6 +148,20 @@ export function ProfilePage() { placeholder="Your full name" />
+
+ +
+ {avatarOptions.map(option => ( +
setAvatarUrl(option)} + > + Avatar option +
+ ))} +
+
{/* Physical Info */}