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 { 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}
@ -84,79 +87,42 @@ const Navbar = () => {
</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>
<LanguageSelector
isDesktop={false}
isOpen={state.isMobileLanguageOpen}
selectedLanguage={state.selectedLanguage}
languageRef={mobileLanguageRef}
setState={setState}
/>
</div>
</div>
{/* Desktop Navigation */}
<DesktopNav {...state} setIsProductsOpen={(value) =>
setState(prev => ({ ...prev, isProductsOpen: value }))}
<DesktopNav
isProductsOpen={state.isProductsOpen}
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">
<div className="hidden md:flex items-center space-x-4">
<button
onClick={() => setState(prev => ({ ...prev, isSearchOpen: true }))}
className={styles.searchButton}
aria-label="Open search"
aria-label="Search"
>
<FiSearch className="h-5 w-5" />
</button>
<LanguageSelector
isDesktop={true}
isOpen={state.isLanguageOpen}
selectedLanguage={state.selectedLanguage}
languageRef={desktopLanguageRef}
@ -164,9 +130,9 @@ const Navbar = () => {
/>
</div>
{/* Mobile menu button */}
{/* Mobile menu toggle button */}
<button
className="md:hidden"
className="md:hidden text-gray-600 hover:text-[#1E0E62] transition-colors"
onClick={() => setState(prev => ({
...prev,
isMobileMenuOpen: !prev.isMobileMenuOpen
@ -181,9 +147,7 @@ const Navbar = () => {
</button>
</div>
</div>
</div>
{/* Mobile Navigation */}
<MobileNav
isMobileMenuOpen={state.isMobileMenuOpen}
setState={setState}

View File

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

View File

@ -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,

View File

@ -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]"
<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}
</a>
</Link>
)}
</div>
))}
</div>
</div>
@ -39,6 +87,4 @@ const MobileNav = memo(({ isMobileMenuOpen, setState }) => {
);
});
MobileNav.displayName = '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 { 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,
duration: 0.15,
ease: "easeOut"
},
opacity: {
duration: 0.2,
ease: "easeOut"
}
}
},
exit: {
opacity: 0,
height: 0,
y: -10,
transition: {
height: {
duration: 0.3,
duration: 0.15,
ease: "easeIn"
},
opacity: {
duration: 0.2,
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}
<div className={styles.sidebarContent}>
<h3 className={styles.productTitle}>
{product.name}
</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.
<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>
<button className="text-indigo-600 text-sm mt-2 hover:text-indigo-800">
<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
</button>
</>
<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>

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 { 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" />

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 = [
'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."
}
];

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

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