Bob AI: replace the existing hero section with the [Block: Hero Bill

This commit is contained in:
kudinDmitriyUp
2026-04-21 06:51:01 +00:00
parent 0ced28ab2b
commit e3673eea09
4 changed files with 98 additions and 62 deletions

View File

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

View File

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

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

View File

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