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:
3
public/assets/icons/signin-user-icon.svg
Normal file
3
public/assets/icons/signin-user-icon.svg
Normal 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 |
@@ -145,7 +145,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
return true;
|
||||
},
|
||||
|
||||
async jwt({ token, user }) {
|
||||
async jwt({ token, user, trigger, session: updateData }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.name = user.name;
|
||||
@@ -158,6 +158,12 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
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)
|
||||
// Backend JWT_ACCESS_EXPIRATION=15m, so refresh at ~13 minutes
|
||||
const ACCESS_TOKEN_MAX_AGE = 13 * 60 * 1000; // 13 minutes in ms
|
||||
|
||||
@@ -19,8 +19,10 @@ const navLinks = [
|
||||
export function CommonHeader() {
|
||||
const { data: session } = useSession();
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const [showGuestMenu, setShowGuestMenu] = useState(false);
|
||||
const [showMobileMenu, setShowMobileMenu] = useState(false);
|
||||
const profileMenuRef = useRef<HTMLDivElement>(null);
|
||||
const guestMenuRef = useRef<HTMLDivElement>(null);
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [profileImage, setProfileImage] = 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)) {
|
||||
setShowProfileMenu(false);
|
||||
}
|
||||
if (guestMenuRef.current && !guestMenuRef.current.contains(event.target as Node)) {
|
||||
setShowGuestMenu(false);
|
||||
}
|
||||
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
|
||||
setShowMobileMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (showProfileMenu || showMobileMenu) {
|
||||
if (showProfileMenu || showGuestMenu || showMobileMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showProfileMenu, showMobileMenu]);
|
||||
}, [showProfileMenu, showGuestMenu, showMobileMenu]);
|
||||
|
||||
// Fetch profile data (image and name) from backend based on user role
|
||||
const fetchProfileData = useCallback(async () => {
|
||||
@@ -292,14 +297,83 @@ export function CommonHeader() {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/login" className="hover:opacity-80 transition-opacity">
|
||||
<Image
|
||||
src="/assets/icons/profile-circle-orange-icon.svg"
|
||||
alt="Login"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</Link>
|
||||
<div className="relative" ref={guestMenuRef}>
|
||||
<button
|
||||
onClick={() => setShowGuestMenu(!showGuestMenu)}
|
||||
className="flex items-center gap-2 md:gap-3 hover:opacity-80 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src="/assets/icons/signin-user-icon.svg"
|
||||
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 */}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function SpecializationSection({ fieldsData }: SpecializationSectionProps
|
||||
|
||||
{/* Top row - up to 3 cards */}
|
||||
{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 === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' :
|
||||
'lg:grid-cols-3'
|
||||
@@ -54,7 +54,7 @@ export function SpecializationSection({ fieldsData }: SpecializationSectionProps
|
||||
|
||||
{/* Bottom row - remaining cards */}
|
||||
{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 === 2 ? 'lg:grid-cols-2 lg:max-w-2xl lg:mx-auto' :
|
||||
'lg:grid-cols-3'
|
||||
|
||||
Reference in New Issue
Block a user