Compare commits
6 Commits
version_1_
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bb8094c96 | |||
| 885d13185c | |||
| 8d5fb41632 | |||
| 4b616d1cdf | |||
| 8996a6925b | |||
| b50d4a7b99 |
16
src/App.tsx
16
src/App.tsx
@@ -40,7 +40,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<div id="hero" data-section="hero" className="relative">
|
||||
<HeroBillboard
|
||||
tag="Since 1994"
|
||||
title="Artisan Bread & Pastries"
|
||||
@@ -57,7 +57,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="about" data-section="about">
|
||||
<div id="about" data-section="about" className="relative">
|
||||
<AboutMediaOverlay
|
||||
tag="Our Story"
|
||||
title="Quality You Can Taste"
|
||||
@@ -70,7 +70,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="products" data-section="products">
|
||||
<div id="products" data-section="products" className="relative">
|
||||
<ProductMediaCards
|
||||
tag="Our Bakes"
|
||||
title="Fresh From The Oven"
|
||||
@@ -110,7 +110,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="features" data-section="features">
|
||||
<div id="features" data-section="features" className="relative">
|
||||
<FeaturesTaggedCards
|
||||
tag="Our Philosophy"
|
||||
title="Why Choose Us?"
|
||||
@@ -138,7 +138,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="metrics" data-section="metrics">
|
||||
<div id="metrics" data-section="metrics" className="relative">
|
||||
<MetricsSimpleCards
|
||||
tag="Our Impact"
|
||||
title="By The Numbers"
|
||||
@@ -160,7 +160,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="testimonials" data-section="testimonials">
|
||||
<div id="testimonials" data-section="testimonials" className="relative">
|
||||
<TestimonialMarqueeCards
|
||||
tag="Loved By Locals"
|
||||
title="What People Say"
|
||||
@@ -200,7 +200,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="faq" data-section="faq">
|
||||
<div id="faq" data-section="faq" className="relative">
|
||||
<FaqTwoColumn
|
||||
tag="Questions?"
|
||||
title="Common Inquiries"
|
||||
@@ -226,7 +226,7 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="contact" data-section="contact">
|
||||
<div id="contact" data-section="contact" className="relative">
|
||||
<ContactSplitEmail
|
||||
tag="Get in touch"
|
||||
title="Stay Connected"
|
||||
|
||||
15
src/components/GlassmorphicBadge.tsx
Normal file
15
src/components/GlassmorphicBadge.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
type GlassmorphicBadgeProps = {
|
||||
fact: string;
|
||||
};
|
||||
|
||||
const GlassmorphicBadge = ({ fact }: GlassmorphicBadgeProps) => {
|
||||
return (
|
||||
<div className="absolute top-4 right-4 p-4 rounded-xl border border-foreground/10 bg-background-accent/20 backdrop-blur-lg max-w-xs shadow-lg z-20">
|
||||
<p className="text-sm text-foreground">
|
||||
<span className="font-bold">Fun Fact:</span> {fact}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlassmorphicBadge;
|
||||
@@ -2,6 +2,8 @@ import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type AboutMediaOverlayProps = {
|
||||
tag: string;
|
||||
@@ -21,7 +23,10 @@ const AboutMediaOverlay = ({
|
||||
videoSrc,
|
||||
}: AboutMediaOverlayProps) => {
|
||||
return (
|
||||
<section aria-label="About section" className="py-20">
|
||||
<section aria-label="About section" className="py-20 relative">
|
||||
<div className="w-full text-center mb-4">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center py-8 md:py-12 mx-auto w-content-width rounded overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
|
||||
|
||||
@@ -3,6 +3,8 @@ import { motion } from "motion/react";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import { sendContactEmail } from "@/lib/api/email";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type ContactSplitEmailProps = {
|
||||
tag: string;
|
||||
@@ -38,6 +40,9 @@ const ContactSplitEmail = ({
|
||||
|
||||
return (
|
||||
<section aria-label="Contact section" className="py-20">
|
||||
<div className="w-full text-center mb-8">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Plus } from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type FaqItem = {
|
||||
question: string;
|
||||
@@ -72,6 +74,9 @@ const FaqTwoColumn = ({
|
||||
|
||||
return (
|
||||
<section aria-label="FAQ section" className="py-20">
|
||||
<div className="w-full text-center mb-8">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2">
|
||||
<span className="card rounded px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
@@ -1,92 +1,45 @@
|
||||
import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type FeatureItem = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
interface FeaturesTaggedCardsProps {
|
||||
type FeaturesTaggedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
};
|
||||
|
||||
const FeaturesTaggedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesTaggedCardsProps) => {
|
||||
const FeaturesTaggedCards = ({ tag, title, description, items }: FeaturesTaggedCardsProps) => {
|
||||
return (
|
||||
<section aria-label="Features section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
|
||||
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
<section className="relative py-20 bg-background text-foreground">
|
||||
<div className="w-full text-center mb-12">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
<h2 className="text-6xl font-medium text-center text-balance mt-4">{title}</h2>
|
||||
<p className="md:max-w-6/10 text-lg leading-tight text-center mx-auto mt-4">{description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{items.map((item) => (
|
||||
<div key={item.title} className="card p-6 rounded-lg">
|
||||
<img src={item.imageSrc} alt={item.title} className="rounded-md h-48 w-full object-cover mb-4" />
|
||||
<span className="text-xs uppercase bg-accent/20 text-accent px-2 py-1 rounded">{item.tag}</span>
|
||||
<h3 className="text-xl font-semibold mt-2">{item.title}</h3>
|
||||
<p className="text-foreground/80 mt-2">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<GridOrCarousel carouselThreshold={3}>
|
||||
{items.map((item) => (
|
||||
<div key={item.title} className="flex flex-col gap-5 h-full group">
|
||||
<div className="relative aspect-square rounded overflow-hidden">
|
||||
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="transition-transform duration-500 ease-in-out group-hover:scale-105" />
|
||||
<span className="absolute top-5 right-5 px-3 py-1 text-sm card rounded">{item.tag}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 p-5 flex-1 card rounded">
|
||||
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
|
||||
<p className="text-base leading-tight">{item.description}</p>
|
||||
{(item.primaryButton || item.secondaryButton) && (
|
||||
<div className="flex flex-wrap gap-3 mt-2">
|
||||
{item.primaryButton && <Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />}
|
||||
{item.secondaryButton && <Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesTaggedCards;
|
||||
export default FeaturesTaggedCards;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useButtonClick } from "@/hooks/useButtonClick";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
@@ -35,8 +37,11 @@ const FooterBasic = ({
|
||||
}) => {
|
||||
return (
|
||||
<footer aria-label="Site footer" className="w-full pt-20 pb-10">
|
||||
<div className="w-full text-center mb-10">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="w-full flex flex-wrap justify-between gap-y-10 mb-10">
|
||||
<div className="w-full flex flex-wrap justify-between gap-y-10">
|
||||
{columns.map((column) => (
|
||||
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
|
||||
<h3 className="text-sm opacity-50">{column.title}</h3>
|
||||
@@ -47,7 +52,7 @@ const FooterBasic = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20" />
|
||||
<div className="w-full h-px bg-foreground/20 mt-10" />
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-5">
|
||||
<span className="text-sm opacity-50">{leftText}</span>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type HeroBillboardProps = {
|
||||
tag: string;
|
||||
@@ -9,7 +8,8 @@ type HeroBillboardProps = {
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
const HeroBillboard = ({
|
||||
tag,
|
||||
@@ -18,45 +18,24 @@ const HeroBillboard = ({
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section aria-label="Hero section" className="pt-25 pb-20 md:py-30">
|
||||
<div className="flex flex-col gap-10 md:gap-13 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h1"
|
||||
className="text-6xl font-medium text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
||||
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
|
||||
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
|
||||
</div>
|
||||
<section className="relative py-20 bg-background text-foreground">
|
||||
<div className="w-full text-center mb-4">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
<h1 className="text-6xl font-medium text-balance mt-4">{title}</h1>
|
||||
<p className="text-lg leading-tight mt-4 max-w-2xl mx-auto">{description}</p>
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-8">
|
||||
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" />
|
||||
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
|
||||
className="w-full p-3 md:p-5 card rounded overflow-hidden"
|
||||
>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-4/5 md:aspect-video" />
|
||||
</motion.div>
|
||||
<img src={imageSrc} alt={title} className="mt-12 mx-auto rounded-lg w-full max-w-4xl" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboard;
|
||||
export default HeroBillboard;
|
||||
@@ -1,72 +1,41 @@
|
||||
import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const MetricsSimpleCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: {
|
||||
type MetricsSimpleCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
}) => (
|
||||
<section aria-label="Metrics section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2 w-content-width mx-auto">
|
||||
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
};
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
const MetricsSimpleCards = ({ tag, title, description, metrics }: MetricsSimpleCardsProps) => {
|
||||
return (
|
||||
<section className="relative py-20 bg-background text-foreground">
|
||||
<div className="w-full text-center mb-12">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<GridOrCarousel carouselThreshold={3}>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
<h2 className="text-6xl font-medium text-center text-balance mt-4">{title}</h2>
|
||||
<p className="md:max-w-6/10 text-lg leading-tight text-center mx-auto mt-4">{description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.value} className="flex flex-col justify-between gap-5 p-5 min-h-70 h-full card rounded">
|
||||
<span className="text-7xl md:text-8xl font-medium leading-none truncate">{metric.value}</span>
|
||||
<p className="text-base leading-tight text-balance">{metric.description}</p>
|
||||
<div key={metric.description} className="card p-8 rounded-lg">
|
||||
<p className="text-5xl font-bold text-accent">{metric.value}</p>
|
||||
<p className="text-lg mt-2 text-foreground/80">{metric.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsSimpleCards;
|
||||
export default MetricsSimpleCards;
|
||||
@@ -1,120 +1,43 @@
|
||||
import { ArrowUpRight, Loader2 } 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 GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import useProducts from "@/hooks/useProducts";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type Product = {
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
type ProductMediaCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
products?: {
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
products: Product[];
|
||||
};
|
||||
|
||||
const ProductMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
products: productsProp,
|
||||
}: ProductMediaCardsProps) => {
|
||||
const { products: fetchedProducts, isLoading } = useProducts();
|
||||
const isFromApi = fetchedProducts.length > 0;
|
||||
const products = isFromApi
|
||||
? fetchedProducts.map((p) => ({
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
imageSrc: p.imageSrc,
|
||||
onClick: p.onProductClick,
|
||||
}))
|
||||
: productsProp;
|
||||
|
||||
if (isLoading && !productsProp) {
|
||||
return (
|
||||
<section aria-label="Products section" className="py-20">
|
||||
<div className="w-content-width mx-auto flex justify-center">
|
||||
<Loader2 className="size-8 animate-spin text-foreground" strokeWidth={1.5} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ProductMediaCards = ({ tag, title, description, products }: ProductMediaCardsProps) => {
|
||||
return (
|
||||
<section aria-label="Products section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
|
||||
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
<section className="relative py-20 bg-background text-foreground">
|
||||
<div className="w-full text-center mb-12">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
<h2 className="text-6xl font-medium text-center text-balance mt-4">{title}</h2>
|
||||
<p className="md:max-w-6/10 text-lg leading-tight text-center mx-auto mt-4">{description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{products.map((product) => (
|
||||
<div key={product.name} className="card p-4 rounded-lg text-left">
|
||||
<img src={product.imageSrc} alt={product.name} className="rounded-md h-64 w-full object-cover" />
|
||||
<h3 className="text-xl font-semibold mt-4">{product.name}</h3>
|
||||
<p className="text-accent mt-1">{product.price}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<GridOrCarousel carouselThreshold={3}>
|
||||
{products.map((product) => (
|
||||
<button
|
||||
key={product.name}
|
||||
onClick={product.onClick}
|
||||
className="group h-full flex flex-col gap-5 p-5 text-left card rounded cursor-pointer"
|
||||
>
|
||||
<div className="aspect-square rounded overflow-hidden">
|
||||
<ImageOrVideo imageSrc={product.imageSrc} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-medium truncate">{product.name}</h3>
|
||||
<p className="text-2xl font-medium">{product.price}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center size-10 shrink-0 rounded primary-button">
|
||||
<ArrowUpRight className="size-4 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductMediaCards;
|
||||
export default ProductMediaCards;
|
||||
@@ -2,6 +2,8 @@ import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
@@ -30,6 +32,9 @@ const TestimonialMarqueeCards = ({
|
||||
|
||||
return (
|
||||
<section aria-label="Testimonials section" className="py-20">
|
||||
<div className="w-full text-center mb-8">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2 w-content-width mx-auto">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
|
||||
9
src/components/ui/GlassmorphicBadge.tsx
Normal file
9
src/components/ui/GlassmorphicBadge.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const GlassmorphicBadge = ({ fact }: { fact: string }) => {
|
||||
return (
|
||||
<div className="inline-block backdrop-blur-sm bg-white/30 rounded-lg px-3 py-1 text-sm text-foreground max-w-xs text-balance">
|
||||
{fact}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlassmorphicBadge;
|
||||
@@ -4,6 +4,8 @@ import { motion, AnimatePresence } from "motion/react";
|
||||
import { Plus, ArrowRight } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import Button from "@/components/ui/Button";
|
||||
import GlassmorphicBadge from "@/components/ui/GlassmorphicBadge";
|
||||
import { getRandomFact } from "@/utils/facts";
|
||||
|
||||
interface NavbarCenteredProps {
|
||||
logo: string;
|
||||
@@ -60,10 +62,13 @@ const NavbarCentered = ({ logo, navItems, ctaButton }: NavbarCenteredProps) => {
|
||||
<nav
|
||||
className={cls(
|
||||
"fixed z-1000 top-0 left-0 w-full transition-all duration-500 ease-in-out",
|
||||
isScrolled ? "h-15 bg-background/80 backdrop-blur-sm" : "h-20 bg-background/0 backdrop-blur-0"
|
||||
isScrolled ? "py-2 bg-background/80 backdrop-blur-sm" : "py-4 bg-background/0 backdrop-blur-0"
|
||||
)}
|
||||
>
|
||||
<div className="relative flex items-center justify-between h-full w-content-width mx-auto">
|
||||
<div className="w-full text-center">
|
||||
<GlassmorphicBadge fact={getRandomFact()} />
|
||||
</div>
|
||||
<div className="relative flex items-center justify-between w-content-width mx-auto mt-2">
|
||||
<Link to="/" className="text-xl font-medium text-foreground">{logo}</Link>
|
||||
|
||||
<div className="hidden md:flex absolute left-1/2 items-center gap-6 -translate-x-1/2">
|
||||
|
||||
@@ -175,3 +175,11 @@ h6 {
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
border: 1px solid var(--color-secondary-cta);
|
||||
}
|
||||
|
||||
.glassmorphic-badge {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
14
src/utils/facts.ts
Normal file
14
src/utils/facts.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const funnyFacts: string[] = [
|
||||
"Our sourdough starter is named 'Dough-minatrix'. She's very demanding.",
|
||||
"We once tried to make a bread so big it had its own zip code.",
|
||||
"Our croissants are 99% butter, 1% magic. And a little bit of flour.",
|
||||
"Legend says our head baker can communicate with yeast on a spiritual level.",
|
||||
"We accidentally invented a new pastry. We call it the 'Oopsie-danish'.",
|
||||
"Our cinnamon rolls are so good, they've been known to solve family disputes.",
|
||||
"The secret ingredient is love. And an alarming amount of butter.",
|
||||
"Our bakers sing to the bread. It makes the crust extra crispy.",
|
||||
];
|
||||
|
||||
export const getRandomFact = (): string => {
|
||||
return funnyFacts[Math.floor(Math.random() * funnyFacts.length)];
|
||||
};
|
||||
Reference in New Issue
Block a user