Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 79 additions & 34 deletions Client/src/components/common/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Link, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { clearAuthToken, getAuthToken } from '../../lib/auth';
import { getMyProfile } from '../../lib/api';
import { DEFAULT_AVATAR, getProfileDisplayName, resolveAvatarUrl } from '../../lib/profile';
import { DEFAULT_AVATAR, getDefaultDashboardPath, getProfileDisplayName, hasRole, PROFILE_UPDATED_EVENT, resolveAvatarUrl } from '../../lib/profile';
import type { UserProfile } from '../../types';

const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false);
Expand All @@ -12,8 +13,12 @@ const Navbar = () => {
const [profileName, setProfileName] = useState('User');
const [profileEmail, setProfileEmail] = useState('');
const [profileAvatar, setProfileAvatar] = useState(DEFAULT_AVATAR);
const [dashboardPath, setDashboardPath] = useState('/user-dashboard');
const [settingsPath, setSettingsPath] = useState('/user-dashboard/settings');
const [canSwitchToOwner, setCanSwitchToOwner] = useState(false);
const location = useLocation();
const isHome = location.pathname === "/";
const isUserDashboard = location.pathname.startsWith('/user-dashboard');

// Prevent scrolling when mobile menu is open
useEffect(() => {
Expand All @@ -32,6 +37,9 @@ const Navbar = () => {
setProfileName('User');
setProfileEmail('');
setProfileAvatar(DEFAULT_AVATAR);
setDashboardPath('/user-dashboard');
setSettingsPath('/user-dashboard/settings');
setCanSwitchToOwner(false);
return;
}

Expand All @@ -41,17 +49,41 @@ const Navbar = () => {
setProfileName(getProfileDisplayName(profile.full_name, profile.email));
setProfileEmail(profile.email);
setProfileAvatar(resolveAvatarUrl(profile.avatar_url));
setDashboardPath(getDefaultDashboardPath(profile));
setSettingsPath(getDefaultDashboardPath(profile) === '/dashboard' ? '/dashboard/settings' : '/user-dashboard/settings');
setCanSwitchToOwner(hasRole(profile.roles, 'vehicle_owner'));
} catch {
setIsLoggedIn(false);
setProfileName('User');
setProfileEmail('');
setProfileAvatar(DEFAULT_AVATAR);
setDashboardPath('/user-dashboard');
setSettingsPath('/user-dashboard/settings');
setCanSwitchToOwner(false);
}
};

void loadProfile();
}, [location.pathname]);

useEffect(() => {
const handleProfileUpdated = (event: Event) => {
const profile = (event as CustomEvent<UserProfile>).detail;
if (!profile) return;

setIsLoggedIn(true);
setProfileName(getProfileDisplayName(profile.full_name, profile.email));
setProfileEmail(profile.email);
setProfileAvatar(resolveAvatarUrl(profile.avatar_url));
setDashboardPath(getDefaultDashboardPath(profile));
setSettingsPath(getDefaultDashboardPath(profile) === '/dashboard' ? '/dashboard/settings' : '/user-dashboard/settings');
setCanSwitchToOwner(hasRole(profile.roles, 'vehicle_owner'));
};

window.addEventListener(PROFILE_UPDATED_EVENT, handleProfileUpdated as EventListener);
return () => window.removeEventListener(PROFILE_UPDATED_EVENT, handleProfileUpdated as EventListener);
}, []);

// Dynamic styling based on current page
const navBaseClass = isHome
? "absolute bg-transparent text-white"
Expand Down Expand Up @@ -80,39 +112,49 @@ const Navbar = () => {
{/* Auth Buttons / Profile */}
<div className="hidden md:flex items-center gap-6">
{isLoggedIn ? (
<div className="relative">
<button
onClick={() => setShowProfileMenu(!showProfileMenu)}
className="flex items-center gap-2 p-1 pl-3 bg-gray-50 rounded-full hover:bg-gray-100 transition border border-gray-200"
>
<span className="text-sm font-bold text-gray-700">{profileName}</span>
<img src={profileAvatar} alt="Profile" className="w-8 h-8 rounded-full object-cover" />
</button>

{/* Dropdown Menu */}
{showProfileMenu && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-100 py-2 animate-in fade-in slide-in-from-top-2">
<div className="px-4 py-3 border-b border-gray-50 mb-2">
<p className="font-bold text-sm text-gray-900">{profileName}</p>
<p className="text-xs text-gray-500">{profileEmail}</p>
</div>
<Link to="/user-dashboard" className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 font-medium">
<User size={16} /> My Profile
</Link>
<Link to="/user-dashboard/settings" className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 font-medium">
<Settings size={16} /> Settings
</Link>
<div className="h-px bg-gray-50 my-2"></div>
<Link
to="/signin"
onClick={clearAuthToken}
className="flex items-center gap-2 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 font-medium"
>
<LogOut size={16} /> Sign Out
</Link>
</div>
<>
{isUserDashboard && canSwitchToOwner && (
<Link
to="/dashboard"
className="text-sm font-semibold text-gray-500 hover:text-orange-600 transition"
>
Switch to Vehicle Owner
</Link>
)}
</div>
<div className="relative">
<button
onClick={() => setShowProfileMenu(!showProfileMenu)}
className="flex items-center gap-2 p-1 pl-3 bg-gray-50 rounded-full hover:bg-gray-100 transition border border-gray-200"
>
<span className="text-sm font-bold text-gray-700">{profileName}</span>
<img src={profileAvatar} alt="Profile" className="w-8 h-8 rounded-full object-cover" />
</button>

{/* Dropdown Menu */}
{showProfileMenu && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-100 py-2 animate-in fade-in slide-in-from-top-2">
<div className="px-4 py-3 border-b border-gray-50 mb-2">
<p className="font-bold text-sm text-gray-900">{profileName}</p>
<p className="text-xs text-gray-500">{profileEmail}</p>
</div>
<Link to={dashboardPath} className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 font-medium">
<User size={16} /> My Profile
</Link>
<Link to={settingsPath} className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 font-medium">
<Settings size={16} /> Settings
</Link>
<div className="h-px bg-gray-50 my-2"></div>
<Link
to="/signin"
onClick={clearAuthToken}
className="flex items-center gap-2 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 font-medium"
>
<LogOut size={16} /> Sign Out
</Link>
</div>
)}
</div>
</>
) : (
<>
<Link to="/signin" className="text-sm font-bold hover:opacity-80 transition">Sign In</Link>
Expand Down Expand Up @@ -149,7 +191,10 @@ const Navbar = () => {
<Link to="/about" className="hover:text-orange-500" onClick={closeMenu}>About</Link>
<Link to="/contact" className="hover:text-orange-500" onClick={closeMenu}>Contact Us</Link>
{isLoggedIn && (
<Link to="/user-dashboard" className="hover:text-orange-500" onClick={closeMenu}>My Dashboard</Link>
<Link to={dashboardPath} className="hover:text-orange-500" onClick={closeMenu}>My Dashboard</Link>
)}
{isLoggedIn && isUserDashboard && canSwitchToOwner && (
<Link to="/dashboard" className="hover:text-orange-500" onClick={closeMenu}>Switch to Vehicle Owner</Link>
)}
</div>

Expand Down
27 changes: 24 additions & 3 deletions Client/src/components/dashboard/DashboardNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,46 @@ import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { clearAuthToken } from '../../lib/auth';
import { getMyProfile } from '../../lib/api';
import { DEFAULT_AVATAR, getProfileDisplayName, getRoleLabel, resolveAvatarUrl } from '../../lib/profile';
import { DEFAULT_AVATAR, getProfileDisplayName, getRoleLabel, hasRole, PROFILE_UPDATED_EVENT, resolveAvatarUrl } from '../../lib/profile';
import type { UserProfile } from '../../types';

const DashboardNavbar = () => {
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [userName, setUserName] = useState('User');
const [roleLabel, setRoleLabel] = useState('Owner');
const [avatarSrc, setAvatarSrc] = useState(DEFAULT_AVATAR);
const [renterDestination, setRenterDestination] = useState('/');

useEffect(() => {
const loadProfile = async () => {
try {
const profile = await getMyProfile();
setUserName(getProfileDisplayName(profile.full_name, profile.email));
setRoleLabel(getRoleLabel(profile.role));
setRoleLabel(getRoleLabel(profile.roles));
setAvatarSrc(resolveAvatarUrl(profile.avatar_url));
setRenterDestination(hasRole(profile.roles, 'renter') || hasRole(profile.roles, 'user') ? '/user-dashboard' : '/');
} catch {
// Keep fallback labels if profile request fails.
}
};
void loadProfile();
}, []);

useEffect(() => {
const handleProfileUpdated = (event: Event) => {
const profile = (event as CustomEvent<UserProfile>).detail;
if (!profile) return;

setUserName(getProfileDisplayName(profile.full_name, profile.email));
setRoleLabel(getRoleLabel(profile.roles));
setAvatarSrc(resolveAvatarUrl(profile.avatar_url));
setRenterDestination(hasRole(profile.roles, 'renter') || hasRole(profile.roles, 'user') ? '/user-dashboard' : '/');
};

window.addEventListener(PROFILE_UPDATED_EVENT, handleProfileUpdated as EventListener);
return () => window.removeEventListener(PROFILE_UPDATED_EVENT, handleProfileUpdated as EventListener);
}, []);

return (
<nav className="w-full bg-white border-b border-gray-100 px-6 md:px-12 py-4 flex items-center justify-between sticky top-0 z-50">
{/* Logo */}
Expand All @@ -37,7 +55,10 @@ const DashboardNavbar = () => {

{/* User Actions */}
<div className="flex items-center gap-4">
<Link to="/" className="text-sm font-semibold text-gray-500 hover:text-gray-900 transition mr-4 hidden md:block">
<Link
to={renterDestination}
className="text-sm font-semibold text-gray-500 hover:text-gray-900 transition mr-4 hidden md:block"
>
Switch to Renter
</Link>
<div className="relative">
Expand Down
7 changes: 4 additions & 3 deletions Client/src/layouts/UserDashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Outlet, NavLink } from 'react-router-dom';
import { User, Calendar, Heart, Settings } from 'lucide-react';

const UserDashboardLayout = () => {

return (
<div className="min-h-screen bg-gray-50 pt-24 pb-12">
<div className="max-w-7xl mx-auto px-6">

<div className="mb-8">
<h1 className="text-3xl font-bold text-[#003049]">My Profile</h1>
<p className="text-gray-500 mt-2">Manage your account and view your rental activity</p>
<div>
<h1 className="text-3xl font-bold text-[#003049]">My Profile</h1>
<p className="text-gray-500 mt-2">Manage your account and view your rental activity</p>
</div>
</div>

{/* Dashboard Navigation */}
Expand Down
16 changes: 11 additions & 5 deletions Client/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AuthResponse, PublicUserProfile, RentApi, UserProfile, VehicleApi } from '../types';
import type { AuthResponse, PublicUserProfile, RentApi, UserProfile, UserRole, VehicleApi } from '../types';
import { clearAuthToken, getAuthToken } from './auth';
import { notifyProfileUpdated } from './profile';

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';

Expand Down Expand Up @@ -79,7 +80,7 @@ export async function registerWithEmail(payload: {
address: string;
nic: string;
phone: string;
role: string;
roles: UserRole[];
}): Promise<void> {
await apiRequest('/auth/register/email', 'POST', payload);
}
Expand All @@ -97,8 +98,11 @@ export async function updateMyProfile(payload: {
address?: string;
nic?: string;
phone?: string;
roles?: UserRole[];
}): Promise<UserProfile> {
return apiRequest<UserProfile>('/users/me', 'PATCH', payload, true);
const profile = await apiRequest<UserProfile>('/users/me', 'PATCH', payload, true);
notifyProfileUpdated(profile);
return profile;
}

export async function loginSocial(idToken: string): Promise<UserProfile> {
Expand All @@ -110,7 +114,7 @@ export async function registerSocial(
address: string;
nic: string;
phone: string;
role: string;
roles: UserRole[];
},
idToken?: string,
): Promise<void> {
Expand Down Expand Up @@ -301,5 +305,7 @@ export async function uploadMyAvatar(file: File): Promise<UserProfile> {
throw new Error(errorMessage);
}

return response.json() as Promise<UserProfile>;
const profile = await response.json() as UserProfile;
notifyProfileUpdated(profile);
return profile;
}
42 changes: 37 additions & 5 deletions Client/src/lib/profile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { UserProfile, UserRole } from '../types';

export const DEFAULT_AVATAR =
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=facearea&facepad=2&w=256&h=256&q=80';
export const PROFILE_UPDATED_EVENT = 'autoshare:profile-updated';

export function getDisplayNameFromEmail(email: string): string {
const local = (email || '').split('@')[0] || 'User';
Expand All @@ -18,11 +21,35 @@ export function getProfileDisplayName(fullName: string | null | undefined, email
return getDisplayNameFromEmail(email);
}

export function getRoleLabel(role: string): string {
const normalized = role.toLowerCase();
if (normalized === 'owner') return 'Owner';
if (normalized === 'user' || normalized === 'renter') return 'Renter';
return role;
export function getNormalizedRoles(roles?: UserRole[] | null): UserRole[] {
if (!Array.isArray(roles) || roles.length === 0) {
return ['user'];
}

return Array.from(new Set(roles));
}

export function hasRole(roles: UserRole[] | null | undefined, role: UserRole): boolean {
return getNormalizedRoles(roles).includes(role);
}

export function isVehicleOwner(roles: UserRole[] | null | undefined): boolean {
return hasRole(roles, 'vehicle_owner');
}

export function getDefaultDashboardPath(profile: Pick<UserProfile, 'roles'>): string {
return isVehicleOwner(profile.roles) ? '/dashboard' : '/user-dashboard';
}

export function getRoleLabel(roles: UserRole[] | null | undefined): string {
const normalized = getNormalizedRoles(roles);
const isOwner = normalized.includes('vehicle_owner');
const isRenter = normalized.includes('renter') || normalized.includes('user');

if (isOwner && isRenter) return 'Owner & Renter';
if (isOwner) return 'Owner';
if (isRenter) return 'Renter';
return normalized.join(', ');
}

export function resolveBackendAssetUrl(assetUrl?: string | null, fallback = DEFAULT_AVATAR): string {
Expand All @@ -46,3 +73,8 @@ export function getPrimaryVehicleImage(
const candidate = (imageUrls && imageUrls.length > 0 ? imageUrls[0] : legacyImageUrl) || null;
return resolveBackendAssetUrl(candidate, fallback);
}

export function notifyProfileUpdated(profile: UserProfile): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent<UserProfile>(PROFILE_UPDATED_EVENT, { detail: profile }));
}
5 changes: 3 additions & 2 deletions Client/src/pages/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Car, Mail, Lock, ArrowLeft } from 'lucide-react';
import { loginSocial, getMyProfile, loginWithEmail } from '../lib/api';
import { setAuthToken } from '../lib/auth';
import { signInWithGooglePopup } from '../lib/firebase';
import { getDefaultDashboardPath } from '../lib/profile';

const SignIn = () => {
const navigate = useNavigate();
Expand All @@ -24,7 +25,7 @@ const SignIn = () => {
}
setAuthToken(auth.idToken);
const profile = await getMyProfile();
navigate(profile.role === 'owner' ? '/dashboard' : '/user-dashboard');
navigate(getDefaultDashboardPath(profile));
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to sign in');
} finally {
Expand All @@ -39,7 +40,7 @@ const SignIn = () => {
const { idToken } = await signInWithGooglePopup();
setAuthToken(idToken);
const profile = await loginSocial(idToken);
navigate(profile.role === 'owner' ? '/dashboard' : '/user-dashboard');
navigate(getDefaultDashboardPath(profile));
} catch (err) {
const message = err instanceof Error ? err.message : 'Unable to sign in with Google';
if (message.toLowerCase().includes('profile not found')) {
Expand Down
3 changes: 2 additions & 1 deletion Client/src/pages/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Car, Mail, Lock, User, ArrowLeft } from 'lucide-react';
import { loginSocial } from '../lib/api';
import { setAuthToken } from '../lib/auth';
import { signInWithGooglePopup } from '../lib/firebase';
import { getDefaultDashboardPath } from '../lib/profile';

const SignUp = () => {
const navigate = useNavigate();
Expand All @@ -24,7 +25,7 @@ const SignUp = () => {
const { idToken } = await signInWithGooglePopup();
setAuthToken(idToken);
const profile = await loginSocial(idToken);
navigate(profile.role === 'owner' ? '/dashboard' : '/user-dashboard');
navigate(getDefaultDashboardPath(profile));
} catch (err) {
const message = err instanceof Error ? err.message : 'Unable to sign up with Google';
if (message.toLowerCase().includes('profile not found')) {
Expand Down
Loading
Loading