Bob AI: replace the existing hero section with the [Block: Hero Bill
This commit is contained in:
@@ -85,6 +85,11 @@ export default function App() {
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/top-view-plastic-box-with-leftover-cookie_23-2148666825.jpg?_wi=1",
|
||||
},
|
||||
]}
|
||||
testimonials={[
|
||||
{ name: "Sarah J.", review: "The Kimchi Carbonara is absolutely divine! A perfect blend of creamy and spicy. I could eat this every day." },
|
||||
{ name: "Michael K.", review: "Incredible flavors and super fast delivery. SeoulPasta is my new go-to for takeout. The Gochujang Bolognese is a must-try." },
|
||||
{ name: "Emily R.", review: "I was skeptical about Korean-Italian fusion, but I'm a convert! The Bulgogi Lasagna was rich, savory, and so unique." },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import { useCarouselControls } from "@/hooks/useCarouselControls";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TestimonialCard from "@/components/shared/TestimonialCard";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
type Slide = {
|
||||
tag: string;
|
||||
@@ -15,78 +12,63 @@ type Slide = {
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
review: string;
|
||||
};
|
||||
|
||||
type HeroBillboardCarouselProps = {
|
||||
slides: Slide[];
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const HeroBillboardCarousel = ({ slides }: HeroBillboardCarouselProps) => {
|
||||
const HeroBillboardCarousel = ({ slides, testimonials }: HeroBillboardCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
|
||||
const { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
|
||||
const { scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
|
||||
|
||||
return (
|
||||
<section aria-label="Hero section" className="relative">
|
||||
<div className="overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex">
|
||||
<section className="hero-billboard-carousel relative h-screen text-foreground">
|
||||
<div className="overflow-hidden h-full" ref={emblaRef}>
|
||||
<div className="flex h-full">
|
||||
{slides.map((slide, index) => (
|
||||
<div className="flex-[0_0_100%] min-w-0 relative h-screen" key={index}>
|
||||
<div className="absolute inset-0 bg-black/40 z-10" />
|
||||
<ImageOrVideo
|
||||
imageSrc={slide.imageSrc}
|
||||
videoSrc={slide.videoSrc}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 z-20 flex flex-col justify-center items-center text-center text-foreground p-6">
|
||||
<div className="w-content-width mx-auto flex flex-col items-center gap-3">
|
||||
<span className="px-3 py-1 mb-1 text-sm bg-white/10 backdrop-blur-sm border border-white/20 rounded">{slide.tag}</span>
|
||||
<TextAnimation
|
||||
text={slide.title}
|
||||
variant="slide-up"
|
||||
tag="h1"
|
||||
className="text-6xl font-medium text-balance"
|
||||
/>
|
||||
<TextAnimation
|
||||
text={slide.description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="text-base md:text-lg leading-tight text-balance max-w-3xl"
|
||||
/>
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
||||
<Button text={slide.primaryButton.text} href={slide.primaryButton.href} variant="primary" animateImmediately />
|
||||
<Button text={slide.secondaryButton.text} href={slide.secondaryButton.href} variant="secondary" animateImmediately delay={0.1} />
|
||||
</div>
|
||||
<div className="relative flex-[0_0_100%] min-w-0 h-full flex items-center justify-center" key={index}>
|
||||
<img src={slide.imageSrc} alt={slide.title} className="absolute top-0 left-0 w-full h-full object-cover -z-10 brightness-[0.6]" />
|
||||
<div className="text-center max-w-3xl p-4">
|
||||
<span className="inline-block px-3 py-1 mb-4 text-sm bg-white/10 border border-white/20 rounded-[var(--radius)]">{slide.tag}</span>
|
||||
<h1 className="text-6xl font-bold mb-4">{slide.title}</h1>
|
||||
<p className="text-lg mb-8">{slide.description}</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
{slide.primaryButton && <Button text={slide.primaryButton.text} href={slide.primaryButton.href} variant="primary" />}
|
||||
{slide.secondaryButton && <Button text={slide.secondaryButton.text} href={slide.secondaryButton.href} variant="secondary" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20 w-content-width mx-auto flex items-center justify-between px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={scrollPrev}
|
||||
disabled={prevDisabled}
|
||||
className="size-10 flex items-center justify-center bg-white/10 backdrop-blur-sm border border-white/20 rounded-full text-foreground disabled:opacity-50 transition-opacity"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={scrollNext}
|
||||
disabled={nextDisabled}
|
||||
className="size-10 flex items-center justify-center bg-white/10 backdrop-blur-sm border border-white/20 rounded-full text-foreground disabled:opacity-50 transition-opacity"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<ChevronRight className="size-5" />
|
||||
</button>
|
||||
|
||||
<div className="absolute bottom-40 left-1/2 -translate-x-1/2 flex items-center gap-4 bg-black/30 p-2 rounded-[var(--radius)] backdrop-blur-sm">
|
||||
<button onClick={scrollPrev} className="p-2 transition-opacity hover:opacity-80">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="w-24 h-0.5 bg-white/30 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-background" style={{ width: `${scrollProgress}%` }} />
|
||||
</div>
|
||||
<div className="w-full max-w-xs h-1 bg-white/20 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-background"
|
||||
style={{ width: `${scrollProgress}%` }}
|
||||
/>
|
||||
<button onClick={scrollNext} className="p-2 transition-opacity hover:opacity-80">
|
||||
<ChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="testimonials-section">
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<div className="flex justify-center gap-6 flex-wrap">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<TestimonialCard key={index} name={testimonial.name} review={testimonial.review} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
15
src/components/shared/TestimonialCard.tsx
Normal file
15
src/components/shared/TestimonialCard.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
type TestimonialCardProps = {
|
||||
name: string;
|
||||
review: string;
|
||||
};
|
||||
|
||||
const TestimonialCard = ({ name, review }: TestimonialCardProps) => {
|
||||
return (
|
||||
<div className="testimonial-card">
|
||||
<p className="review">"{review}"</p>
|
||||
<p className="name">- {name}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialCard;
|
||||
@@ -173,3 +173,37 @@ h6 {
|
||||
var(--color-secondary-cta);
|
||||
box-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||
}
|
||||
|
||||
.testimonials-section {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 2rem 0;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 20%, transparent);
|
||||
}
|
||||
|
||||
.testimonial-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
max-width: 350px;
|
||||
text-align: left;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.testimonial-card .review {
|
||||
font-style: italic;
|
||||
margin-bottom: 1rem;
|
||||
font-size: var(--text-base);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.testimonial-card .name {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user