Finished the Navbar and product dropdown issue

This commit is contained in:
Asgarsk 2024-11-29 17:06:41 +05:30
parent 1844fb8b45
commit 4508039803
16 changed files with 520 additions and 230 deletions

View File

@ -7,6 +7,7 @@ import OptimizedImage from '../common/OptimizedImage';
import { menuItems, languages, mobileMenuVariants } from './constants/navigationData'; import { menuItems, languages, mobileMenuVariants } from './constants/navigationData';
import { headerSchema } from './schema/headerSchema'; import { headerSchema } from './schema/headerSchema';
import styles from './styles/Header.module.css'; import styles from './styles/Header.module.css';
import baseStyles from './styles/base.module.css';
import Logo from '@/assets/Logo/Tech4biz-logo.webp'; import Logo from '@/assets/Logo/Tech4biz-logo.webp';
const SearchOverlay = lazy(() => import('./components/SearchOverlay')); const SearchOverlay = lazy(() => import('./components/SearchOverlay'));
@ -15,7 +16,7 @@ const MobileNav = lazy(() => import('./components/MobileNav'));
const LanguageSelector = lazy(() => import('./components/LanguageSelector')); const LanguageSelector = lazy(() => import('./components/LanguageSelector'));
const Navbar = () => { const Navbar = () => {
// State management with hooks // State management remains the same
const [state, setState] = useState({ const [state, setState] = useState({
isProductsOpen: false, isProductsOpen: false,
isLanguageOpen: false, isLanguageOpen: false,
@ -26,10 +27,11 @@ const Navbar = () => {
isSticky: false isSticky: false
}); });
// Refs and handlers remain the same
const desktopLanguageRef = useRef(null); const desktopLanguageRef = useRef(null);
const mobileLanguageRef = useRef(null); const mobileLanguageRef = useRef(null);
// Event handlers with useCallback // Event handlers with useCallback remain the same
const handleClickOutside = useCallback((event) => { const handleClickOutside = useCallback((event) => {
if (desktopLanguageRef.current && !desktopLanguageRef.current.contains(event.target)) { if (desktopLanguageRef.current && !desktopLanguageRef.current.contains(event.target)) {
setState(prev => ({ ...prev, isLanguageOpen: false })); setState(prev => ({ ...prev, isLanguageOpen: false }));
@ -43,12 +45,11 @@ const Navbar = () => {
setState(prev => ({ ...prev, isSticky: window.scrollY > 0 })); setState(prev => ({ ...prev, isSticky: window.scrollY > 0 }));
}, []); }, []);
// Effect for event listeners // Effects remain the same
useEffect(() => { useEffect(() => {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
// Inject schema
const script = document.createElement('script'); const script = document.createElement('script');
script.type = 'application/ld+json'; script.type = 'application/ld+json';
script.text = JSON.stringify(headerSchema); script.text = JSON.stringify(headerSchema);
@ -64,13 +65,15 @@ const Navbar = () => {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}> <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 : ''}`}> <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={baseStyles.container}>
<div className="flex justify-between items-center h-16"> <div className={`${baseStyles.flexBetween} h-16`}>
{/* Left section with logo and mobile actions */} <div className={`${baseStyles.flexCenter} space-x-4`}>
<div className="flex items-center space-x-4">
<Link to="/" className="flex-shrink-0"> <Link to="/" className="flex-shrink-0">
<OptimizedImage <OptimizedImage
src={Logo} src={Logo}
@ -82,108 +85,69 @@ const Navbar = () => {
priority={true} priority={true}
/> />
</Link> </Link>
{/* Mobile search and language */} {/* 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 <button
onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))} onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))}
className="p-2 text-gray-600 hover:text-[#1E0E62] transition-colors" className={styles.searchButton}
aria-label="Search" aria-label="Search"
> >
<FiSearch className="h-5 w-5" /> <FiSearch className="h-5 w-5" />
</button> </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 <LanguageSelector
isOpen={state.isLanguageOpen} isDesktop={false}
isOpen={state.isMobileLanguageOpen}
selectedLanguage={state.selectedLanguage} selectedLanguage={state.selectedLanguage}
languageRef={desktopLanguageRef} languageRef={mobileLanguageRef}
setState={setState} setState={setState}
/> />
</div> </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> </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>
</div> </div>
{/* Mobile Navigation */}
<MobileNav <MobileNav
isMobileMenuOpen={state.isMobileMenuOpen} isMobileMenuOpen={state.isMobileMenuOpen}
setState={setState} setState={setState}

View File

@ -4,14 +4,13 @@ import { FiChevronDown } from 'react-icons/fi';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { menuItems } from '../constants/navigationData'; import { menuItems } from '../constants/navigationData';
import ProductShowcase from './ProductShowcase'; import ProductShowcase from './ProductShowcase';
import styles from '../styles/DesktopNav.module.css';
import baseStyles from '../styles/base.module.css';
const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => { const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
const handleMouseEnter = () => { const handleProductsClick = (e) => {
setIsProductsOpen(true); e.stopPropagation();
}; setIsProductsOpen(!isProductsOpen);
const handleMouseLeave = () => {
setIsProductsOpen(false);
}; };
const getItemPath = (item) => { const getItemPath = (item) => {
@ -30,19 +29,17 @@ const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
}; };
return ( return (
<div className="hidden md:flex items-center space-x-8 max-[956px]:space-x-5"> <div className={styles.container}>
{menuItems.map((item) => ( {menuItems.map((item) => (
<div key={item} className="relative"> <div key={item} className="relative">
{item === 'PRODUCTS' ? ( {item === 'PRODUCTS' ? (
<div <div>
className="static"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button <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-expanded={isProductsOpen}
aria-haspopup="true" aria-haspopup="true"
data-products-trigger
> >
{item} {item}
<FiChevronDown <FiChevronDown
@ -51,13 +48,13 @@ const DesktopNav = memo(({ isProductsOpen, setIsProductsOpen }) => {
/> />
</button> </button>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{isProductsOpen && <ProductShowcase />} {isProductsOpen && <ProductShowcase setIsProductsOpen={setIsProductsOpen} />}
</AnimatePresence> </AnimatePresence>
</div> </div>
) : ( ) : (
<Link <Link
to={getItemPath(item)} to={getItemPath(item)}
className="text-gray-600 hover:text-[#1E0E62] transition-colors font-poppins font-bold text-[14px]" className={styles.menuLink}
> >
{item} {item}
</Link> </Link>

View File

@ -2,7 +2,7 @@ import React, { memo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FiChevronDown } from 'react-icons/fi'; import { FiChevronDown } from 'react-icons/fi';
import { languages } from '../constants/navigationData'; import { languages } from '../constants/navigationData';
import styles from '../styles/Header.module.css'; import styles from '../styles/LanguageSelector.module.css';
const LanguageSelector = memo(({ const LanguageSelector = memo(({
isDesktop = true, isDesktop = true,
@ -11,12 +11,8 @@ const LanguageSelector = memo(({
languageRef, languageRef,
setState setState
}) => { }) => {
const containerClasses = isDesktop
? "hidden md:block relative"
: "relative";
return ( return (
<div ref={languageRef} className={containerClasses}> <div ref={languageRef} className={`${styles.container} ${isDesktop ? 'hidden md:block' : ''}`}>
<button <button
onClick={() => setState(prev => ({ onClick={() => setState(prev => ({
...prev, ...prev,
@ -36,13 +32,13 @@ const LanguageSelector = memo(({
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }} 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" role="listbox"
> >
{languages.map((language) => ( {languages.map((language) => (
<button <button
key={language} 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 => ({ onClick={() => setState(prev => ({
...prev, ...prev,
selectedLanguage: language, selectedLanguage: language,

View File

@ -1,10 +1,29 @@
import React, { memo } from 'react'; import React, { memo, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; 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 { 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 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 ( return (
<AnimatePresence> <AnimatePresence>
{isMobileMenuOpen && ( {isMobileMenuOpen && (
@ -14,7 +33,7 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
exit="closed" exit="closed"
variants={mobileMenuVariants} variants={mobileMenuVariants}
transition={{ duration: 0.3, ease: "easeInOut" }} 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" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Mobile navigation menu" aria-label="Mobile navigation menu"
@ -22,14 +41,43 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{menuItems.map((item) => ( {menuItems.map((item) => (
<a <div key={item}>
key={item} {item === 'PRODUCTS' ? (
href="#" <>
className="block px-4 py-3 text-gray-600 hover:text-[#1E0E62] hover:bg-[#1E0E62]/5 font-poppins font-bold text-[14px]" <button
onClick={() => setState(prev => ({ ...prev, isMobileMenuOpen: false }))} className={styles.menuButton}
> onClick={() => setIsProductsOpen(!isProductsOpen)}
{item} aria-expanded={isProductsOpen}
</a> >
{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>
</div> </div>
@ -39,6 +87,4 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
); );
}); });
MobileNav.displayName = 'MobileNav';
export default MobileNav; export default MobileNav;

View 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;

View File

@ -1,107 +1,74 @@
import React from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { ChevronRight } from 'lucide-react';
// Import images import styles from '../styles/ProductShowcase.module.css';
import lmsImage from '@/assets/Header/Product-Card/learning-management-system.webp'; import baseStyles from '../styles/base.module.css';
import codenukImage from '@/assets/Header/Product-Card/codenuk-coding-solution-platform.webp'; import { products, productList } from '../constants/navigationData';
import cloudtopiaaImage from '@/assets/Header/Product-Card/cloudtopiaa-enterprise-solutions.webp';
import cloudDriveImage from '@/assets/Header/Product-Card/cloud-drive-storage-platform.webp';
const containerVariants = { const containerVariants = {
initial: { initial: {
opacity: 0, opacity: 0,
height: 0, y: -10,
transformOrigin: "top"
}, },
animate: { animate: {
opacity: 1, opacity: 1,
height: "auto", y: 0,
transition: { transition: {
height: { duration: 0.15,
duration: 0.3, ease: "easeOut"
ease: "easeOut"
},
opacity: {
duration: 0.2,
ease: "easeOut"
}
} }
}, },
exit: { exit: {
opacity: 0, opacity: 0,
height: 0, y: -10,
transition: { transition: {
height: { duration: 0.15,
duration: 0.3, ease: "easeIn"
ease: "easeIn"
},
opacity: {
duration: 0.2,
ease: "easeIn"
}
} }
} }
}; };
const ProductShowcase = () => { const ProductShowcase = ({ setIsProductsOpen }) => {
const products = [ const [hoveredIndex, setHoveredIndex] = useState(0);
{ const [isFirstItemLocked, setIsFirstItemLocked] = useState(true);
id: 1, const showcaseRef = useRef(null);
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 productList = [ useEffect(() => {
"Product name one", const handleClickOutside = (event) => {
"Product name two", if (
"Product name three", showcaseRef.current &&
"Product name four", !showcaseRef.current.contains(event.target) &&
"Product name five", !event.target.closest('[data-products-trigger]')
"Product name six", ) {
"Product name seven", setIsProductsOpen(false);
"Product name eight" }
]; };
document.addEventListener('mousedown', handleClickOutside, true);
return () => document.removeEventListener('mousedown', handleClickOutside, true);
}, [setIsProductsOpen]);
return ( return (
<motion.div <motion.div
ref={showcaseRef}
variants={containerVariants} variants={containerVariants}
initial="initial" initial="initial"
animate="animate" animate="animate"
exit="exit" 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' }} 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="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) => ( {products.map((product) => (
<motion.div <motion.div
key={product.id} key={product.id}
className="group cursor-pointer" className={`group ${styles.productCard}`}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<div className="hidden md:block relative overflow-hidden rounded-lg"> <div className={styles.productImage}>
<img <img
src={product.image} src={product.image}
alt={product.name} alt={product.name}
@ -109,43 +76,88 @@ const ProductShowcase = () => {
loading="lazy" loading="lazy"
/> />
</div> </div>
<h3 className="mt-4 text-xl font-semibold text-indigo-900"> <h3 className={styles.productTitle}>
{product.name} {product.name}
</h3> </h3>
<p className="mt-2 text-gray-600 text-sm"> <p className={styles.productDescription}>
{product.description} {product.description}
</p> </p>
</motion.div> </motion.div>
))} ))}
</div> </div>
{/* Desktop view for All Products */}
<div className="hidden lg:block lg:w-1/3"> <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"> <div className="space-y-4">
{productList.map((product, index) => ( {productList.map((product, index) => (
<motion.div <motion.div
key={index} key={index}
className="cursor-pointer" className={styles.sidebarItem}
whileHover={{ x: 10 }} whileHover={{ x: 5 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
> >
<h3 className="text-lg font-semibold text-indigo-900"> <div className={styles.sidebarContent}>
{product} <h3 className={styles.productTitle}>
</h3> {product.name}
{index === 0 && ( </h3>
<> <AnimatePresence mode="popLayout">
<p className="text-gray-600 text-sm mt-2"> {hoveredIndex === index && (
Breakthrough coding challenges instantly with Codenuk. Secure cloud storage for seamless sharing and collaboration. <motion.div
</p> className={styles.expandedContent}
<button className="text-indigo-600 text-sm mt-2 hover:text-indigo-800"> initial={{ height: 0, opacity: 0 }}
Read more animate={{
</button> 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> </motion.div>
))} ))}
</div> </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>
</div> </div>
</motion.div> </motion.div>

View File

@ -1,12 +1,13 @@
import React, { useCallback, memo } from 'react'; import React, { memo, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FiSearch, FiX } from 'react-icons/fi'; import { FiSearch, FiX } from 'react-icons/fi';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import styles from '../styles/SearchOverlay.module.css';
import baseStyles from '../styles/base.module.css';
const SearchOverlay = memo(({ isOpen, onClose }) => { const SearchOverlay = memo(({ isOpen, onClose }) => {
const handleSearch = useCallback( const handleSearch = useCallback(
debounce((searchTerm) => { debounce((searchTerm) => {
// Implement search logic here
console.log('Searching for:', searchTerm); console.log('Searching for:', searchTerm);
}, 300), }, 300),
[] []
@ -21,7 +22,7 @@ const SearchOverlay = memo(({ isOpen, onClose }) => {
animate={{ opacity: 0.4 }} animate={{ opacity: 0.4 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="fixed inset-0 bg-white z-40" className={styles.overlay}
onClick={onClose} onClick={onClose}
role="presentation" role="presentation"
/> />
@ -31,24 +32,24 @@ const SearchOverlay = memo(({ isOpen, onClose }) => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="fixed top-0 left-0 right-0 bg-white z-50" className={styles.container}
role="dialog" role="dialog"
aria-label="Search overlay" aria-label="Search overlay"
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className={baseStyles.container}>
<div className="flex items-center h-16 border-b border-gray-200"> <div className={`${baseStyles.flexCenter} h-16 border-b border-gray-200`}>
<FiSearch className="h-5 w-5 text-gray-400" aria-hidden="true" /> <FiSearch className="h-5 w-5 text-gray-400" aria-hidden="true" />
<input <input
type="search" type="search"
placeholder="What are you looking for?" 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)} onChange={(e) => handleSearch(e.target.value)}
aria-label="Search input" aria-label="Search input"
autoFocus autoFocus
/> />
<button <button
onClick={onClose} onClick={onClose}
className="text-gray-400 hover:text-[#1E0E62] transition-colors" className={styles.closeButton}
aria-label="Close search" aria-label="Close search"
> >
<FiX className="h-5 w-5" /> <FiX className="h-5 w-5" />

View File

@ -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 = [ export const menuItems = [
'HOME', 'HOME',
'PRODUCTS', 'PRODUCTS',
@ -5,9 +10,72 @@ export const menuItems = [
'ABOUT', 'ABOUT',
'CONTACT' 'CONTACT'
]; ];
export const languages = ['English', 'Spanish', 'French']; export const languages = ['English', 'Spanish', 'French'];
export const mobileMenuVariants = { export const mobileMenuVariants = {
closed: { opacity: 0, x: "100%" }, closed: { opacity: 0, x: "100%" },
open: { opacity: 1, x: 0 } 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."
}
];

View 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];
}

View File

@ -7,7 +7,7 @@
} }
.navLink { .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 { .activeNavLink {
@ -20,4 +20,6 @@
.searchButton { .searchButton {
@apply text-gray-600 hover:text-[#1E0E62] transition-colors; @apply text-gray-600 hover:text-[#1E0E62] transition-colors;
} @apply max-md:scale-90;
}

View 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;
}

View 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];
}

View File

@ -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;
}

View 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;
}

View 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;
}

View 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;
}