Finished the Navbar and product dropdown issue
This commit is contained in:
parent
1844fb8b45
commit
4508039803
@ -7,6 +7,7 @@ import OptimizedImage from '../common/OptimizedImage';
|
||||
import { menuItems, languages, mobileMenuVariants } from './constants/navigationData';
|
||||
import { headerSchema } from './schema/headerSchema';
|
||||
import styles from './styles/Header.module.css';
|
||||
import baseStyles from './styles/base.module.css';
|
||||
import Logo from '@/assets/Logo/Tech4biz-logo.webp';
|
||||
|
||||
const SearchOverlay = lazy(() => import('./components/SearchOverlay'));
|
||||
@ -15,7 +16,7 @@ const MobileNav = lazy(() => import('./components/MobileNav'));
|
||||
const LanguageSelector = lazy(() => import('./components/LanguageSelector'));
|
||||
|
||||
const Navbar = () => {
|
||||
// State management with hooks
|
||||
// State management remains the same
|
||||
const [state, setState] = useState({
|
||||
isProductsOpen: false,
|
||||
isLanguageOpen: false,
|
||||
@ -26,10 +27,11 @@ const Navbar = () => {
|
||||
isSticky: false
|
||||
});
|
||||
|
||||
// Refs and handlers remain the same
|
||||
const desktopLanguageRef = useRef(null);
|
||||
const mobileLanguageRef = useRef(null);
|
||||
|
||||
// Event handlers with useCallback
|
||||
// Event handlers with useCallback remain the same
|
||||
const handleClickOutside = useCallback((event) => {
|
||||
if (desktopLanguageRef.current && !desktopLanguageRef.current.contains(event.target)) {
|
||||
setState(prev => ({ ...prev, isLanguageOpen: false }));
|
||||
@ -43,12 +45,11 @@ const Navbar = () => {
|
||||
setState(prev => ({ ...prev, isSticky: window.scrollY > 0 }));
|
||||
}, []);
|
||||
|
||||
// Effect for event listeners
|
||||
// Effects remain the same
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Inject schema
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(headerSchema);
|
||||
@ -64,13 +65,15 @@ const Navbar = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SearchOverlay isOpen={state.isSearchOpen} onClose={() => setState(prev => ({ ...prev, isSearchOpen: false }))} />
|
||||
<SearchOverlay
|
||||
isOpen={state.isSearchOpen}
|
||||
onClose={() => setState(prev => ({ ...prev, isSearchOpen: false }))}
|
||||
/>
|
||||
|
||||
<nav className={`${styles.navContainer} ${state.isSticky ? styles.stickyNav : ''}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Left section with logo and mobile actions */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={baseStyles.container}>
|
||||
<div className={`${baseStyles.flexBetween} h-16`}>
|
||||
<div className={`${baseStyles.flexCenter} space-x-4`}>
|
||||
<Link to="/" className="flex-shrink-0">
|
||||
<OptimizedImage
|
||||
src={Logo}
|
||||
@ -82,108 +85,69 @@ const Navbar = () => {
|
||||
priority={true}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
|
||||
{/* Mobile search and language */}
|
||||
<div className="flex items-center space-x-4 md:hidden">
|
||||
<div className="md:hidden flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))}
|
||||
className="p-2 text-gray-600 hover:text-[#1E0E62] transition-colors"
|
||||
className={styles.searchButton}
|
||||
aria-label="Search"
|
||||
>
|
||||
<FiSearch className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div ref={mobileLanguageRef} className="relative">
|
||||
<button
|
||||
className="flex items-center justify-between text-gray-600 hover:text-[#1E0E62] transition-colors border rounded-md px-2 py-1 w-[60px]"
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
isMobileLanguageOpen: !prev.isMobileLanguageOpen
|
||||
}))}
|
||||
aria-label="Select language"
|
||||
>
|
||||
<span className="font-poppins font-bold text-[14px] truncate">
|
||||
{state.selectedLanguage.substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
<FiChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{state.isMobileLanguageOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
className="absolute left-0 mt-2 w-24 bg-white rounded-md shadow-lg py-1"
|
||||
role="listbox"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-[#1E0E62]/5 font-poppins font-bold text-[14px]"
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
selectedLanguage: lang,
|
||||
isMobileLanguageOpen: false
|
||||
}))}
|
||||
role="option"
|
||||
aria-selected={state.selectedLanguage === lang}
|
||||
>
|
||||
{lang}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<DesktopNav {...state} setIsProductsOpen={(value) =>
|
||||
setState(prev => ({ ...prev, isProductsOpen: value }))}
|
||||
/>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Desktop search and language */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
<button
|
||||
onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))}
|
||||
className={styles.searchButton}
|
||||
aria-label="Open search"
|
||||
>
|
||||
<FiSearch className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<LanguageSelector
|
||||
isOpen={state.isLanguageOpen}
|
||||
isDesktop={false}
|
||||
isOpen={state.isMobileLanguageOpen}
|
||||
selectedLanguage={state.selectedLanguage}
|
||||
languageRef={desktopLanguageRef}
|
||||
languageRef={mobileLanguageRef}
|
||||
setState={setState}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="md:hidden"
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
isMobileMenuOpen: !prev.isMobileMenuOpen
|
||||
}))}
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
{state.isMobileMenuOpen ? (
|
||||
<FiX className="h-6 w-6" />
|
||||
) : (
|
||||
<FiMenu className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DesktopNav
|
||||
isProductsOpen={state.isProductsOpen}
|
||||
setIsProductsOpen={(value) => setState(prev => ({ ...prev, isProductsOpen: value }))}
|
||||
/>
|
||||
|
||||
{/* Desktop search and language */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))}
|
||||
className={styles.searchButton}
|
||||
aria-label="Search"
|
||||
>
|
||||
<FiSearch className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<LanguageSelector
|
||||
isDesktop={true}
|
||||
isOpen={state.isLanguageOpen}
|
||||
selectedLanguage={state.selectedLanguage}
|
||||
languageRef={desktopLanguageRef}
|
||||
setState={setState}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu toggle button */}
|
||||
<button
|
||||
className="md:hidden text-gray-600 hover:text-[#1E0E62] transition-colors"
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
isMobileMenuOpen: !prev.isMobileMenuOpen
|
||||
}))}
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
{state.isMobileMenuOpen ? (
|
||||
<FiX className="h-6 w-6" />
|
||||
) : (
|
||||
<FiMenu className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<MobileNav
|
||||
isMobileMenuOpen={state.isMobileMenuOpen}
|
||||
setState={setState}
|
||||
|
||||
@ -4,14 +4,13 @@ import { FiChevronDown } from 'react-icons/fi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { menuItems } from '../constants/navigationData';
|
||||
import ProductShowcase from './ProductShowcase';
|
||||
import styles from '../styles/DesktopNav.module.css';
|
||||
import baseStyles from '../styles/base.module.css';
|
||||
|
||||
const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
|
||||
const handleMouseEnter = () => {
|
||||
setIsProductsOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsProductsOpen(false);
|
||||
const handleProductsClick = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsProductsOpen(!isProductsOpen);
|
||||
};
|
||||
|
||||
const getItemPath = (item) => {
|
||||
@ -30,19 +29,17 @@ const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hidden md:flex items-center space-x-8 max-[956px]:space-x-5">
|
||||
<div className={styles.container}>
|
||||
{menuItems.map((item) => (
|
||||
<div key={item} className="relative">
|
||||
{item === 'PRODUCTS' ? (
|
||||
<div
|
||||
className="static"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
className="flex items-center text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px]"
|
||||
className={styles.menuButton}
|
||||
onClick={handleProductsClick}
|
||||
aria-expanded={isProductsOpen}
|
||||
aria-haspopup="true"
|
||||
data-products-trigger
|
||||
>
|
||||
{item}
|
||||
<FiChevronDown
|
||||
@ -51,13 +48,13 @@ const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
|
||||
/>
|
||||
</button>
|
||||
<AnimatePresence mode="wait">
|
||||
{isProductsOpen && <ProductShowcase />}
|
||||
{isProductsOpen && <ProductShowcase setIsProductsOpen={setIsProductsOpen} />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={getItemPath(item)}
|
||||
className="text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px]"
|
||||
className={styles.menuLink}
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { memo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FiChevronDown } from 'react-icons/fi';
|
||||
import { languages } from '../constants/navigationData';
|
||||
import styles from '../styles/Header.module.css';
|
||||
import styles from '../styles/LanguageSelector.module.css';
|
||||
|
||||
const LanguageSelector = memo(({
|
||||
isDesktop = true,
|
||||
@ -11,12 +11,8 @@ const LanguageSelector = memo(({
|
||||
languageRef,
|
||||
setState
|
||||
}) => {
|
||||
const containerClasses = isDesktop
|
||||
? "hidden md:block relative"
|
||||
: "relative";
|
||||
|
||||
return (
|
||||
<div ref={languageRef} className={containerClasses}>
|
||||
<div ref={languageRef} className={`${styles.container} ${isDesktop ? 'hidden md:block' : ''}`}>
|
||||
<button
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
@ -36,13 +32,13 @@ const LanguageSelector = memo(({
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
className="absolute right-[-35px] mt-2 w-40 bg-white rounded-md shadow-lg z-50"
|
||||
className={styles.dropdown}
|
||||
role="listbox"
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={language}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
className={styles.dropdownItem}
|
||||
onClick={() => setState(prev => ({
|
||||
...prev,
|
||||
selectedLanguage: language,
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FiChevronDown, FiX } from 'react-icons/fi';
|
||||
import { FiChevronDown } from 'react-icons/fi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { menuItems, mobileMenuVariants } from '../constants/navigationData';
|
||||
import styles from '../styles/Header.module.css';
|
||||
import MobileProductShowcase from './MobileProductShowcase';
|
||||
import styles from '../styles/MobileNav.module.css';
|
||||
|
||||
const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
|
||||
const [isProductsOpen, setIsProductsOpen] = useState(false);
|
||||
|
||||
const getItemPath = (item) => {
|
||||
switch (item) {
|
||||
case 'HOME':
|
||||
return '/';
|
||||
case 'SERVICES':
|
||||
return '/services';
|
||||
case 'ABOUT':
|
||||
return '/about';
|
||||
case 'CONTACT':
|
||||
return '/contact';
|
||||
default:
|
||||
return '#';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
@ -14,7 +33,7 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
|
||||
exit="closed"
|
||||
variants={mobileMenuVariants}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="md:hidden fixed top-16 right-0 h-[calc(100vh-64px)] w-full sm:w-[350px] z-30 bg-white shadow-lg overflow-y-auto"
|
||||
className={styles.container}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Mobile navigation menu"
|
||||
@ -22,14 +41,43 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="overflow-y-auto">
|
||||
{menuItems.map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href="#"
|
||||
className="block px-4 py-3 text-gray-600 hover:text-[#1E0E62] hover:bg-[#1E0E62]/5 font-poppins font-bold text-[14px]"
|
||||
onClick={() => setState(prev => ({ ...prev, isMobileMenuOpen: false }))}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
<div key={item}>
|
||||
{item === 'PRODUCTS' ? (
|
||||
<>
|
||||
<button
|
||||
className={styles.menuButton}
|
||||
onClick={() => setIsProductsOpen(!isProductsOpen)}
|
||||
aria-expanded={isProductsOpen}
|
||||
>
|
||||
{item}
|
||||
<FiChevronDown
|
||||
className={`ml-1 transition-transform duration-300 ${isProductsOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isProductsOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<MobileProductShowcase />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to={getItemPath(item)}
|
||||
className={styles.menuLink}
|
||||
onClick={() => setState(prev => ({ ...prev, isMobileMenuOpen: false }))}
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -39,6 +87,4 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
|
||||
);
|
||||
});
|
||||
|
||||
MobileNav.displayName = 'MobileNav';
|
||||
|
||||
export default MobileNav;
|
||||
49
src/components/Header/components/MobileProductShowcase.jsx
Normal file
49
src/components/Header/components/MobileProductShowcase.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { products } from '../constants/navigationData';
|
||||
import styles from '../styles/MobileProductShowcase.module.css';
|
||||
|
||||
const MobileProductShowcase = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.productList}>
|
||||
{products.map((product) => (
|
||||
<motion.div
|
||||
key={product.id}
|
||||
className={styles.productCard}
|
||||
whileHover={{ x: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<h3 className={styles.productTitle}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className={styles.productDescription}>
|
||||
{product.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<motion.button
|
||||
className={styles.viewAllButton}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
View All Products
|
||||
<svg
|
||||
className="ml-2 w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileProductShowcase;
|
||||
@ -1,107 +1,74 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Import images
|
||||
import lmsImage from '@/assets/Header/Product-Card/learning-management-system.webp';
|
||||
import codenukImage from '@/assets/Header/Product-Card/codenuk-coding-solution-platform.webp';
|
||||
import cloudtopiaaImage from '@/assets/Header/Product-Card/cloudtopiaa-enterprise-solutions.webp';
|
||||
import cloudDriveImage from '@/assets/Header/Product-Card/cloud-drive-storage-platform.webp';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import styles from '../styles/ProductShowcase.module.css';
|
||||
import baseStyles from '../styles/base.module.css';
|
||||
import { products, productList } from '../constants/navigationData';
|
||||
|
||||
const containerVariants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transformOrigin: "top"
|
||||
y: -10,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
y: 0,
|
||||
transition: {
|
||||
height: {
|
||||
duration: 0.3,
|
||||
ease: "easeOut"
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.2,
|
||||
ease: "easeOut"
|
||||
}
|
||||
duration: 0.15,
|
||||
ease: "easeOut"
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
y: -10,
|
||||
transition: {
|
||||
height: {
|
||||
duration: 0.3,
|
||||
ease: "easeIn"
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.2,
|
||||
ease: "easeIn"
|
||||
}
|
||||
duration: 0.15,
|
||||
ease: "easeIn"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ProductShowcase = () => {
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'CloudDriv',
|
||||
image: cloudDriveImage,
|
||||
description: 'Secure cloud storage for seamless sharing and collaboration.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Codenuk',
|
||||
image: codenukImage,
|
||||
description: 'Breakthrough coding challenges instantly with Codenuk.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'LMS',
|
||||
image: lmsImage,
|
||||
description: 'Streamline education delivery with our comprehensive Learning Management System.'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Cloudtopiaa',
|
||||
image: cloudtopiaaImage,
|
||||
description: 'Secure, scalable cloud solutions that power your business growth.'
|
||||
}
|
||||
];
|
||||
const ProductShowcase = ({ setIsProductsOpen }) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState(0);
|
||||
const [isFirstItemLocked, setIsFirstItemLocked] = useState(true);
|
||||
const showcaseRef = useRef(null);
|
||||
|
||||
const productList = [
|
||||
"Product name one",
|
||||
"Product name two",
|
||||
"Product name three",
|
||||
"Product name four",
|
||||
"Product name five",
|
||||
"Product name six",
|
||||
"Product name seven",
|
||||
"Product name eight"
|
||||
];
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
showcaseRef.current &&
|
||||
!showcaseRef.current.contains(event.target) &&
|
||||
!event.target.closest('[data-products-trigger]')
|
||||
) {
|
||||
setIsProductsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
}, [setIsProductsOpen]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={showcaseRef}
|
||||
variants={containerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className="fixed left-0 right-0 w-full bg-white shadow-lg z-50 border-b-[5px] border-[#2563EB] overflow-hidden"
|
||||
className={styles.container}
|
||||
style={{ top: '64px' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className={baseStyles.container}>
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:w-2/3">
|
||||
<div className={styles.productGrid}>
|
||||
{products.map((product) => (
|
||||
<motion.div
|
||||
key={product.id}
|
||||
className="group cursor-pointer"
|
||||
className={`group ${styles.productCard}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="hidden md:block relative overflow-hidden rounded-lg">
|
||||
<div className={styles.productImage}>
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
@ -109,43 +76,88 @@ const ProductShowcase = () => {
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mt-4 text-xl font-semibold text-indigo-900">
|
||||
<h3 className={styles.productTitle}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-600 text-sm">
|
||||
<p className={styles.productDescription}>
|
||||
{product.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop view for All Products */}
|
||||
<div className="hidden lg:block lg:w-1/3">
|
||||
<h2 className="text-2xl font-bold text-indigo-900 mb-6">All Products</h2>
|
||||
<h2 className={styles.sidebarTitle}>All Products</h2>
|
||||
<div className="space-y-4">
|
||||
{productList.map((product, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="cursor-pointer"
|
||||
whileHover={{ x: 10 }}
|
||||
className={styles.sidebarItem}
|
||||
whileHover={{ x: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-indigo-900">
|
||||
{product}
|
||||
</h3>
|
||||
{index === 0 && (
|
||||
<>
|
||||
<p className="text-gray-600 text-sm mt-2">
|
||||
Breakthrough coding challenges instantly with Codenuk. Secure cloud storage for seamless sharing and collaboration.
|
||||
</p>
|
||||
<button className="text-indigo-600 text-sm mt-2 hover:text-indigo-800">
|
||||
Read more
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.sidebarContent}>
|
||||
<h3 className={styles.productTitle}>
|
||||
{product.name}
|
||||
</h3>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{hoveredIndex === index && (
|
||||
<motion.div
|
||||
className={styles.expandedContent}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{
|
||||
height: "auto",
|
||||
opacity: 1,
|
||||
transition: {
|
||||
height: { duration: 0.2 },
|
||||
opacity: { duration: 0.1, delay: 0.1 }
|
||||
}
|
||||
}}
|
||||
exit={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
height: { duration: 0.2 },
|
||||
opacity: { duration: 0.1 }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className={styles.productDescription}>
|
||||
{product.description}
|
||||
</p>
|
||||
<motion.button
|
||||
className="text-[#6B7280] text-sm mt-2 hover:text-[#1E0E62] font-poppins inline-flex items-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
Read more
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile/Tablet view with View All Products button */}
|
||||
<div className="lg:hidden w-full">
|
||||
<motion.button
|
||||
className={styles.viewAllButton}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setIsProductsOpen(false)}
|
||||
>
|
||||
View All Products
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useCallback, memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FiSearch, FiX } from 'react-icons/fi';
|
||||
import { debounce } from 'lodash';
|
||||
import styles from '../styles/SearchOverlay.module.css';
|
||||
import baseStyles from '../styles/base.module.css';
|
||||
|
||||
const SearchOverlay = memo(({ isOpen, onClose }) => {
|
||||
const handleSearch = useCallback(
|
||||
debounce((searchTerm) => {
|
||||
// Implement search logic here
|
||||
console.log('Searching for:', searchTerm);
|
||||
}, 300),
|
||||
[]
|
||||
@ -21,7 +22,7 @@ const SearchOverlay = memo(({ isOpen, onClose }) => {
|
||||
animate={{ opacity: 0.4 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed inset-0 bg-white z-40"
|
||||
className={styles.overlay}
|
||||
onClick={onClose}
|
||||
role="presentation"
|
||||
/>
|
||||
@ -31,24 +32,24 @@ const SearchOverlay = memo(({ isOpen, onClose }) => {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed top-0 left-0 right-0 bg-white z-50"
|
||||
className={styles.container}
|
||||
role="dialog"
|
||||
aria-label="Search overlay"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center h-16 border-b border-gray-200">
|
||||
<div className={baseStyles.container}>
|
||||
<div className={`${baseStyles.flexCenter} h-16 border-b border-gray-200`}>
|
||||
<FiSearch className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="What are you looking for?"
|
||||
className="flex-1 px-4 text-gray-600 placeholder-gray-400 bg-transparent border-none focus:outline-none focus:ring-0"
|
||||
className={styles.searchInput}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
aria-label="Search input"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-[#1E0E62] transition-colors"
|
||||
className={styles.closeButton}
|
||||
aria-label="Close search"
|
||||
>
|
||||
<FiX className="h-5 w-5" />
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
import lmsImage from '@/assets/Header/Product-Card/learning-management-system.webp';
|
||||
import codenukImage from '@/assets/Header/Product-Card/codenuk-coding-solution-platform.webp';
|
||||
import cloudtopiaaImage from '@/assets/Header/Product-Card/cloudtopiaa-enterprise-solutions.webp';
|
||||
import cloudDriveImage from '@/assets/Header/Product-Card/cloud-drive-storage-platform.webp';
|
||||
|
||||
export const menuItems = [
|
||||
'HOME',
|
||||
'PRODUCTS',
|
||||
@ -5,9 +10,72 @@ export const menuItems = [
|
||||
'ABOUT',
|
||||
'CONTACT'
|
||||
];
|
||||
|
||||
export const languages = ['English', 'Spanish', 'French'];
|
||||
|
||||
export const mobileMenuVariants = {
|
||||
closed: { opacity: 0, x: "100%" },
|
||||
open: { opacity: 1, x: 0 }
|
||||
};
|
||||
};
|
||||
|
||||
export const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'CloudDriv',
|
||||
image: cloudDriveImage,
|
||||
description: 'Secure cloud storage for seamless sharing and collaboration.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Codenuk',
|
||||
image: codenukImage,
|
||||
description: 'Breakthrough coding challenges instantly with Codenuk.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'LMS',
|
||||
image: lmsImage,
|
||||
description: 'Streamline education delivery with our comprehensive Learning Management System.'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Cloudtopiaa',
|
||||
image: cloudtopiaaImage,
|
||||
description: 'Secure, scalable cloud solutions that power your business growth.'
|
||||
}
|
||||
];
|
||||
|
||||
export const productList = [
|
||||
{
|
||||
name: "Product name one",
|
||||
description: "Breakthrough coding challenges instantly with Codenuk. Secure cloud storage for seamless sharing and collaboration."
|
||||
},
|
||||
{
|
||||
name: "Product name two",
|
||||
description: "Advanced cloud computing solutions for enterprise-level businesses with scalable infrastructure."
|
||||
},
|
||||
{
|
||||
name: "Product name three",
|
||||
description: "Comprehensive learning management system with interactive features and real-time analytics."
|
||||
},
|
||||
{
|
||||
name: "Product name four",
|
||||
description: "Secure data storage solutions with enterprise-grade encryption and collaboration tools."
|
||||
},
|
||||
{
|
||||
name: "Product name five",
|
||||
description: "AI-powered development tools for faster and more efficient coding workflows."
|
||||
},
|
||||
{
|
||||
name: "Product name six",
|
||||
description: "Automated testing platform for comprehensive quality assurance and bug detection."
|
||||
},
|
||||
{
|
||||
name: "Product name seven",
|
||||
description: "DevOps automation tools for streamlined deployment and continuous integration."
|
||||
},
|
||||
{
|
||||
name: "Product name eight",
|
||||
description: "Cloud-native application platform with microservices architecture support."
|
||||
}
|
||||
];
|
||||
11
src/components/Header/styles/DesktopNav.module.css
Normal file
11
src/components/Header/styles/DesktopNav.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.container {
|
||||
@apply hidden md:flex items-center space-x-8 max-[956px]:space-x-5;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
@apply flex items-center text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px];
|
||||
}
|
||||
|
||||
.menuLink {
|
||||
@apply text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px];
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.navLink {
|
||||
@apply text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px];
|
||||
@apply font-poppins font-bold text-[14px] text-gray-600 hover:text-[#1E0E62] transition-colors;
|
||||
}
|
||||
|
||||
.activeNavLink {
|
||||
@ -20,4 +20,6 @@
|
||||
|
||||
.searchButton {
|
||||
@apply text-gray-600 hover:text-[#1E0E62] transition-colors;
|
||||
}
|
||||
@apply max-md:scale-90;
|
||||
}
|
||||
|
||||
|
||||
17
src/components/Header/styles/LanguageSelector.module.css
Normal file
17
src/components/Header/styles/LanguageSelector.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.languageButton {
|
||||
@apply flex items-center justify-between text-gray-600 hover:text-[#1E0E62] transition-colors border rounded-md px-3 py-1;
|
||||
@apply max-md:text-sm max-md:px-2 max-md:py-0.5;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@apply absolute mt-2 w-40 bg-white rounded-md shadow-lg z-50;
|
||||
@apply max-md:right-0 md:right-[-35px];
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
@apply block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100;
|
||||
}
|
||||
11
src/components/Header/styles/MobileNav.module.css
Normal file
11
src/components/Header/styles/MobileNav.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.container {
|
||||
@apply md:hidden fixed top-16 right-0 h-[calc(100vh-64px)] w-full sm:w-[350px] z-30 bg-white shadow-lg overflow-y-auto;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
@apply w-full flex justify-between items-center px-4 py-3 text-gray-600 hover:bg-[#1E0E62]/5 font-poppins font-bold text-[14px];
|
||||
}
|
||||
|
||||
.menuLink {
|
||||
@apply block px-4 py-3 text-gray-600 hover:bg-[#1E0E62]/5 font-poppins font-bold text-[14px];
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
.container {
|
||||
@apply bg-gray-50 px-4 py-4;
|
||||
}
|
||||
|
||||
.productList {
|
||||
@apply space-y-6;
|
||||
}
|
||||
|
||||
.productCard {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.productTitle {
|
||||
@apply text-[18px] font-[500] text-[#1E0E62] font-poppins;
|
||||
}
|
||||
|
||||
.productDescription {
|
||||
@apply text-[14px] font-[500] text-[#6B7280] font-poppins mt-1;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
@apply pt-4 text-center border-t border-gray-200;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
@apply inline-flex items-center px-4 py-2 bg-[#1E0E62] text-white font-semibold rounded-lg hover:bg-[#1E0E62] transition-colors font-poppins;
|
||||
}
|
||||
43
src/components/Header/styles/ProductShowcase.module.css
Normal file
43
src/components/Header/styles/ProductShowcase.module.css
Normal file
@ -0,0 +1,43 @@
|
||||
.container {
|
||||
@apply fixed left-0 right-0 w-full bg-white shadow-lg z-50 border-b-[5px] border-[#2563EB] py-6;
|
||||
}
|
||||
|
||||
.productGrid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 gap-4 lg:w-2/3;
|
||||
}
|
||||
|
||||
.productCard {
|
||||
@apply cursor-pointer p-2;
|
||||
}
|
||||
|
||||
.productImage {
|
||||
@apply hidden md:block relative overflow-hidden rounded-lg mb-2;
|
||||
}
|
||||
|
||||
.productTitle {
|
||||
@apply mt-2 text-[22px] font-[500] text-[#1E0E62] font-poppins;
|
||||
}
|
||||
|
||||
.productDescription {
|
||||
@apply mt-2 text-[14px] font-[500] text-[#6B7280] font-poppins mb-2;
|
||||
}
|
||||
|
||||
.sidebarTitle {
|
||||
@apply text-[32px] font-[700] text-[#1E0E62] mb-6 font-poppins;
|
||||
}
|
||||
|
||||
.sidebarItem {
|
||||
@apply cursor-pointer mb-6 bg-white;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
@apply w-full flex items-center justify-center px-4 py-3 bg-[#1E0E62] text-white font-semibold rounded-lg hover:bg-[#1E0E62]/90 transition-colors font-poppins mt-4;
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.expandedContent {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
15
src/components/Header/styles/SearchOverlay.module.css
Normal file
15
src/components/Header/styles/SearchOverlay.module.css
Normal file
@ -0,0 +1,15 @@
|
||||
.overlay {
|
||||
@apply fixed inset-0 bg-white z-40;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply fixed top-0 left-0 right-0 bg-white z-50;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
@apply flex-1 px-4 text-gray-600 placeholder-gray-400 bg-transparent border-none focus:outline-none focus:ring-0;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
@apply text-gray-400 hover:text-[#1E0E62] transition-colors;
|
||||
}
|
||||
31
src/components/Header/styles/base.module.css
Normal file
31
src/components/Header/styles/base.module.css
Normal file
@ -0,0 +1,31 @@
|
||||
.container {
|
||||
@apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.flexCenter {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.flexBetween {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.transitionBase {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
.textBase {
|
||||
@apply font-poppins font-bold text-[14px];
|
||||
}
|
||||
|
||||
.textPrimary {
|
||||
@apply text-[#1E0E62];
|
||||
}
|
||||
|
||||
.textGray {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.hoverPrimary {
|
||||
@apply hover:text-[#1E0E62] transition-colors;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user