Merge version_2_1776716419597 into main #1

Merged
bender merged 1 commits from version_2_1776716419597 into main 2026-04-20 20:23:37 +00:00
8 changed files with 126 additions and 276 deletions

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

View File

@@ -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/GlassmorphicBadge";
import { getRandomFact } from "@/utils/facts";
type AboutMediaOverlayProps = {
tag: string;
@@ -21,7 +23,8 @@ const AboutMediaOverlay = ({
videoSrc,
}: AboutMediaOverlayProps) => {
return (
<section aria-label="About section" className="py-20">
<section aria-label="About section" className="py-20 relative">
<GlassmorphicBadge fact={getRandomFact()} />
<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} />

View File

@@ -1,92 +1,43 @@
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/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">
<GlassmorphicBadge fact={getRandomFact()} />
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<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;

View File

@@ -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/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,22 @@ 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">
<GlassmorphicBadge fact={getRandomFact()} />
<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;

View File

@@ -1,72 +1,39 @@
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/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>
)}
</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}>
const MetricsSimpleCards = ({ tag, title, description, metrics }: MetricsSimpleCardsProps) => {
return (
<section className="relative py-20 bg-background text-foreground">
<GlassmorphicBadge fact={getRandomFact()} />
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<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;

View File

@@ -1,120 +1,41 @@
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/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">
<GlassmorphicBadge fact={getRandomFact()} />
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<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;

View File

@@ -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/GlassmorphicBadge";
import { getRandomFact } from "@/utils/facts";
type Testimonial = {
name: string;

14
src/utils/facts.ts Normal file
View 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)];
};