feat: Redesign header with mobile menu and authenticated user profile, add search input validation to the hero section, and enhance user session data.
This commit is contained in:
4
public/assets/icons/close-icon.svg
Normal file
4
public/assets/icons/close-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 18L18 6" stroke="#00293D" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M6 6L18 18" stroke="#00293D" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 267 B |
5
public/assets/icons/menu-icon.svg
Normal file
5
public/assets/icons/menu-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 6H21" stroke="#00293D" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M3 12H21" stroke="#00293D" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M3 18H21" stroke="#00293D" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 342 B |
@@ -107,6 +107,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
|
token.name = user.name;
|
||||||
|
token.email = user.email;
|
||||||
|
token.picture = user.image;
|
||||||
token.role = (user as any).role;
|
token.role = (user as any).role;
|
||||||
token.accessToken = (user as any).accessToken;
|
token.accessToken = (user as any).accessToken;
|
||||||
token.refreshToken = (user as any).refreshToken;
|
token.refreshToken = (user as any).refreshToken;
|
||||||
@@ -117,6 +120,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
(session.user as any).id = token.id;
|
(session.user as any).id = token.id;
|
||||||
|
session.user.name = token.name as string;
|
||||||
|
session.user.email = token.email as string;
|
||||||
|
session.user.image = token.picture as string;
|
||||||
(session.user as any).role = token.role;
|
(session.user as any).role = token.role;
|
||||||
(session.user as any).accessToken = token.accessToken;
|
(session.user as any).accessToken = token.accessToken;
|
||||||
(session.user as any).refreshToken = token.refreshToken;
|
(session.user as any).refreshToken = token.refreshToken;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function HeroSection() {
|
|||||||
location: '',
|
location: '',
|
||||||
});
|
});
|
||||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch agent types on mount
|
// Fetch agent types on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,6 +31,19 @@ export function HeroSection() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
// Validate that at least one field has a value
|
||||||
|
const hasType = searchParams.type.trim() !== '';
|
||||||
|
const hasName = searchParams.name.trim() !== '';
|
||||||
|
const hasLocation = searchParams.location.trim() !== '';
|
||||||
|
|
||||||
|
if (!hasType && !hasName && !hasLocation) {
|
||||||
|
setValidationError('Please enter at least one search criteria (Type, Name/Keyword, or Location)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any previous validation error
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchParams.type) params.set('type', searchParams.type);
|
if (searchParams.type) params.set('type', searchParams.type);
|
||||||
if (searchParams.name) params.set('name', searchParams.name);
|
if (searchParams.name) params.set('name', searchParams.name);
|
||||||
@@ -48,6 +62,12 @@ export function HeroSection() {
|
|||||||
const selectType = (value: string) => {
|
const selectType = (value: string) => {
|
||||||
setSearchParams({ ...searchParams, type: value });
|
setSearchParams({ ...searchParams, type: value });
|
||||||
setOpenDropdown(null);
|
setOpenDropdown(null);
|
||||||
|
setValidationError(null); // Clear error when user selects a type
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: 'name' | 'location', value: string) => {
|
||||||
|
setSearchParams({ ...searchParams, [field]: value });
|
||||||
|
setValidationError(null); // Clear error when user starts typing
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,7 +136,7 @@ export function HeroSection() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Name or Keyword"
|
placeholder="Name or Keyword"
|
||||||
value={searchParams.name}
|
value={searchParams.name}
|
||||||
onChange={(e) => setSearchParams({ ...searchParams, name: e.target.value })}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +147,7 @@ export function HeroSection() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Location"
|
placeholder="Location"
|
||||||
value={searchParams.location}
|
value={searchParams.location}
|
||||||
onChange={(e) => setSearchParams({ ...searchParams, location: e.target.value })}
|
onChange={(e) => handleInputChange('location', e.target.value)}
|
||||||
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,6 +166,15 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Error Message */}
|
||||||
|
{validationError && (
|
||||||
|
<div className="mt-3 text-center">
|
||||||
|
<p className="text-white font-serif text-sm bg-red-500/90 rounded-[7px] py-2 px-4 inline-block">
|
||||||
|
{validationError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Section - pushed to bottom */}
|
{/* Bottom Section - pushed to bottom */}
|
||||||
|
|||||||
@@ -3,79 +3,171 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { Button } from '../ui/Button';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// Get first name from session
|
||||||
|
const firstName = session?.user?.name?.split(' ')[0] || 'User';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-[#00293d] text-white">
|
<header className="bg-[#5d7a87]">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-20">
|
<div className="flex items-center justify-between h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src="/assets/logo-white.svg"
|
src="/assets/logo-white.svg"
|
||||||
alt="RE-Quest"
|
alt="RE-Quest"
|
||||||
width={150}
|
width={184}
|
||||||
height={40}
|
height={44}
|
||||||
className="h-10 w-auto"
|
className="h-11 w-auto"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation */}
|
{session ? (
|
||||||
<nav className="hidden md:flex items-center gap-8">
|
/* ============ AUTHENTICATED HEADER ============ */
|
||||||
<Link href="/education" className="hover:text-[#f5a623] transition-colors">
|
<>
|
||||||
Education
|
{/* Navigation - Desktop */}
|
||||||
</Link>
|
<nav className="hidden md:flex items-center gap-10">
|
||||||
<Link href="/about" className="hover:text-[#f5a623] transition-colors">
|
<Link
|
||||||
About Us
|
href="/education"
|
||||||
</Link>
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors"
|
||||||
<Link href="/faq" className="hover:text-[#f5a623] transition-colors">
|
>
|
||||||
FAQ's
|
Education
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors"
|
||||||
|
>
|
||||||
|
About Us
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/faq"
|
||||||
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors"
|
||||||
|
>
|
||||||
|
FAQ's
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{/* Right Side */}
|
{/* Right Side - Authenticated */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-5">
|
||||||
{session ? (
|
|
||||||
<>
|
|
||||||
{/* Notification Bell */}
|
{/* Notification Bell */}
|
||||||
<button className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
<Link
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
href="/notifications"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
className="relative hover:opacity-80 transition-opacity"
|
||||||
</svg>
|
>
|
||||||
</button>
|
<Image
|
||||||
{/* Profile */}
|
src="/assets/icons/notification-bell-icon.svg"
|
||||||
<Link href="/user/dashboard" className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
alt="Notifications"
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
width={20}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
height={22}
|
||||||
</svg>
|
/>
|
||||||
|
{/* Red notification dot */}
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-red-500 rounded-full border border-[#5d7a87]"></span>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Link href="/login">
|
|
||||||
<Button variant="outline" size="sm" className="border-white text-white hover:bg-white hover:text-[#00293d]">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/register">
|
|
||||||
<Button variant="primary" size="sm">
|
|
||||||
Sign Up
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Profile Section */}
|
||||||
<button className="md:hidden p-2 hover:bg-white/10 rounded-lg transition-colors">
|
<Link href="/user/dashboard" className="flex items-center gap-3">
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{/* Profile Avatar */}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<div className="w-[35px] h-[35px] rounded-full overflow-hidden bg-gray-300 flex-shrink-0">
|
||||||
</svg>
|
{session.user?.image ? (
|
||||||
</button>
|
<Image
|
||||||
</div>
|
src={session.user.image}
|
||||||
|
alt="Profile"
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/user-placeholder-icon.svg"
|
||||||
|
alt="Profile"
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Greeting Text */}
|
||||||
|
<div className="hidden sm:flex flex-col">
|
||||||
|
<span className="font-bold text-[14px] text-[#e58625] leading-tight">
|
||||||
|
Hi {firstName} !
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-[9px] text-[#00293d] leading-tight">
|
||||||
|
Welcome back !
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
className="md:hidden p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/close-icon.svg"
|
||||||
|
alt="Close menu"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/menu-icon.svg"
|
||||||
|
alt="Open menu"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* ============ NON-AUTHENTICATED HEADER ============ */
|
||||||
|
/* Simple: Just logo + Get Started button */
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="bg-[#e58625] hover:bg-[#d4780f] text-white font-bold text-[14px] px-6 py-2.5 rounded-[15px] transition-colors"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Dropdown - Authenticated Only */}
|
||||||
|
{session && mobileMenuOpen && (
|
||||||
|
<div className="md:hidden pb-4 border-t border-white/20 mt-2 pt-4">
|
||||||
|
<nav className="flex flex-col gap-3">
|
||||||
|
<Link
|
||||||
|
href="/education"
|
||||||
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors py-2"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
Education
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors py-2"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
About Us
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/faq"
|
||||||
|
className="font-bold text-[15px] text-[#00293d] hover:text-[#e58625] transition-colors py-2"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
FAQ's
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user