Hero AI Ecommerce
Hero AI Ecommerce é um componente de layout para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
UI Layouts
MIT
layout
O que é
Hero AI Ecommerce é um componente de layout para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Hero AI Ecommerce
'use client'
import { useRef } from 'react'
import { CreditCard, LogIn } from 'lucide-react'
import { EcommerceDash } from '../../assets/index'
import { TimelineAnimation } from '@/components/ui/timeline-animation'
import { useMediaQuery } from '@/hooks/use-media-query'
import MotionDrawer from '@/components/ui/motion-drawer'
export const HeroAiEcommerce = () => {
const timelineRef = useRef<HTMLDivElement>(null)
const isMobile = useMediaQuery('(max-width: 768px)')
return (
<section
ref={timelineRef}
className="min-h-screen text-black bg-white relative overflow-hidden flex flex-col items-center"
>
<div className="absolute inset-0 z-0 bg-[url('https://images.unsplash.com/photo-1764138370667-d15f89ee1c45?q=80&w=1760&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')] bg-cover bg-center opacity-50" />
{/* Mobile Navigation */}
{isMobile && (
<div className="flex gap-4 justify-between items-center px-10 pt-4">
<MotionDrawer
direction="left"
width={300}
backgroundColor={'#ffffff'}
clsBtnClassName="bg-neutral-800 border-r border-neutral-900 text-white"
contentClassName="bg-white border-r border-neutral-200 text-black"
btnClassName="bg-white text-black relative w-fit p-2 left-0 top-0 rounded-full shadow-xs border border-neutral-200"
>
<nav className="space-y-4 ">
<div className="flex items-center gap-2 text-black">
<svg
className="fill-black w-8 h-8"
width="97"
height="108"
viewBox="0 0 97 108"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M55.5 0C61.0005 0.00109895 64.5005 2.50586 64.5 7.5V17C64.5 24.5059 68.5005 27.5 81 27.5H88C94.0005 27.5059 96.5 29.5059 96.5 37.5V98.5C96.5 106.006 95.0005 107.5 88 107.5H41.5C36.5005 107.5 32 104.506 32 98.5V88C32 84.5 28.5 80 20.5 80H8.5C3 80 0 76.5 0 71.5V6.5C0.00048844 1.50586 2.50049 0.00585937 8.5 0H55.5ZM31 20C28.7909 20 27 21.7909 27 24V74C27 76.2091 28.7909 78 31 78H58C60.2091 78 62 76.2091 62 74V24C62 21.7909 60.2091 20 58 20H31Z" />
</svg>
<span>UI-Layouts</span>
</div>
<a
href="#"
className="block p-2 hover:bg-neutral-200 hover:text-black rounded-sm"
>
Our Service
</a>
<a
href="#"
className="block p-2 hover:bg-neutral-200 hover:text-black rounded-sm"
>
About Us
</a>
<a
href="#"
className="block p-2 hover:bg-neutral-200 hover:text-black rounded-sm"
>
Contact
</a>
</nav>
</MotionDrawer>
<TimelineAnimation
as="button"
animationNum={2}
timelineRef={timelineRef}
className="flex items-center gap-2 bg-white px-4 py-2.5 rounded-full shadow-xs border border-neutral-200 text-sm font-medium"
>
<LogIn size={16} /> Sign in
</TimelineAnimation>
</div>
)}
{/* Navigation */}
{!isMobile && (
<header className="w-full relative z-10 max-w-5xl mx-auto flex items-center justify-between px-6 py-6">
<TimelineAnimation
as="nav"
animationNum={1}
timelineRef={timelineRef}
className="flex items-center gap-8 bg-white px-6 py-3 rounded-full shadow-xs border border-neutral-200"
>
<div className="text-2xl font-extrabold text-[#5d5dff]">
<svg
className="fill-indigo-500 w-8 h-8"
width="97"
height="108"
viewBox="0 0 97 108"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M55.5 0C61.0005 0.00109895 64.5005 2.50586 64.5 7.5V17C64.5 24.5059 68.5005 27.5 81 27.5H88C94.0005 27.5059 96.5 29.5059 96.5 37.5V98.5C96.5 106.006 95.0005 107.5 88 107.5H41.5C36.5005 107.5 32 104.506 32 98.5V88C32 84.5 28.5 80 20.5 80H8.5C3 80 0 76.5 0 71.5V6.5C0.00048844 1.50586 2.50049 0.00585937 8.5 0H55.5ZM31 20C28.7909 20 27 21.7909 27 24V74C27 76.2091 28.7909 78 31 78H58C60.2091 78 62 76.2091 62 74V24C62 21.7909 60.2091 20 58 20H31Z" />
</svg>
</div>
<nav className="hidden md:flex items-center gap-6 text-sm font-semibold text-neutral-800">
<a href="#" className="hover:text-indigo-500">
How It Works
</a>
<a href="#" className="hover:text-indigo-500">
Internships
</a>
<a href="#" className="hover:text-indigo-500">
Students
</a>
<a href="#" className="hover:text-indigo-500">
Professors
</a>
<a href="#" className="hover:text-indigo-500">
FAQs
</a>
</nav>
</TimelineAnimation>
<div className="flex items-center gap-3">
<TimelineAnimation
as="button"
animationNum={2}
timelineRef={timelineRef}
className="flex items-center gap-2 bg-white px-4 py-2.5 rounded-full shadow-xs border border-neutral-200 text-sm font-medium"
>
<LogIn size={16} /> Sign in
</TimelineAnimation>
</div>
</header>
)}
{/* Hero Body */}
<div className="text-center relative px-4 pt-16 pb-12 z-10">
<TimelineAnimation
as="h1"
animationNum={3}
timelineRef={timelineRef}
className="sm:text-6xl text-5xl md:text-7xl font-medium tracking-tight text-neutral-800 mb-8"
>
Empower Your Learning <br className="sm:inline-block hidden" />
With{' '}
<TimelineAnimation
as="span"
animationNum={4}
timelineRef={timelineRef}
className="relative inline-block border-2 border-indigo-500 px-4 py-1 bg-indigo-100 text-indigo-500 rounded-md"
>
AI Communities
<div className="absolute -top-1.5 -left-1.5 w-3 h-3 bg-white border-2 border-indigo-500"></div>
<div className="absolute -top-1.5 -right-1.5 w-3 h-3 bg-white border-2 border-indigo-500"></div>
<div className="absolute -bottom-1.5 -left-1.5 w-3 h-3 bg-white border-2 border-indigo-500"></div>
<div className="absolute -bottom-1.5 -right-1.5 w-3 h-3 bg-white border-2 border-indigo-500"></div>
</TimelineAnimation>
</TimelineAnimation>
<TimelineAnimation
as="p"
animationNum={5}
timelineRef={timelineRef}
className="text-neutral-500 text-lg md:text-xl max-w-2xl mx-auto mb-5 font-normal leading-relaxed"
>
Join AI-powered course communities that help you study faster,
collaborate with peers, and excel in your classes.
</TimelineAnimation>
<div className="flex flex-col items-center gap-6">
<TimelineAnimation
as="button"
animationNum={6}
timelineRef={timelineRef}
className="p-1.5 bg-linear-to-t from-indigo-800 to-indigo-100 h-20 rounded-full"
>
<span className="bg-linear-to-l from-indigo-500 to-indigo-600 shadow-[inset_4px_4px_5px_0px_rgba(168,170,241,0.5),inset_-1px_-2px_5px_0px_rgba(74,78,197,0.5),inset_-1px_4px_8px_0px_rgba(44,58,98,0.25)] text-white px-10 py-5 rounded-full text-lg font-semibold cursor-pointer">
Start Your 14 Day Free Trial
</span>
</TimelineAnimation>
<TimelineAnimation
as="p"
animationNum={6}
timelineRef={timelineRef}
className="text-neutral-600 text-sm flex items-center gap-2 font-medium"
>
<CreditCard size={16} /> No Credit Card Required
</TimelineAnimation>
</div>
</div>
{/* Dashboard Preview */}
<div className="w-full max-w-7xl mx-auto rounded-xl relative mt-10">
<TimelineAnimation
animationNum={7}
timelineRef={timelineRef}
className="rounded-2xl bg-white/50 backdrop-blur-lg p-4"
>
<TimelineAnimation
animationNum={8}
as="img"
timelineRef={timelineRef}
// @ts-ignore
src={EcommerceDash?.src}
alt="phoneMockUP"
className="w-full relative z-4 rounded-2xl"
/>
</TimelineAnimation>
</div>
</section>
)
}
'use client';
import { cn } from '@/lib/utils';
import { Menu, X } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import type React from 'react';
import { useState } from 'react';
export type SideMenuDirection = 'left' | 'right';
export type ButtonOpeningVariants = 'push' | 'merge' | 'stay';
interface SideMenuProps {
// Appearance
overlayColor?: string;
width?: number;
direction?: SideMenuDirection;
backgroundColor?: string;
// Content
children: React.ReactNode;
// Behavior
isOpen?: boolean;
onToggle?: (isOpen: boolean) => void;
showToggleButton?: boolean;
toggleButtonText?: {
open: string;
close: string;
};
// Styling
btnClassName?: string;
className?: string;
contentClassName?: string;
clsBtnClassName?: string;
overlayClassName?: string;
// Animation
animationConfig?: {
type?: 'spring' | 'tween';
damping?: number;
stiffness?: number;
duration?: number;
};
// Drag behavior
enableDrag?: boolean;
dragThreshold?: number;
// New prop
buttonOpeningVariants?: ButtonOpeningVariants;
}
const getOpenButtonVariants = (
direction: SideMenuDirection,
width: number,
type: ButtonOpeningVariants
) => {
switch (type) {
case 'merge':
return direction === 'left'
? {
closed: { x: 0, opacity: 1, scale: 1, borderRadius: '0.5rem' },
open: {
x: width - 68,
opacity: 0,
scale: 1,
borderRadius: '0rem',
},
}
: {
closed: { x: 0, opacity: 1, scale: 1, borderRadius: '0.5rem' },
open: {
x: 68 - width,
opacity: 0,
scale: 1,
borderRadius: '0rem',
},
};
case 'push':
return direction === 'left'
? { closed: { x: 0, opacity: 1 }, open: { x: width + 20, opacity: 0 } }
: {
closed: { x: 0, opacity: 1 },
open: { x: -(width + 20), opacity: 0 },
};
case 'stay':
default:
return {
closed: { x: 0, opacity: 1 },
open: { x: 0, opacity: 0 },
};
}
};
const MotionDrawer: React.FC<SideMenuProps> = ({
// Appearance
overlayColor = 'rgba(0, 0, 0, 0.3)',
width = 250,
direction = 'left',
backgroundColor = '#ffffff',
// Content
children,
// Behavior
isOpen: controlledIsOpen,
onToggle,
showToggleButton = true,
// Styling
btnClassName = '',
clsBtnClassName = '',
className = '',
contentClassName = '',
overlayClassName = '',
// Animation
animationConfig = {
type: 'spring',
damping: 25,
stiffness: 120,
},
// Drag behavior
enableDrag = true,
dragThreshold = 0.3,
buttonOpeningVariants = 'merge',
}) => {
const [internalIsOpen, setInternalIsOpen] = useState<boolean>(false);
const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
const setIsOpen = (value: boolean) => {
if (controlledIsOpen === undefined) {
setInternalIsOpen(value);
}
onToggle?.(value);
};
const getDrawerVariants = () => {
if (direction === 'left') {
return {
closed: { x: -width },
open: { x: 0 },
};
} else {
return {
closed: { x: width },
open: { x: 0 },
};
}
};
const buttonVariants = getOpenButtonVariants(direction, width, buttonOpeningVariants);
const getDragConstraints = () => {
if (direction === 'left') {
return { left: -width, right: 0 };
} else {
return { left: 0, right: width };
}
};
const handleDragEnd = (_event: any, info: any) => {
if (!enableDrag) return;
const threshold = width * dragThreshold;
const dragDistance = Math.abs(info.offset.x);
if (direction === 'left') {
const isDraggingLeft = info.offset.x < 0;
if (isDraggingLeft && dragDistance > threshold && isOpen) {
setIsOpen(false);
} else if (!isDraggingLeft && dragDistance > threshold && !isOpen) {
setIsOpen(true);
}
} else {
const isDraggingRight = info.offset.x > 0;
if (isDraggingRight && dragDistance > threshold && isOpen) {
setIsOpen(false);
} else if (!isDraggingRight && dragDistance > threshold && !isOpen) {
setIsOpen(true);
}
}
};
const drawerPositionClasses = direction === 'left' ? 'left-0' : 'right-0';
const openButtonPositionClasses = direction === 'left' ? 'top-4 left-4' : 'top-4 right-4';
return (
<>
{showToggleButton && (
<motion.button
className={cn(
`fixed z-99 text-primary cursor-pointer ${openButtonPositionClasses}`,
btnClassName
)}
onClick={() => setIsOpen(true)}
variants={buttonVariants}
animate={isOpen ? 'open' : 'closed'}
transition={animationConfig}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Menu />
{/* Open */}
</motion.button>
)}
<AnimatePresence>
{isOpen && (
<div className={`fixed w-full h-full top-0 left-0 z-9999 ${className}`}>
{/* Overlay */}
<motion.div
className={`absolute w-full h-full top-0 left-0 ${overlayClassName}`}
style={{ backgroundColor: overlayColor }}
onClick={() => setIsOpen(false)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
/>
{/* Drawer */}
<motion.div
className={`absolute h-full shadow-[8px_1px_21px_0px_rgba(17,17,26,0.1)] ${drawerPositionClasses} ${contentClassName}`}
style={{
backgroundColor,
width: `${width}px`,
padding: '60px 30px 30px 30px',
boxSizing: 'border-box',
}}
drag={enableDrag ? 'x' : false}
dragElastic={0.1}
dragConstraints={getDragConstraints()}
dragMomentum={false}
onDragEnd={handleDragEnd}
variants={getDrawerVariants()}
initial='closed'
animate='open'
exit='closed'
transition={animationConfig}
>
{/* Close Button */}
{showToggleButton && (
<motion.button
className={cn(
'absolute top-2 right-8 p-2 text-black cursor-pointer',
clsBtnClassName
)}
onClick={() => setIsOpen(false)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
transition={{ duration: 0.2 }}
>
<X size={20} /> {/* Close */}
</motion.button>
)}
{/* Content */}
<div className='h-full overflow-y-auto'>{children}</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
};
export default MotionDrawer;
import type { Variants } from 'motion/react';
import { type HTMLMotionProps, motion, useInView } from 'motion/react';
import type React from 'react';
type TimelineContentProps<T extends keyof HTMLElementTagNameMap> = {
children?: React.ReactNode;
animationNum: number;
className?: string;
timelineRef: React.RefObject<HTMLElement | null>;
as?: T;
customVariants?: Variants;
once?: boolean;
} & HTMLMotionProps<T>;
export const TimelineAnimation = <T extends keyof HTMLElementTagNameMap = 'div'>({
children,
animationNum,
timelineRef,
className,
as,
customVariants,
once = true,
...props
}: TimelineContentProps<T>) => {
const defaultSequenceVariants = {
visible: (i: number) => ({
filter: 'blur(0px)',
y: 0,
opacity: 1,
transition: {
delay: i * 0.5,
duration: 0.5,
},
}),
hidden: {
filter: 'blur(20px)',
y: 0,
opacity: 0,
},
};
const sequenceVariants = customVariants || defaultSequenceVariants;
const isInView = useInView(timelineRef, {
once,
});
const MotionComponent = motion[as || 'div'] as React.ElementType;
return (
<MotionComponent
initial='hidden'
animate={isInView ? 'visible' : 'hidden'}
custom={animationNum}
variants={sequenceVariants}
className={className}
{...props}
>
{children}
</MotionComponent>
);
};
'use client';
import { useEffect, useState } from 'react';
export function useMediaQuery(query: string) {
const [value, setValue] = useState(false);
useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}
const result = matchMedia(query);
result.addEventListener('change', onChange);
setValue(result.matches);
return () => result.removeEventListener('change', onChange);
}, [query]);
return value;
}