Spaces:
Running
Running
deepsite
/
Website-Generation-Request (1)
/src
/components
/blocks
/bento-grids
/three-column-bento-grid.tsx
"use client"; | |
import { cn } from "@/lib/utils"; | |
import React, { useEffect, useState, useRef, useId } from "react"; | |
import { motion } from "motion/react"; | |
import Image from "next/image"; | |
import { Home, Building, Car, Sparkles } from "lucide-react"; | |
export function ThreeColumnBentoGrid() { | |
return ( | |
<div className="mx-auto my-20 w-full max-w-7xl px-4 md:px-8 bg-white"> | |
<h2 className="text-bold text-neutral-8000 font-sans text-xl font-bold tracking-tight md:text-4xl text-[var(--color-dark-gray-secondary)]"> | |
Product Applications | |
</h2> | |
<p className="mt-4 max-w-lg text-sm text-[var(--color-supporting-gray)]"> | |
Discover how Ceramic Shield enhances comfort and protection across various settings. | |
</p> | |
<div className="mt-20 grid grid-flow-col grid-cols-1 grid-rows-6 gap-2 md:grid-cols-2 md:grid-rows-3 xl:grid-cols-3 xl:grid-rows-2"> | |
<Card className="row-span-2"> | |
<CardContent> | |
<CardTitle>Residential Windows</CardTitle> | |
<CardDescription> | |
Perfect for homes, providing UV protection, glare reduction, and enhanced privacy while maintaining natural light. | |
</CardDescription> | |
</CardContent> | |
<CardSkeletonBody> | |
<SkeletonOne /> | |
</CardSkeletonBody> | |
</Card> | |
<Card className="overflow-hidden"> | |
<CardContent> | |
<CardTitle>Commercial Buildings</CardTitle> | |
<CardDescription> | |
Professional-grade window film solutions for offices, retail spaces, and commercial properties. | |
</CardDescription> | |
</CardContent> | |
<CardSkeletonBody> | |
<SkeletonTwo /> | |
</CardSkeletonBody> | |
</Card> | |
<Card> | |
<CardContent> | |
<CardTitle>Automotive</CardTitle> | |
<CardDescription> | |
Premium window tinting for luxury vehicles, providing heat rejection and UV protection. | |
</CardDescription> | |
</CardContent> | |
<CardSkeletonBody className=""> | |
<SkeletonThree /> | |
</CardSkeletonBody> | |
</Card> | |
<Card className="row-span-2"> | |
<CardContent> | |
<CardTitle>Specialty Applications</CardTitle> | |
<CardDescription> | |
Custom installations for skylights, glass doors, and unique architectural features. | |
</CardDescription> | |
</CardContent> | |
<CardSkeletonBody className="h-full max-h-full overflow-hidden"> | |
<SkeletonFour /> | |
</CardSkeletonBody> | |
</Card> | |
</div> | |
</div> | |
); | |
} | |
// Skeletons | |
const SkeletonOne = () => { | |
const homes = [ | |
{ | |
src: "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=400&h=300&fit=crop", | |
type: "Modern Villa", | |
}, | |
{ | |
src: "https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=400&h=300&fit=crop", | |
type: "Family Home", | |
}, | |
{ | |
src: "https://images.unsplash.com/photo-1449844908441-8829872d2607?w=400&h=300&fit=crop", | |
type: "Contemporary House", | |
}, | |
{ | |
src: "https://images.unsplash.com/photo-1605146769289-440113cc3d00?w=400&h=300&fit=crop", | |
type: "Luxury Residence", | |
}, | |
]; | |
const [active, setActive] = useState(homes[0]); | |
const intervalTime = 2000; | |
useEffect(() => { | |
const interval = setInterval(() => { | |
setActive((prev) => { | |
const currentIndex = homes.indexOf(prev); | |
const nextIndex = (currentIndex + 1) % homes.length; | |
return homes[nextIndex]; | |
}); | |
}, intervalTime); | |
return () => clearInterval(interval); | |
}, []); | |
const Highlighter = () => { | |
return ( | |
<motion.div layoutId="highlighter" className="absolute inset-0"> | |
<div className="absolute -left-px -top-px h-4 w-4 rounded-tl-lg border-l-2 border-t-2 border-[var(--color-red-primary)] bg-transparent"></div> | |
<div className="absolute -right-px -top-px h-4 w-4 rounded-tr-lg border-r-2 border-t-2 border-[var(--color-red-primary)] bg-transparent"></div> | |
<div className="absolute -bottom-px -left-px h-4 w-4 rounded-bl-lg border-b-2 border-l-2 border-[var(--color-red-primary)] bg-transparent"></div> | |
<div className="absolute -bottom-px -right-px h-4 w-4 rounded-br-lg border-b-2 border-r-2 border-[var(--color-red-primary)] bg-transparent"></div> | |
</motion.div> | |
); | |
}; | |
const Badge = () => { | |
return ( | |
<motion.span | |
initial={{ opacity: 0, y: -20 }} | |
animate={{ opacity: 1, y: 0 }} | |
exit={{ opacity: 0, y: 20 }} | |
transition={{ | |
type: "spring", | |
stiffness: 260, | |
damping: 20, | |
delay: 1, | |
repeat: 0, | |
}} | |
className="absolute inset-x-0 bottom-4 m-auto h-fit w-fit rounded-md border border-neutral-100 bg-white px-2 py-1 text-xs text-black flex items-center gap-1" | |
> | |
<Home className="h-3 w-3" /> | |
<span className="font-medium">{active.type}</span> | |
</motion.span> | |
); | |
}; | |
return ( | |
<div className="p-6"> | |
<div className="grid grid-cols-2 justify-center gap-4"> | |
{homes.map((home, index) => ( | |
<motion.div | |
key={`home-${index}-home-skeleton-one`} | |
className="relative" | |
animate={{ | |
opacity: active.src === home.src ? 1 : 0.5, | |
filter: active.src === home.src ? "none" : "grayscale(100%)", | |
scale: active.src === home.src ? 0.95 : 1, | |
}} | |
transition={{ duration: 1 }} | |
> | |
{active.src === home.src && <Highlighter />} | |
{active.src === home.src && <Badge />} | |
<Image | |
key={`home-${index}`} | |
src={home.src} | |
alt="residential window" | |
width={100} | |
height={140} | |
className="h-[200px] w-full rounded-lg object-cover" | |
/> | |
</motion.div> | |
))} | |
</div> | |
</div> | |
); | |
}; | |
export const SkeletonTwo = () => { | |
const Cursor = ({ | |
className, | |
textClassName, | |
text, | |
}: { | |
className?: string; | |
textClassName?: string; | |
text?: string; | |
}) => { | |
return ( | |
<div | |
className={cn( | |
"absolute z-30 h-4 w-4 transition-all duration-200", | |
className | |
)} | |
> | |
<Building className="h-4 w-4 text-[var(--color-red-primary)]" /> | |
<div | |
className={cn( | |
"absolute left-3 top-3 whitespace-pre rounded-md p-1 text-[10px] text-neutral-500 transition duration-200", | |
textClassName | |
)} | |
> | |
{text ?? "Installing"} | |
</div> | |
</div> | |
); | |
}; | |
const Container = ({ | |
className, | |
children, | |
}: { | |
className?: string; | |
children: React.ReactNode; | |
}) => { | |
return ( | |
<div | |
className={cn( | |
"relative z-20 w-fit rounded-lg border border-neutral-100 p-0.5 shadow-sm", | |
className | |
)} | |
> | |
<div | |
className={cn( | |
"flex h-10 items-center justify-center rounded-[5px] bg-neutral-100 px-2 text-xs text-neutral-600 shadow-lg" | |
)} | |
> | |
{children} | |
</div> | |
</div> | |
); | |
}; | |
const CircleWithLine = ({ className }: { className?: string }) => { | |
const id = useId(); | |
return ( | |
<div | |
className={cn("flex flex-col items-center justify-center", className)} | |
> | |
<div | |
className={cn( | |
`h-3 w-3 rounded-full border border-neutral-200 bg-neutral-100 shadow-[2px_4px_16px_0px_rgba(248,248,248,0.06)_inset]` | |
)} | |
/> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width="2" | |
height="265" | |
viewBox="0 0 2 265" | |
fill="none" | |
className="h-full text-[var(--color-red-primary)]" | |
> | |
<path | |
d="M1 265V1" | |
stroke={`url(#${id})`} | |
strokeOpacity="0.3" | |
strokeWidth="1.5" | |
strokeLinecap="round" | |
/> | |
<defs> | |
<linearGradient | |
id={id} | |
x1="1.5" | |
y1="1" | |
x2="1.5" | |
y2="265" | |
gradientUnits="userSpaceOnUse" | |
> | |
<stop stopColor="currentColor" stopOpacity="0.1" /> | |
<stop | |
offset="0.530519" | |
stopColor="currentColor" | |
stopOpacity="0.8" | |
/> | |
<stop offset="1" stopColor="currentColor" stopOpacity="0.1" /> | |
</linearGradient> | |
</defs> | |
</svg> | |
</div> | |
); | |
}; | |
return ( | |
<div className="group/bento relative flex h-40 flex-col overflow-hidden px-2 py-8"> | |
<div className="absolute inset-0 flex h-full w-full flex-shrink-0 flex-row justify-center gap-4"> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
<CircleWithLine /> | |
</div> | |
<Container className="ml-20 mt-2">Site Assessment</Container> | |
<Container className="ml-40 mt-4 transition duration-200 group-hover/bento:scale-[1.02] group-hover/bento:border-[var(--color-red-primary)]"> | |
Professional Installation | |
</Container> | |
<Cursor | |
className="left-20 top-20 group-hover/bento:left-32" | |
textClassName="group-hover/bento:text-[var(--color-red-primary)]" | |
/> | |
</div> | |
); | |
}; | |
export const SkeletonThree = () => { | |
const Beam = ({ | |
className, | |
delay, | |
duration, | |
width = 600, | |
...svgProps | |
}: { | |
className?: string; | |
delay?: number; | |
duration?: number; | |
width?: number; | |
} & React.ComponentProps<typeof motion.svg>) => { | |
const id = useId(); | |
return ( | |
<motion.svg | |
width={width ?? "600"} | |
height="1" | |
viewBox={`0 0 ${width ?? "600"} 1`} | |
fill="none" | |
xmlns="http://www.w3.org/2000/svg" | |
className={cn("absolute inset-x-0 w-full", className)} | |
{...svgProps} | |
> | |
<motion.path | |
d={`M0 0.5H${width ?? "600"}`} | |
stroke={`url(#svgGradient-${id})`} | |
/> | |
<defs> | |
<motion.linearGradient | |
id={`svgGradient-${id}`} | |
gradientUnits="userSpaceOnUse" | |
initial={{ x1: "0%", x2: "-5%", y1: 0, y2: 0 }} | |
animate={{ x1: "110%", x2: "105%", y1: 0, y2: 0 }} | |
transition={{ | |
duration: duration ?? 2, | |
ease: "linear", | |
repeat: Infinity, | |
delay: delay, | |
repeatDelay: Math.random() * (2 - 1) + 1, | |
}} | |
> | |
<stop stopColor="#DC2626" stopOpacity="0" /> | |
<stop stopColor="#DC2626" /> | |
<stop offset="1" stopColor="#DC2626" stopOpacity="0" /> | |
</motion.linearGradient> | |
</defs> | |
</motion.svg> | |
); | |
}; | |
const firstImageTextVariants = { | |
initial: { y: 20, opacity: 0 }, | |
animate: { y: -30, opacity: 1 }, | |
}; | |
return ( | |
<motion.div className="relative pb-4"> | |
<Beam className="left-0 top-2 w-full" delay={0.5} /> | |
<Beam className="left-0 top-4 w-full" delay={0} /> | |
<Beam className="left-0 top-6 w-full" delay={1} /> | |
<Beam className="left-0 top-6 w-full" delay={3} /> | |
<Beam className="left-0 top-10 w-full" delay={2} /> | |
<Beam className="left-0 top-12 w-full" delay={3} /> | |
<Beam className="left-0 top-14 w-full" delay={0.2} /> | |
<Beam className="left-0 top-16 w-full" delay={1.2} /> | |
<Beam className="left-0 top-20 w-full" delay={2.3} /> | |
<div className="relative z-20 mx-auto grid max-w-[calc(100%-4rem)] grid-cols-2 justify-center gap-4"> | |
<div className="relative rounded-lg border border-neutral-200 bg-neutral-100 p-2"> | |
<motion.div | |
variants={firstImageTextVariants} | |
className="absolute inset-x-0 bottom-0 mx-auto w-fit rounded-md bg-white px-2 py-1 text-xs text-black opacity-0 flex items-center gap-1" | |
> | |
<Car className="h-3 w-3" /> | |
Luxury Sedan | |
</motion.div> | |
<Image | |
src="https://images.unsplash.com/photo-1555215695-3004980ad54e?w=400&h=300&fit=crop" | |
alt="luxury car" | |
width={100} | |
height={100} | |
className="h-full w-full rounded-lg object-cover" | |
/> | |
</div> | |
<div className="relative rounded-lg border border-neutral-200 bg-neutral-100 p-2"> | |
<motion.div | |
variants={firstImageTextVariants} | |
transition={{ delay: 0.5 }} | |
className="absolute inset-x-0 bottom-0 mx-auto w-fit rounded-md bg-white px-2 py-1 text-xs text-black opacity-0 flex items-center gap-1" | |
> | |
<Car className="h-3 w-3" /> | |
SUV Premium | |
</motion.div> | |
<Image | |
src="https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=400&h=300&fit=crop" | |
alt="luxury suv" | |
width={100} | |
height={100} | |
className="h-full w-full rounded-lg object-cover" | |
/> | |
</div> | |
</div> | |
</motion.div> | |
); | |
}; | |
export const SkeletonFour = ({}: {}) => { | |
const [activeFeature, setActiveFeature] = useState(0); | |
const features = [ | |
{ icon: Sparkles, name: "Skylights", description: "Custom solutions" }, | |
{ icon: Building, name: "Glass Doors", description: "Architectural grade" }, | |
{ icon: Home, name: "Conservatories", description: "Climate control" }, | |
]; | |
useEffect(() => { | |
const interval = setInterval(() => { | |
setActiveFeature((prev) => (prev + 1) % features.length); | |
}, 2000); | |
return () => clearInterval(interval); | |
}, []); | |
return ( | |
<div className="h-full w-full overflow-hidden"> | |
<motion.div | |
whileHover="animate" | |
className="relative block h-full w-full overflow-hidden rounded-none" | |
> | |
<div className="absolute inset-0 [mask-image:linear-gradient(to_top,white,transparent)]"> | |
<GridPattern /> | |
</div> | |
<div className="flex h-full max-h-96 flex-col items-center justify-center overflow-hidden [mask-image:linear-gradient(to_top,transparent,white,transparent)]"> | |
<div className="relative mx-auto w-full max-w-xs"> | |
{features.map((feature, idx) => ( | |
<motion.div | |
key={`feature-${idx}`} | |
className={cn( | |
"relative z-40 mx-auto mt-4 flex w-full flex-col items-center justify-center overflow-hidden rounded-md bg-white p-6 shadow-lg", | |
idx === activeFeature ? "opacity-100" : "opacity-40" | |
)} | |
animate={{ | |
scale: idx === activeFeature ? 1.05 : 1, | |
opacity: idx === activeFeature ? 1 : 0.4, | |
}} | |
transition={{ duration: 0.5 }} | |
> | |
<feature.icon className="h-8 w-8 text-[var(--color-red-primary)] mb-2" /> | |
<motion.p | |
className="text-sm font-medium text-neutral-700" | |
> | |
{feature.name} | |
</motion.p> | |
<motion.p | |
className="text-xs text-neutral-500 mt-1" | |
> | |
{feature.description} | |
</motion.p> | |
</motion.div> | |
))} | |
</div> | |
</div> | |
</motion.div> | |
</div> | |
); | |
}; | |
// Card structure | |
const CardSkeletonBody = ({ | |
children, | |
className, | |
}: { | |
children: React.ReactNode; | |
className?: string; | |
}) => { | |
return <div className={cn("", className)}>{children}</div>; | |
}; | |
const CardContent = ({ | |
children, | |
className, | |
}: { | |
children: React.ReactNode; | |
className?: string; | |
}) => { | |
return <div className={cn("p-6", className)}>{children}</div>; | |
}; | |
const CardTitle = ({ | |
children, | |
className, | |
}: { | |
children: React.ReactNode; | |
className?: string; | |
}) => { | |
return ( | |
<h3 | |
className={cn( | |
"font-sans text-sm font-medium tracking-tight text-[var(--color-dark-gray-secondary)]", | |
className | |
)} | |
> | |
{children} | |
</h3> | |
); | |
}; | |
const CardDescription = ({ | |
children, | |
className, | |
}: { | |
children: React.ReactNode; | |
className?: string; | |
}) => { | |
return ( | |
<h3 | |
className={cn( | |
"mt-2 max-w-xs font-sans text-sm font-normal tracking-tight text-[var(--color-supporting-gray)]", | |
className | |
)} | |
> | |
{children} | |
</h3> | |
); | |
}; | |
const Card = ({ | |
children, | |
className, | |
}: { | |
children: React.ReactNode; | |
className?: string; | |
}) => { | |
return ( | |
<motion.div | |
whileHover="animate" | |
className={cn( | |
"group isolate flex flex-col overflow-hidden rounded-2xl bg-white shadow-[0_1px_1px_rgba(0,0,0,0.05),0_4px_6px_rgba(34,42,53,0.04),0_24px_68px_rgba(47,48,55,0.05),0_2px_3px_rgba(0,0,0,0.04)]", | |
className | |
)} | |
> | |
{children} | |
</motion.div> | |
); | |
}; | |
// Variants | |
const mainVariant = { | |
initial: { x: 0, y: 0 }, | |
animate: { x: 20, y: -20, opacity: 0.9 }, | |
}; | |
const secondaryVariant = { initial: { opacity: 0 }, animate: { opacity: 1 } }; | |
// Utils | |
export function GridPattern() { | |
const columns = 20; | |
const rows = 5; | |
return ( | |
<div className="flex flex-shrink-0 scale-110 flex-wrap items-center justify-center gap-x-px gap-y-px bg-gray-100"> | |
{Array.from({ length: rows }).map((_, row) => | |
Array.from({ length: columns }).map((_, col) => { | |
const index = row * columns + col; | |
return ( | |
<div | |
key={`${col}-${row}-grid-pattern`} | |
className={`flex h-10 w-10 flex-shrink-0 rounded-[2px] ${ | |
index % 2 === 0 | |
? "bg-gray-50" | |
: "bg-gray-50 shadow-[0px_0px_1px_3px_rgba(255,255,255,1)_inset]" | |
}`} | |
/> | |
); | |
}) | |
)} | |
</div> | |
); | |
} |