feat: Implement guest menu in the common header for unauthenticated users and enhance JWT session handling for user profile updates.

This commit is contained in:
pradeepkumar
2026-03-13 12:41:35 +05:30
parent 45adaf2deb
commit b339dd865c
4 changed files with 96 additions and 13 deletions

View File

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 20 22.4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M5.55556 4.97778C5.55556 3.65759 6.02381 2.39147 6.8573 1.45796C7.6908 0.524443 8.82126 0 10 0C11.1787 0 12.3092 0.524443 13.1427 1.45796C13.9762 2.39147 14.4444 3.65759 14.4444 4.97778C14.4444 6.29797 13.9762 7.56408 13.1427 8.4976C12.3092 9.43111 11.1787 9.95555 10 9.95555C8.82126 9.95555 7.6908 9.43111 6.8573 8.4976C6.02381 7.56408 5.55556 6.29797 5.55556 4.97778ZM5.55556 12.4444C4.08213 12.4444 2.66905 13.1 1.62718 14.2669C0.585316 15.4338 0 17.0164 0 18.6667C0 19.6568 0.35119 20.6064 0.976311 21.3065C1.60143 22.0067 2.44928 22.4 3.33333 22.4H16.6667C17.5507 22.4 18.3986 22.0067 19.0237 21.3065C19.6488 20.6064 20 19.6568 20 18.6667C20 17.0164 19.4147 15.4338 18.3728 14.2669C17.3309 13.1 15.9179 12.4444 14.4444 12.4444H5.55556Z" fill="var(--fill-0, #E58625)"/>
</svg>

After

Width:  |  Height:  |  Size: 1014 B

View File

@@ -145,7 +145,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
return true; return true;
}, },
async jwt({ token, user }) { async jwt({ token, user, trigger, session: updateData }) {
if (user) { if (user) {
token.id = user.id; token.id = user.id;
token.name = user.name; token.name = user.name;
@@ -158,6 +158,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
token.accessTokenIssuedAt = Date.now(); token.accessTokenIssuedAt = Date.now();
} }
// Handle session updates from client-side updateSession() calls
if (trigger === "update" && updateData?.user) {
if (updateData.user.name) token.name = updateData.user.name;
if (updateData.user.image !== undefined) token.picture = updateData.user.image;
}
// Auto-refresh the access token if it's about to expire (refresh 2 minutes before expiry) // Auto-refresh the access token if it's about to expire (refresh 2 minutes before expiry)
// Backend JWT_ACCESS_EXPIRATION=15m, so refresh at ~13 minutes // Backend JWT_ACCESS_EXPIRATION=15m, so refresh at ~13 minutes
const ACCESS_TOKEN_MAX_AGE = 13 * 60 * 1000; // 13 minutes in ms const ACCESS_TOKEN_MAX_AGE = 13 * 60 * 1000; // 13 minutes in ms

View File

@@ -19,8 +19,10 @@ const navLinks = [
export function CommonHeader() { export function CommonHeader() {
const { data: session } = useSession(); const { data: session } = useSession();
const [showProfileMenu, setShowProfileMenu] = useState(false); const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showGuestMenu, setShowGuestMenu] = useState(false);
const [showMobileMenu, setShowMobileMenu] = useState(false); const [showMobileMenu, setShowMobileMenu] = useState(false);
const profileMenuRef = useRef<HTMLDivElement>(null); const profileMenuRef = useRef<HTMLDivElement>(null);
const guestMenuRef = useRef<HTMLDivElement>(null);
const mobileMenuRef = useRef<HTMLDivElement>(null); const mobileMenuRef = useRef<HTMLDivElement>(null);
const [profileImage, setProfileImage] = useState<string | null>(null); const [profileImage, setProfileImage] = useState<string | null>(null);
const [profileName, setProfileName] = useState<string | null>(null); const [profileName, setProfileName] = useState<string | null>(null);
@@ -32,19 +34,22 @@ export function CommonHeader() {
if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) { if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) {
setShowProfileMenu(false); setShowProfileMenu(false);
} }
if (guestMenuRef.current && !guestMenuRef.current.contains(event.target as Node)) {
setShowGuestMenu(false);
}
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) { if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
setShowMobileMenu(false); setShowMobileMenu(false);
} }
} }
if (showProfileMenu || showMobileMenu) { if (showProfileMenu || showGuestMenu || showMobileMenu) {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
} }
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [showProfileMenu, showMobileMenu]); }, [showProfileMenu, showGuestMenu, showMobileMenu]);
// Fetch profile data (image and name) from backend based on user role // Fetch profile data (image and name) from backend based on user role
const fetchProfileData = useCallback(async () => { const fetchProfileData = useCallback(async () => {
@@ -292,14 +297,83 @@ export function CommonHeader() {
</div> </div>
</> </>
) : ( ) : (
<Link href="/login" className="hover:opacity-80 transition-opacity"> <div className="relative" ref={guestMenuRef}>
<Image <button
src="/assets/icons/profile-circle-orange-icon.svg" onClick={() => setShowGuestMenu(!showGuestMenu)}
alt="Login" className="flex items-center gap-2 md:gap-3 hover:opacity-80 transition-opacity cursor-pointer"
width={32} >
height={32} <Image
/> src="/assets/icons/signin-user-icon.svg"
</Link> alt="Sign in"
width={20}
height={22}
/>
<div className="hidden sm:flex flex-col text-left">
<span className="font-fractul font-bold text-[14px] text-[#e58625] leading-tight">
Sign in
</span>
<span className="font-fractul font-bold text-[10px] text-[#00293d] leading-tight">
Sign into connect
</span>
</div>
</button>
{showGuestMenu && (
<div className="absolute right-0 top-full mt-3 w-[271px] bg-white rounded-[15px] border border-black/20 z-50 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]">
<div className="absolute -top-2 right-6 w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-b-[8px] border-b-white"></div>
<div className="flex flex-col items-center pt-6 pb-4 border-b border-black/10">
<Image
src="/assets/icons/signin-user-icon.svg"
alt="User"
width={36}
height={40}
/>
<p className="font-serif text-[14px] text-black/50 mt-3">
Sign in or create account to continue
</p>
</div>
<Link
href="/login"
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowGuestMenu(false)}
>
<Image src="/assets/icons/settings-icon.svg" alt="Settings" width={24} height={24} />
<div className="flex flex-col">
<p className="font-fractul text-[16px] leading-[18px] text-black">Account Settings</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">Profile, Security</p>
</div>
</Link>
<Link
href="/login"
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowGuestMenu(false)}
>
<Image src="/assets/icons/notification-settings-icon.svg" alt="Notifications" width={24} height={24} />
<p className="font-fractul text-[16px] leading-[18px] text-black">Notification Settings</p>
</Link>
<div className="flex gap-3 px-4 py-4">
<Link
href="/signup"
onClick={() => setShowGuestMenu(false)}
className="flex-1 h-[37px] bg-[#e58625] rounded-[15px] flex items-center justify-center font-fractul text-[14px] text-white hover:bg-[#d47920] transition-colors"
>
Sign up
</Link>
<Link
href="/login"
onClick={() => setShowGuestMenu(false)}
className="flex-1 h-[37px] border border-[#e58625] rounded-[15px] flex items-center justify-center font-fractul text-[14px] text-[#e58625] hover:bg-[#e58625]/5 transition-colors"
>
Login
</Link>
</div>
</div>
)}
</div>
)} )}
{/* Mobile Menu Button */} {/* Mobile Menu Button */}

View File

@@ -29,7 +29,7 @@ export function SpecializationSection({ fieldsData }: SpecializationSectionProps
{/* Top row - up to 3 cards */} {/* Top row - up to 3 cards */}
{topRowFields.length > 0 && ( {topRowFields.length > 0 && (
<div className={`flex flex-col lg:grid lg:items-start gap-6 mb-6 ${ <div className={`flex flex-col lg:grid lg:items-stretch gap-6 mb-6 ${
topRowFields.length === 1 ? 'lg:grid-cols-1 lg:max-w-md lg:mx-auto' : topRowFields.length === 1 ? 'lg:grid-cols-1 lg:max-w-md lg:mx-auto' :
topRowFields.length === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' : topRowFields.length === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' :
'lg:grid-cols-3' 'lg:grid-cols-3'
@@ -54,7 +54,7 @@ export function SpecializationSection({ fieldsData }: SpecializationSectionProps
{/* Bottom row - remaining cards */} {/* Bottom row - remaining cards */}
{bottomRowFields.length > 0 && ( {bottomRowFields.length > 0 && (
<div className={`flex flex-col lg:grid lg:items-start gap-6 ${ <div className={`flex flex-col lg:grid lg:items-stretch gap-6 ${
bottomRowFields.length === 1 ? 'lg:grid-cols-1 lg:max-w-md lg:mx-auto' : bottomRowFields.length === 1 ? 'lg:grid-cols-1 lg:max-w-md lg:mx-auto' :
bottomRowFields.length === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' : bottomRowFields.length === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' :
'lg:grid-cols-3' 'lg:grid-cols-3'