Initial commit
This commit is contained in:
121
src/components/Layout.tsx
Normal file
121
src/components/Layout.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import FooterSimpleReveal from '@/components/sections/footer/FooterSimpleReveal';
|
||||
import NavbarInline from '@/components/ui/NavbarInline';
|
||||
import SiteBackgroundSlot from "@/components/ui/SiteBackgroundSlot";
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { StyleProvider } from "@/components/ui/StyleProvider";
|
||||
|
||||
export default function Layout() {
|
||||
const navItems = [
|
||||
{
|
||||
"name": "Home",
|
||||
"href": "#hero"
|
||||
},
|
||||
{
|
||||
"name": "About",
|
||||
"href": "#about"
|
||||
},
|
||||
{
|
||||
"name": "Collections",
|
||||
"href": "#products"
|
||||
},
|
||||
{
|
||||
"name": "Experiences",
|
||||
"href": "#pricing"
|
||||
},
|
||||
{
|
||||
"name": "Testimonials",
|
||||
"href": "#testimonials"
|
||||
},
|
||||
{
|
||||
"name": "FAQ",
|
||||
"href": "#faq"
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"href": "#contact"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StyleProvider buttonVariant="elastic" siteBackground="gridDots" heroBackground="gradientBars">
|
||||
<SiteBackgroundSlot />
|
||||
<NavbarInline
|
||||
ctaButton={{
|
||||
text: "Shop Now",
|
||||
href: "#products",
|
||||
}}
|
||||
logo="Brand"
|
||||
navItems={navItems} />
|
||||
<main className="flex-grow">
|
||||
<Outlet />
|
||||
</main>
|
||||
<FooterSimpleReveal
|
||||
brand="Store of Emotions"
|
||||
columns={[
|
||||
{
|
||||
title: "Shop",
|
||||
items: [
|
||||
{
|
||||
label: "Collections",
|
||||
href: "#products",
|
||||
},
|
||||
{
|
||||
label: "Subscriptions",
|
||||
href: "#pricing",
|
||||
},
|
||||
{
|
||||
label: "New Arrivals",
|
||||
href: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Support",
|
||||
items: [
|
||||
{
|
||||
label: "FAQ",
|
||||
href: "#faq",
|
||||
},
|
||||
{
|
||||
label: "Contact Us",
|
||||
href: "#contact",
|
||||
},
|
||||
{
|
||||
label: "Shipping & Returns",
|
||||
href: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
items: [
|
||||
{
|
||||
label: "About Us",
|
||||
href: "#about",
|
||||
},
|
||||
{
|
||||
label: "Our Philosophy",
|
||||
href: "#about",
|
||||
},
|
||||
{
|
||||
label: "Blog",
|
||||
href: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
copyright="© 2024 Store of Emotions. All rights reserved."
|
||||
links={[
|
||||
{
|
||||
label: "Privacy Policy",
|
||||
href: "#",
|
||||
},
|
||||
{
|
||||
label: "Terms of Service",
|
||||
href: "#",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StyleProvider>
|
||||
);
|
||||
}
|
||||
126
src/components/ecommerce/ProductCart.tsx
Normal file
126
src/components/ecommerce/ProductCart.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useEffect } from "react";
|
||||
import { X, Plus, Minus, Trash2 } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
|
||||
type CartItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
quantity: number;
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
type ProductCartProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
items: CartItem[];
|
||||
total: string;
|
||||
onQuantityChange?: (id: string, quantity: number) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
onCheckout?: () => void;
|
||||
};
|
||||
|
||||
const ProductCart = ({ isOpen, onClose, items, total, onQuantityChange, onRemove, onCheckout }: ProductCartProps) => {
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||
return () => { document.body.style.overflow = ""; };
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-1001">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="absolute inset-0 bg-foreground/50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<motion.aside
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="card absolute right-0 top-0 flex flex-col p-5 h-screen w-screen md:w-96"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-medium text-foreground">Cart ({items.length})</h2>
|
||||
<button onClick={onClose} className="card flex items-center justify-center size-8 rounded cursor-pointer" aria-label="Close cart">
|
||||
<X className="size-4 text-foreground" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 h-px w-full bg-foreground/10" />
|
||||
|
||||
<div className="flex-1 py-5 min-h-0 overflow-y-auto">
|
||||
{items.length === 0 ? (
|
||||
<p className="py-20 text-center text-sm text-foreground/50">Your cart is empty</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex gap-4">
|
||||
<div className="shrink-0 size-24 overflow-hidden rounded">
|
||||
<ImageOrVideo imageSrc={item.imageSrc} className="size-full object-cover" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-between min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="text-base font-medium text-foreground truncate">{item.name}</h3>
|
||||
<p className="shrink-0 text-base font-medium text-foreground">{item.price}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => item.quantity > 1 && onQuantityChange?.(item.id, item.quantity - 1)}
|
||||
className="card flex items-center justify-center size-8 rounded cursor-pointer"
|
||||
>
|
||||
<Minus className="size-4 text-foreground" strokeWidth={1.5} />
|
||||
</button>
|
||||
<span className="min-w-5 text-center text-sm font-medium text-foreground">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => onQuantityChange?.(item.id, item.quantity + 1)}
|
||||
className="card flex items-center justify-center size-8 rounded cursor-pointer"
|
||||
>
|
||||
<Plus className="size-4 text-foreground" strokeWidth={1.5} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRemove?.(item.id)}
|
||||
className="card flex items-center justify-center ml-auto size-8 rounded cursor-pointer"
|
||||
>
|
||||
<Trash2 className="size-4 text-foreground" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="h-px w-full bg-foreground/10" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-medium text-foreground">Total</span>
|
||||
<span className="text-base font-medium text-foreground">{total}</span>
|
||||
</div>
|
||||
<Button text="Checkout" onClick={onCheckout} variant="primary" className="w-full" />
|
||||
</div>
|
||||
</motion.aside>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCart;
|
||||
export type { CartItem };
|
||||
142
src/components/ecommerce/ProductCatalog.tsx
Normal file
142
src/components/ecommerce/ProductCatalog.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Star, ArrowUpRight, Loader2 } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import useProducts from "@/hooks/useProducts";
|
||||
import type { ProductVariant } from "./ProductDetailCard";
|
||||
|
||||
type CatalogProduct = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
category?: string;
|
||||
rating?: number;
|
||||
reviewCount?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type ProductCatalogProps = {
|
||||
products?: CatalogProduct[];
|
||||
searchValue?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
filters?: ProductVariant[];
|
||||
};
|
||||
|
||||
const ProductCatalog = ({ products: productsProp, searchValue = "", onSearchChange, filters }: ProductCatalogProps) => {
|
||||
const { products: fetchedProducts, isLoading } = useProducts();
|
||||
|
||||
const products: CatalogProduct[] = productsProp && productsProp.length > 0
|
||||
? productsProp
|
||||
: fetchedProducts.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
imageSrc: p.imageSrc,
|
||||
category: p.brand,
|
||||
rating: p.rating,
|
||||
reviewCount: p.reviewCount,
|
||||
onClick: p.onProductClick,
|
||||
}));
|
||||
|
||||
if (isLoading && (!productsProp || productsProp.length === 0)) {
|
||||
return (
|
||||
<section className="mx-auto py-20 w-content-width">
|
||||
<div className="flex justify-center">
|
||||
<Loader2 className="size-8 text-foreground animate-spin" strokeWidth={1.5} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mx-auto py-20 w-content-width">
|
||||
{(onSearchChange || (filters && filters.length > 0)) && (
|
||||
<div className="flex flex-col gap-5 mb-5 md:flex-row md:items-end">
|
||||
{onSearchChange && (
|
||||
<div className="flex flex-1 flex-col gap-2 min-w-32">
|
||||
<label className="text-sm font-medium text-foreground">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Search products..."
|
||||
className="card px-4 h-9 w-full md:w-80 text-base text-foreground bg-transparent rounded focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="flex gap-5 items-end">
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.label} className="flex flex-col gap-2 min-w-32">
|
||||
<label className="text-sm font-medium text-foreground">{filter.label}</label>
|
||||
<div className="secondary-button flex items-center px-3 h-9 rounded">
|
||||
<select
|
||||
value={filter.selected}
|
||||
onChange={(e) => filter.onChange(e.target.value)}
|
||||
className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none"
|
||||
>
|
||||
{filter.options.map((option) => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<p className="py-20 text-center text-sm text-foreground/50">No products found</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{products.map((product) => (
|
||||
<button
|
||||
key={product.id}
|
||||
onClick={product.onClick}
|
||||
className="card group h-full flex flex-col gap-3 p-3 text-left rounded cursor-pointer"
|
||||
>
|
||||
<div className="relative aspect-square rounded overflow-hidden">
|
||||
<ImageOrVideo imageSrc={product.imageSrc} className="size-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
<div className="absolute inset-0 flex items-center justify-center transition-all duration-300 group-hover:bg-background/20 group-hover:backdrop-blur-xs">
|
||||
<div className="primary-button flex items-center justify-center size-12 rounded-full opacity-0 scale-75 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100">
|
||||
<ArrowUpRight className="size-5 text-primary-cta-text" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{product.category && (
|
||||
<span className="secondary-button w-fit px-2 py-0.5 text-sm text-secondary-cta-text rounded">{product.category}</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-xl font-medium text-foreground truncate">{product.name}</h3>
|
||||
{product.rating && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={cls("size-4 text-accent", i < Math.floor(product.rating || 0) ? "fill-accent" : "opacity-20")}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{product.reviewCount && (
|
||||
<span className="text-sm text-foreground">({product.reviewCount})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-2xl font-medium text-foreground">{product.price}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCatalog;
|
||||
export type { CatalogProduct };
|
||||
137
src/components/ecommerce/ProductDetailCard.tsx
Normal file
137
src/components/ecommerce/ProductDetailCard.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Transition from "@/components/ui/Transition";
|
||||
|
||||
type ProductVariant = {
|
||||
label: string;
|
||||
options: string[];
|
||||
selected: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type ProductDetailCardProps = {
|
||||
name: string;
|
||||
price: string;
|
||||
salePrice?: string;
|
||||
images: string[];
|
||||
description?: string;
|
||||
rating?: number;
|
||||
ribbon?: string;
|
||||
inventoryStatus?: "in-stock" | "out-of-stock";
|
||||
inventoryQuantity?: number;
|
||||
sku?: string;
|
||||
variants?: ProductVariant[];
|
||||
quantity?: ProductVariant;
|
||||
onAddToCart?: () => void;
|
||||
onBuyNow?: () => void;
|
||||
};
|
||||
|
||||
const ProductDetailCard = ({ name, price, salePrice, images, description, rating = 0, ribbon, inventoryStatus, inventoryQuantity, sku, variants, quantity, onAddToCart, onBuyNow }: ProductDetailCardProps) => {
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
|
||||
return (
|
||||
<section className="mx-auto py-20 w-content-width">
|
||||
<div className="flex flex-col gap-5 md:flex-row">
|
||||
<div className="relative md:w-1/2">
|
||||
<Transition key={selectedImage} className="card aspect-square overflow-hidden rounded" transitionType="fade" whileInView={false}>
|
||||
<ImageOrVideo imageSrc={images[selectedImage]} className="size-full object-cover" />
|
||||
</Transition>
|
||||
{images.length > 1 && (
|
||||
<div className="absolute right-3 top-0 bottom-0 flex flex-col gap-3 py-3 overflow-y-auto mask-fade-y">
|
||||
{images.map((src, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setSelectedImage(i)}
|
||||
className="group card relative shrink-0 size-16 overflow-hidden rounded cursor-pointer"
|
||||
>
|
||||
<ImageOrVideo imageSrc={src} className="size-full object-cover transition-transform duration-300 group-hover:scale-110" />
|
||||
<div className={cls(
|
||||
"absolute top-1 right-1 primary-button size-3 rounded-full transition-transform duration-300",
|
||||
selectedImage === i ? "scale-100" : "scale-0 group-hover:scale-100"
|
||||
)} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card flex flex-col gap-5 p-5 md:w-1/2 rounded">
|
||||
<div className="flex items-start justify-between gap-5">
|
||||
<h2 className="flex-1 text-2xl font-medium text-foreground md:text-3xl">{name}</h2>
|
||||
{ribbon && <span className="secondary-button shrink-0 px-3 py-1 text-sm font-medium rounded text-secondary-cta-text">{ribbon}</span>}
|
||||
</div>
|
||||
<div className="h-px w-full bg-foreground/10" />
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-foreground md:text-2xl">
|
||||
{salePrice ? (
|
||||
<>
|
||||
<span className="text-foreground/75 line-through mr-1">{price}</span>
|
||||
<span>{salePrice}</span>
|
||||
</>
|
||||
) : (
|
||||
price
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star key={i} className={cls("size-5 text-accent", i < Math.floor(rating) ? "fill-accent" : "opacity-20")} strokeWidth={1.5} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{(inventoryStatus || inventoryQuantity || sku) && (
|
||||
<div className="flex flex-wrap gap-3 text-sm">
|
||||
{inventoryStatus && (
|
||||
<span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">
|
||||
{inventoryStatus === "in-stock" ? "In Stock" : "Out of Stock"}
|
||||
</span>
|
||||
)}
|
||||
{inventoryQuantity && (
|
||||
<span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">{inventoryQuantity} available</span>
|
||||
)}
|
||||
{sku && <span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">SKU: {sku}</span>}
|
||||
</div>
|
||||
)}
|
||||
{description && <p className="text-sm text-foreground/75 md:text-base">{description}</p>}
|
||||
{variants && variants.length > 0 && (
|
||||
<div className="flex flex-wrap gap-5">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant.label} className="flex flex-1 flex-col gap-2 min-w-32">
|
||||
<label className="text-sm font-medium text-foreground">{variant.label}</label>
|
||||
<div className="secondary-button flex items-center px-3 h-9 rounded">
|
||||
<select value={variant.selected} onChange={(e) => variant.onChange(e.target.value)} className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none">
|
||||
{variant.options.map((option) => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{quantity && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-foreground">{quantity.label}</label>
|
||||
<div className="secondary-button flex items-center px-3 h-9 w-24 rounded">
|
||||
<select value={quantity.selected} onChange={(e) => quantity.onChange(e.target.value)} className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none">
|
||||
{quantity.options.map((option) => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col mt-auto gap-3 pt-5">
|
||||
<Button text="Add To Cart" onClick={onAddToCart} variant="primary" className="w-full" />
|
||||
<Button text="Buy Now" onClick={onBuyNow} variant="secondary" className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetailCard;
|
||||
export type { ProductVariant };
|
||||
13
src/components/layouts/DefaultsLayout.tsx
Normal file
13
src/components/layouts/DefaultsLayout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const DefaultsLayout = () => {
|
||||
useEffect(() => {
|
||||
document.body.classList.add('use-defaults');
|
||||
return () => document.body.classList.remove('use-defaults');
|
||||
}, []);
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default DefaultsLayout;
|
||||
142
src/components/sections/about/AboutFeaturesSplit.tsx
Normal file
142
src/components/sections/about/AboutFeaturesSplit.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { motion } from "motion/react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
type AboutFeaturesSplitProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: { icon: string | LucideIcon; title: string; description: string }[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const AboutFeaturesSplit = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: AboutFeaturesSplitProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="AboutFeaturesSplit"
|
||||
aria-label="About section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 mx-auto w-content-width">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-stretch gap-5">
|
||||
<div className="flex flex-col justify-center gap-5 p-5 w-full md:w-4/10 2xl:w-3/10 card rounded-theme">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-center shrink-0 mb-1 size-8 primary-button rounded-theme">
|
||||
{typeof item.icon === "function" ? (
|
||||
<item.icon className="h-2/5 w-2/5 text-primary-cta-text" strokeWidth={1.5} />
|
||||
) : (
|
||||
<span className="text-sm text-primary-cta-text">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-medium">{item.title}</h3>
|
||||
<p className="text-base leading-tight">{item.description}</p>
|
||||
</div>
|
||||
{index < items.length - 1 && (
|
||||
<div className="mt-5 border-b border-accent/40" />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="p-5 w-full md:w-6/10 2xl:w-7/10 h-80 md:h-auto card rounded-theme overflow-hidden"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="About video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutFeaturesSplit;
|
||||
107
src/components/sections/about/AboutMediaOverlay.tsx
Normal file
107
src/components/sections/about/AboutMediaOverlay.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type AboutMediaOverlayProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const AboutMediaOverlay = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: AboutMediaOverlayProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="AboutMediaOverlay"
|
||||
aria-label="About section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="relative flex items-center justify-center py-8 md:py-8 mx-auto w-content-width rounded-theme overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="About video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-foreground/40 backdrop-blur-xs pointer-events-none select-none" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center justify-center px-5 py-8 mx-auto min-h-100 md:min-h-120 md:w-1/2 w-content-width">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="mb-1 px-3 py-1 text-sm card rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance text-primary-cta-text"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-primary-cta-text"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutMediaOverlay;
|
||||
90
src/components/sections/about/AboutTestimonial.tsx
Normal file
90
src/components/sections/about/AboutTestimonial.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Quote } from "lucide-react";
|
||||
|
||||
type AboutTestimonialProps = {
|
||||
tag: string;
|
||||
quote: string;
|
||||
author: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const AboutTestimonial = ({
|
||||
tag,
|
||||
quote,
|
||||
author,
|
||||
role,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: AboutTestimonialProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="AboutTestimonial"
|
||||
aria-label="Testimonial section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-5 mx-auto w-content-width">
|
||||
<div className="relative md:col-span-3 p-8 card rounded-theme">
|
||||
<div className="absolute flex items-center justify-center -top-7 -left-7 md:-top-8 md:-left-8 size-12 primary-button rounded-theme">
|
||||
<Quote className="size-5 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col justify-center gap-5 py-8 h-full">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-fit px-3 py-1 mb-1 text-sm card rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||
className="text-3xl md:text-4xl font-medium leading-tight"
|
||||
>
|
||||
{quote}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">{author}</span>
|
||||
<span className="text-accent">•</span>
|
||||
<span className="text-base">{role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="md:col-span-2 aspect-square md:aspect-auto md:h-full card rounded-theme overflow-hidden"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Testimonial video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutTestimonial;
|
||||
56
src/components/sections/about/AboutText.tsx
Normal file
56
src/components/sections/about/AboutText.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
interface AboutTextProps {
|
||||
title: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
}
|
||||
|
||||
const AboutText = ({
|
||||
title,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
}: AboutTextProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="AboutText"
|
||||
aria-label="About section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-3 items-center">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="text-2xl md:text-5xl font-medium text-center leading-tight text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap gap-3 justify-center mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutText;
|
||||
79
src/components/sections/about/AboutTextSplit.tsx
Normal file
79
src/components/sections/about/AboutTextSplit.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
interface AboutTextSplitProps {
|
||||
title: string;
|
||||
descriptions: string[];
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
}
|
||||
|
||||
const AboutTextSplit = ({
|
||||
title,
|
||||
descriptions,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
}: AboutTextSplitProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="AboutTextSplit"
|
||||
aria-label="About section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 mx-auto w-content-width">
|
||||
<div className="flex flex-col md:flex-row gap-3 md:gap-8">
|
||||
<div className="w-full md:w-1/2">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="text-7xl font-medium"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 w-full md:w-1/2">
|
||||
{descriptions.map((desc, index) => (
|
||||
<motion.p
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="text-base md:text-2xl leading-tight"
|
||||
>
|
||||
{desc}
|
||||
</motion.p>
|
||||
))}
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap max-md:justify-center gap-5">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-b border-foreground/10" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutTextSplit;
|
||||
148
src/components/sections/blog/BlogMediaCards.tsx
Normal file
148
src/components/sections/blog/BlogMediaCards.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
type BlogItem = {
|
||||
category: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
authorName: string;
|
||||
authorImageSrc: string;
|
||||
date: string;
|
||||
imageSrc: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type BlogMediaCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items?: BlogItem[];
|
||||
};
|
||||
|
||||
const BlogMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: BlogMediaCardsProps) => {
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="BlogMediaCards"
|
||||
aria-label="Blog section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{items.map((item, index) => (
|
||||
<motion.a
|
||||
key={index}
|
||||
href={item.href ?? "#"}
|
||||
onClick={item.onClick}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="card group flex flex-col justify-between gap-5 p-5 rounded-theme cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="card w-fit rounded-full px-2 py-0.5 text-xs mb-0.5">{item.category}</span>
|
||||
|
||||
<h3 className="text-2xl md:text-3xl font-medium leading-tight line-clamp-2">{item.title}</h3>
|
||||
<p className="text-sm leading-tight opacity-75 line-clamp-2">{item.excerpt}</p>
|
||||
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<img
|
||||
src={item.authorImageSrc}
|
||||
alt={item.authorName}
|
||||
className="size-8 rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{item.authorName}</span>
|
||||
<span className="text-xs opacity-75">{item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative aspect-square rounded-theme overflow-hidden">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt={item.title}
|
||||
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
|
||||
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
|
||||
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogMediaCards;
|
||||
151
src/components/sections/blog/BlogSimpleCards.tsx
Normal file
151
src/components/sections/blog/BlogSimpleCards.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
type BlogItem = {
|
||||
category: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
authorName: string;
|
||||
authorImageSrc: string;
|
||||
date: string;
|
||||
imageSrc: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type BlogSimpleCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items?: BlogItem[];
|
||||
};
|
||||
|
||||
const BlogSimpleCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: BlogSimpleCardsProps) => {
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="BlogSimpleCards"
|
||||
aria-label="Blog section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{items.map((item, index) => (
|
||||
<motion.a
|
||||
key={index}
|
||||
href={item.href ?? "#"}
|
||||
onClick={item.onClick}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="card group flex flex-col gap-5 p-5 rounded-theme cursor-pointer"
|
||||
>
|
||||
<div className="relative aspect-4/3 rounded-theme overflow-hidden">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt={item.title}
|
||||
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
|
||||
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
|
||||
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col justify-between gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="primary-button w-fit rounded-full px-2 py-0.5 text-xs text-primary-cta-text">
|
||||
{item.category}
|
||||
</span>
|
||||
<h3 className="text-xl font-medium leading-tight mt-1">{item.title}</h3>
|
||||
<p className="text-sm leading-tight opacity-75">{item.excerpt}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={item.authorImageSrc}
|
||||
alt={item.authorName}
|
||||
className="size-8 rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{item.authorName}</span>
|
||||
<span className="text-xs opacity-75">{item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogSimpleCards;
|
||||
158
src/components/sections/blog/BlogTagCards.tsx
Normal file
158
src/components/sections/blog/BlogTagCards.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
type BlogItem = {
|
||||
title: string;
|
||||
excerpt: string;
|
||||
authorName: string;
|
||||
authorImageSrc: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
imageSrc: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type BlogTagCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items?: BlogItem[];
|
||||
};
|
||||
|
||||
const BlogTagCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: BlogTagCardsProps) => {
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="BlogTagCards"
|
||||
aria-label="Blog section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{items.map((item, index) => (
|
||||
<motion.a
|
||||
key={index}
|
||||
href={item.href ?? "#"}
|
||||
onClick={item.onClick}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="card group flex flex-col gap-5 p-5 rounded-theme cursor-pointer"
|
||||
>
|
||||
<div className="relative aspect-4/3 rounded-theme overflow-hidden">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt={item.title}
|
||||
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
|
||||
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
|
||||
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col justify-between gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={item.authorImageSrc}
|
||||
alt={item.authorName}
|
||||
className="size-5 rounded-full object-cover"
|
||||
/>
|
||||
<span className="text-xs opacity-75">
|
||||
{item.authorName} • {item.date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-medium leading-tight">{item.title}</h3>
|
||||
<p className="text-sm leading-tight opacity-75">{item.excerpt}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map((itemTag) => (
|
||||
<span
|
||||
key={itemTag}
|
||||
className="primary-button rounded-full px-2 py-0.5 text-xs text-primary-cta-text"
|
||||
>
|
||||
{itemTag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogTagCards;
|
||||
82
src/components/sections/contact/ContactCenter.tsx
Normal file
82
src/components/sections/contact/ContactCenter.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
const ContactCenter = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
inputPlaceholder,
|
||||
buttonText,
|
||||
termsText,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
inputPlaceholder: string;
|
||||
buttonText: string;
|
||||
termsText?: string;
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ContactCenter"
|
||||
aria-label="Contact section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex items-center justify-center py-20 card rounded-theme"
|
||||
>
|
||||
<div className="flex flex-col items-center w-full md:w-1/2 gap-3 px-5">
|
||||
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-5xl md:text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-8/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<form className="flex flex-col md:flex-row w-full md:w-8/10 2xl:w-6/10 gap-3 md:gap-1 p-1 mt-2 card rounded-theme">
|
||||
<input
|
||||
type="email"
|
||||
placeholder={inputPlaceholder}
|
||||
aria-label="Email address"
|
||||
className="flex-1 px-5 py-3 md:py-0 text-base text-center md:text-left bg-transparent placeholder:opacity-75 focus:outline-none truncate"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{termsText && (
|
||||
<p className="text-xs opacity-75 text-center md:max-w-8/10 2xl:max-w-6/10">
|
||||
{termsText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactCenter;
|
||||
70
src/components/sections/contact/ContactCta.tsx
Normal file
70
src/components/sections/contact/ContactCta.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
const ContactCta = ({
|
||||
tag,
|
||||
text,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
}: {
|
||||
tag: string;
|
||||
text: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ContactCta"
|
||||
aria-label="Contact section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex items-center justify-center py-20 px-5 md:px-8 card rounded-theme"
|
||||
>
|
||||
<div className="w-full md:w-3/4 flex flex-col items-center gap-3">
|
||||
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-4xl md:text-5xl font-medium text-center leading-tight text-balance"
|
||||
>
|
||||
{text}
|
||||
</motion.h2>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactCta;
|
||||
108
src/components/sections/contact/ContactSplitEmail.tsx
Normal file
108
src/components/sections/contact/ContactSplitEmail.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type ContactSplitEmailProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
inputPlaceholder: string;
|
||||
buttonText: string;
|
||||
termsText?: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const ContactSplitEmail = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
inputPlaceholder,
|
||||
buttonText,
|
||||
termsText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: ContactSplitEmailProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ContactSplitEmail"
|
||||
aria-label="Contact section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-5"
|
||||
>
|
||||
<div className="flex items-center justify-center py-15 md:py-20 card rounded-theme">
|
||||
<div className="flex flex-col items-center w-full gap-3 px-5">
|
||||
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-5xl md:text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-8/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<form className="flex flex-col md:flex-row w-full md:w-8/10 2xl:w-6/10 gap-3 md:gap-1 p-1 mt-2 card rounded-theme">
|
||||
<input
|
||||
type="email"
|
||||
placeholder={inputPlaceholder}
|
||||
aria-label="Email address"
|
||||
className="flex-1 px-5 py-3 md:py-0 text-base text-center md:text-left bg-transparent placeholder:opacity-75 focus:outline-none truncate"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{termsText && (
|
||||
<p className="text-xs opacity-75 text-center md:max-w-8/10 2xl:max-w-6/10">
|
||||
{termsText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-100 md:h-auto card rounded-theme overflow-hidden">
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Contact video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSplitEmail;
|
||||
135
src/components/sections/contact/ContactSplitForm.tsx
Normal file
135
src/components/sections/contact/ContactSplitForm.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type InputField = {
|
||||
name: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
type TextareaField = {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
rows?: number;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
type ContactSplitFormProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
inputs: InputField[];
|
||||
textarea?: TextareaField;
|
||||
buttonText: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const ContactSplitForm = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
inputs,
|
||||
textarea,
|
||||
buttonText,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: ContactSplitFormProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ContactSplitForm"
|
||||
aria-label="Contact section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 md:auto-rows-fr gap-5"
|
||||
>
|
||||
<div className="p-5 md:p-8 card rounded-theme">
|
||||
<form className="flex flex-col gap-5">
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-4xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-sm md:text-base leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{inputs.map((input) => (
|
||||
<input
|
||||
key={input.name}
|
||||
type={input.type}
|
||||
name={input.name}
|
||||
placeholder={input.placeholder}
|
||||
required={input.required}
|
||||
aria-label={input.placeholder}
|
||||
className="card rounded-theme h-9 px-3 w-full text-sm"
|
||||
/>
|
||||
))}
|
||||
|
||||
{textarea && (
|
||||
<textarea
|
||||
name={textarea.name}
|
||||
placeholder={textarea.placeholder}
|
||||
required={textarea.required}
|
||||
rows={textarea.rows || 5}
|
||||
aria-label={textarea.placeholder}
|
||||
className="card rounded-theme p-3 w-full text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="h-100 md:h-full card rounded-theme overflow-hidden">
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Contact video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSplitForm;
|
||||
107
src/components/sections/faq/FaqSimple.tsx
Normal file
107
src/components/sections/faq/FaqSimple.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
type FaqItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
const FaqSimple = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FaqItem[];
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FaqSimple"
|
||||
aria-label="FAQ section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{items.map((item, index) => (
|
||||
<motion.details
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05 + index * 0.05, ease: "easeOut" }}
|
||||
className="card rounded-theme p-4 group"
|
||||
>
|
||||
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
|
||||
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
|
||||
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
|
||||
</summary>
|
||||
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
|
||||
</motion.details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqSimple;
|
||||
139
src/components/sections/faq/FaqSplitMedia.tsx
Normal file
139
src/components/sections/faq/FaqSplitMedia.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
type FaqItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
type FaqSplitMediaProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FaqItem[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const FaqSplitMedia = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: FaqSplitMediaProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FaqSplitMedia"
|
||||
aria-label="FAQ section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-5">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card relative md:col-span-2 h-80 md:h-auto rounded-theme overflow-hidden"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="FAQ video"
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<div className="md:col-span-3 flex flex-col gap-3">
|
||||
{items.map((item, index) => (
|
||||
<motion.details
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="card rounded-theme p-4 group"
|
||||
>
|
||||
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
|
||||
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
|
||||
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
|
||||
</summary>
|
||||
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
|
||||
</motion.details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqSplitMedia;
|
||||
107
src/components/sections/faq/FaqTwoColumn.tsx
Normal file
107
src/components/sections/faq/FaqTwoColumn.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
type FaqItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
const FaqTwoColumn = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FaqItem[];
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FaqTwoColumn"
|
||||
aria-label="FAQ section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-5">
|
||||
{items.map((item, index) => (
|
||||
<motion.details
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05 + index * 0.05, ease: "easeOut" }}
|
||||
className="card rounded-theme p-4 group"
|
||||
>
|
||||
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
|
||||
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
|
||||
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
|
||||
</summary>
|
||||
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
|
||||
</motion.details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqTwoColumn;
|
||||
159
src/components/sections/features/FeaturesAlternatingSplit.tsx
Normal file
159
src/components/sections/features/FeaturesAlternatingSplit.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesAlternatingSplitProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesAlternatingSplit = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesAlternatingSplitProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesAlternatingSplit"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className={`flex flex-col gap-5 md:gap-8 p-5 md:p-8 card rounded-theme ${index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse"}`}
|
||||
>
|
||||
<div className="flex flex-col justify-center w-full md:w-1/2 gap-3">
|
||||
<span className="flex items-center justify-center size-8 mb-1 text-sm rounded-theme primary-button text-primary-cta-text">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h3 className="text-4xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
|
||||
<p className="text-base leading-tight text-balance">{item.description}</p>
|
||||
{(item.primaryButton || item.secondaryButton) && (
|
||||
<div className="flex flex-wrap gap-3 mt-2">
|
||||
{item.primaryButton && (
|
||||
<a
|
||||
href={item.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{item.primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{item.secondaryButton && (
|
||||
<a
|
||||
href={item.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{item.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 aspect-square rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesAlternatingSplit;
|
||||
142
src/components/sections/features/FeaturesArrowCards.tsx
Normal file
142
src/components/sections/features/FeaturesArrowCards.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
tags: string[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesArrowCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesArrowCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesArrowCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesArrowCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col gap-5 h-full cursor-pointer group"
|
||||
>
|
||||
<div className="aspect-square rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-3 p-4 md:p-5 flex-1 card rounded-theme">
|
||||
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
|
||||
<div className="flex items-center justify-between gap-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{item.tags.map((itemTag) => (
|
||||
<span key={itemTag} className="card rounded-full px-3 py-1 text-sm">{itemTag}</span>
|
||||
))}
|
||||
</div>
|
||||
<ArrowRight className="shrink-0 h-4 w-auto transition-transform duration-300 group-hover:-rotate-45" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesArrowCards;
|
||||
306
src/components/sections/features/FeaturesBento.tsx
Normal file
306
src/components/sections/features/FeaturesBento.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
type FeatureCard = { title: string; description: string } & (
|
||||
| { bentoComponent: "info-card-marquee"; infoCards: { icon: LucideIcon; label: string; value: string }[] }
|
||||
| { bentoComponent: "tilted-stack-cards"; stackCards: [{ icon: LucideIcon; title: string; subtitle: string; detail: string }, { icon: LucideIcon; title: string; subtitle: string; detail: string }, { icon: LucideIcon; title: string; subtitle: string; detail: string }] }
|
||||
| { bentoComponent: "animated-bar-chart" }
|
||||
| { bentoComponent: "orbiting-icons"; centerIcon: LucideIcon; orbitIcons: LucideIcon[] }
|
||||
| { bentoComponent: "icon-text-marquee"; centerIcon: LucideIcon; marqueeTexts: string[] }
|
||||
| { bentoComponent: "chat-marquee"; aiIcon: LucideIcon; userIcon: LucideIcon; exchanges: { userMessage: string; aiResponse: string }[]; placeholder: string }
|
||||
| { bentoComponent: "checklist-timeline"; heading: string; subheading: string; checklistItems: [{ label: string; detail: string }, { label: string; detail: string }, { label: string; detail: string }]; completedLabel: string }
|
||||
| { bentoComponent: "media-stack"; mediaItems: [{ imageSrc?: string; videoSrc?: string }, { imageSrc?: string; videoSrc?: string }, { imageSrc?: string; videoSrc?: string }] }
|
||||
);
|
||||
|
||||
interface FeaturesBentoProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
features: FeatureCard[];
|
||||
}
|
||||
|
||||
const FeaturesBento = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
features,
|
||||
}: FeaturesBentoProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesBento"
|
||||
aria-label="Features bento section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 auto-rows-fr gap-5 w-content-width mx-auto">
|
||||
{features.map((feature, index) => {
|
||||
const spanClass =
|
||||
index % 5 === 0 ? "md:col-span-4" :
|
||||
index % 5 === 1 ? "md:col-span-2" :
|
||||
index % 5 === 2 ? "md:col-span-3" :
|
||||
index % 5 === 3 ? "md:col-span-3" :
|
||||
"md:col-span-6";
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className={`flex flex-col gap-4 p-4 md:p-5 card rounded-theme h-full ${spanClass}`}
|
||||
>
|
||||
<div className="relative h-72 overflow-hidden rounded-theme">
|
||||
{feature.bentoComponent === "info-card-marquee" && (
|
||||
<div className="absolute inset-0 flex flex-col gap-2 p-4 overflow-hidden">
|
||||
<Marquee speed={30}>
|
||||
{feature.infoCards.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<div key={c.label} className="flex items-center gap-3 card rounded-theme px-4 py-3">
|
||||
<Icon className="size-5 shrink-0" strokeWidth={1.5} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">{c.label}</span>
|
||||
<span className="text-base font-medium">{c.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Marquee>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "tilted-stack-cards" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{feature.stackCards.map((card, i) => {
|
||||
const Icon = card.icon;
|
||||
const rotate = (i - 1) * 6;
|
||||
const translate = (i - 1) * 8;
|
||||
return (
|
||||
<div
|
||||
key={card.title}
|
||||
style={{ transform: `rotate(${rotate}deg) translateX(${translate}px)`, zIndex: i }}
|
||||
className="absolute w-48 card rounded-theme p-4 flex flex-col gap-2"
|
||||
>
|
||||
<Icon className="size-5" strokeWidth={1.5} />
|
||||
<h4 className="text-sm font-medium leading-tight">{card.title}</h4>
|
||||
<p className="text-xs leading-tight">{card.subtitle}</p>
|
||||
<p className="text-2xs text-muted-foreground">{card.detail}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "animated-bar-chart" && (
|
||||
<div className="absolute inset-0 flex items-end justify-around gap-2 p-4">
|
||||
{[40, 65, 85, 55, 75, 95, 60].map((h, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ height: 0 }}
|
||||
whileInView={{ height: `${h}%` }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: i * 0.1, ease: "easeOut" }}
|
||||
className="w-full bg-foreground/80 rounded-theme"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "orbiting-icons" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative size-48">
|
||||
<div className="absolute inset-0 rounded-full border border-foreground/10" />
|
||||
<div className="absolute inset-4 rounded-full border border-foreground/10" />
|
||||
<div className="absolute inset-1/2 -translate-x-1/2 -translate-y-1/2 size-12 card rounded-full flex items-center justify-center">
|
||||
<feature.centerIcon className="size-5" strokeWidth={1.5} />
|
||||
</div>
|
||||
{feature.orbitIcons.slice(0, 6).map((Icon, i) => {
|
||||
const angle = (i / Math.min(feature.orbitIcons.length, 6)) * Math.PI * 2;
|
||||
const r = 80;
|
||||
const x = Math.cos(angle) * r;
|
||||
const y = Math.sin(angle) * r;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{ left: `calc(50% + ${x}px)`, top: `calc(50% + ${y}px)` }}
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2 size-8 card rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Icon className="size-4" strokeWidth={1.5} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "icon-text-marquee" && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3">
|
||||
<div className="size-12 card rounded-full flex items-center justify-center">
|
||||
<feature.centerIcon className="size-5" strokeWidth={1.5} />
|
||||
</div>
|
||||
<Marquee speed={25} className="w-full">
|
||||
{feature.marqueeTexts.map((t, i) => (
|
||||
<span key={i} className="card rounded-full px-4 py-2 text-sm whitespace-nowrap">{t}</span>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "chat-marquee" && (
|
||||
<div className="absolute inset-0 flex flex-col gap-2 p-4 overflow-hidden">
|
||||
{feature.exchanges.slice(0, 3).map((ex, i) => {
|
||||
const UserIcon = feature.userIcon;
|
||||
const AiIcon = feature.aiIcon;
|
||||
return (
|
||||
<div key={i} className="flex flex-col gap-2">
|
||||
<div className="flex items-start gap-2 self-end max-w-7/10">
|
||||
<span className="card rounded-theme px-3 py-2 text-xs">{ex.userMessage}</span>
|
||||
<UserIcon className="size-5 shrink-0" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex items-start gap-2 self-start max-w-7/10">
|
||||
<AiIcon className="size-5 shrink-0" strokeWidth={1.5} />
|
||||
<span className="card rounded-theme px-3 py-2 text-xs">{ex.aiResponse}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-auto card rounded-theme px-3 py-2 text-xs text-muted-foreground">{feature.placeholder}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "checklist-timeline" && (
|
||||
<div className="absolute inset-0 flex flex-col gap-3 p-4">
|
||||
<h4 className="text-sm font-medium">{feature.heading}</h4>
|
||||
<p className="text-xs text-muted-foreground">{feature.subheading}</p>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{feature.checklistItems.map((it) => (
|
||||
<div key={it.label} className="flex items-start gap-2">
|
||||
<div className="size-5 rounded-full primary-button flex items-center justify-center shrink-0">
|
||||
<Check className="size-3 text-primary-cta-text" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">{it.label}</span>
|
||||
<span className="text-2xs text-muted-foreground">{it.detail}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="mt-auto text-xs font-medium">{feature.completedLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feature.bentoComponent === "media-stack" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{feature.mediaItems.map((m, i) => {
|
||||
const rotate = (i - 1) * 6;
|
||||
const translate = (i - 1) * 12;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{ transform: `rotate(${rotate}deg) translateX(${translate}px)`, zIndex: i }}
|
||||
className="absolute w-40 aspect-square card rounded-theme overflow-hidden p-2"
|
||||
>
|
||||
{m.videoSrc ? (
|
||||
<video
|
||||
src={m.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={m.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
|
||||
<p className="text-sm leading-tight">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesBento;
|
||||
123
src/components/sections/features/FeaturesBorderGlow.tsx
Normal file
123
src/components/sections/features/FeaturesBorderGlow.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { motion } from "motion/react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
type FeatureItem = {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
interface FeaturesBorderGlowProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
features: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesBorderGlow = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
features,
|
||||
}: FeaturesBorderGlowProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesBorderGlow"
|
||||
aria-label="Features border glow section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col w-content-width mx-auto gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{features.map((feature, index) => {
|
||||
const FeatureIcon = feature.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="relative flex flex-col justify-between gap-4 p-4 md:p-5 h-full min-h-60 card rounded-theme"
|
||||
>
|
||||
<div className="flex items-center justify-center size-12 primary-button rounded-theme">
|
||||
<FeatureIcon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
|
||||
<p className="text-sm leading-tight">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesBorderGlow;
|
||||
121
src/components/sections/features/FeaturesComparison.tsx
Normal file
121
src/components/sections/features/FeaturesComparison.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check, X } from "lucide-react";
|
||||
|
||||
interface FeaturesComparisonProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
negativeItems: string[];
|
||||
positiveItems: string[];
|
||||
}
|
||||
|
||||
const FeaturesComparison = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
negativeItems,
|
||||
positiveItems,
|
||||
}: FeaturesComparisonProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesComparison"
|
||||
aria-label="Features comparison section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-content-width md:w-6/10 mx-auto gap-5"
|
||||
>
|
||||
<div className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme opacity-50">
|
||||
{negativeItems.map((item) => (
|
||||
<div key={item} className="flex items-center gap-2 text-base">
|
||||
<X className="shrink-0 size-4" />
|
||||
<span className="text-base truncate">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme">
|
||||
{positiveItems.map((item) => (
|
||||
<div key={item} className="flex items-center gap-2 text-base">
|
||||
<Check className="shrink-0 size-4" />
|
||||
<span className="text-base truncate">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesComparison;
|
||||
147
src/components/sections/features/FeaturesDetailedCards.tsx
Normal file
147
src/components/sections/features/FeaturesDetailedCards.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesDetailedCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesDetailedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesDetailedCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesDetailedCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-content-width mx-auto gap-5">
|
||||
{items.map((item) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex flex-col md:grid md:grid-cols-10 2xl:w-8/10 mx-auto gap-5 md:gap-8 p-5 md:p-8 cursor-pointer card rounded-theme group"
|
||||
>
|
||||
<div className="flex flex-col md:col-span-6 gap-3 md:gap-8">
|
||||
<h3 className="text-3xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
|
||||
|
||||
<div className="flex flex-col mt-auto gap-5">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map((itemTag) => (
|
||||
<span key={itemTag} className="card rounded-full px-3 py-1 text-sm">{itemTag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-base md:text-2xl leading-tight text-balance">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aspect-square md:col-span-4 rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesDetailedCards;
|
||||
150
src/components/sections/features/FeaturesDetailedSteps.tsx
Normal file
150
src/components/sections/features/FeaturesDetailedSteps.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type StepItem = {
|
||||
tag: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesDetailedStepsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
steps: StepItem[];
|
||||
}
|
||||
|
||||
const FeaturesDetailedSteps = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
steps,
|
||||
}: FeaturesDetailedStepsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesDetailedSteps"
|
||||
aria-label="Features detailed steps section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-content-width mx-auto gap-5">
|
||||
{steps.map((step, index) => {
|
||||
const stepNumber = String(index + 1).padStart(2, "0");
|
||||
const tilt = index % 2 === 0 ? "rotate-3" : "-rotate-3";
|
||||
return (
|
||||
<motion.div
|
||||
key={step.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row justify-between 2xl:w-8/10 mx-auto gap-5 md:gap-8 p-5 md:p-8 card rounded-theme overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col justify-between w-full md:w-1/2">
|
||||
<div className="flex flex-col gap-5">
|
||||
<span className="w-fit card rounded-full px-3 py-1 mb-1 text-sm">{step.tag}</span>
|
||||
<h3 className="text-5xl md:text-8xl font-medium leading-none">{step.title}</h3>
|
||||
</div>
|
||||
<div className="block md:hidden w-full h-px my-5 bg-foreground/20" />
|
||||
<div className="flex flex-col gap-2 md:gap-5">
|
||||
<h4 className="text-2xl md:text-3xl font-medium text-balance">{step.subtitle}</h4>
|
||||
<p className="text-base md:text-lg leading-tight text-balance">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full md:w-35/100 gap-8">
|
||||
<span className="hidden md:block self-end text-8xl font-medium">{stepNumber}</span>
|
||||
<div className={`aspect-square rounded-theme overflow-hidden ${tilt}`}>
|
||||
{step.videoSrc ? (
|
||||
<video
|
||||
src={step.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Step video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={step.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesDetailedSteps;
|
||||
142
src/components/sections/features/FeaturesFlipCards.tsx
Normal file
142
src/components/sections/features/FeaturesFlipCards.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
descriptions: string[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesFlipCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesFlipCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesFlipCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesFlipCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="group flex flex-col gap-4 p-4 md:p-5 card rounded-theme"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-5">
|
||||
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
|
||||
<div className="flex items-center justify-center shrink-0 size-8 primary-button rounded-theme transition-transform duration-300 group-hover:rotate-45">
|
||||
<Plus className="size-4 text-primary-cta-text" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative overflow-hidden aspect-4/5 rounded-theme">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{item.descriptions.map((desc, i) => (
|
||||
<p key={i} className="text-base leading-tight">{desc}</p>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesFlipCards;
|
||||
126
src/components/sections/features/FeaturesIconCards.tsx
Normal file
126
src/components/sections/features/FeaturesIconCards.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { motion } from "motion/react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import HoverPattern from "@/components/ui/hover-pattern";
|
||||
|
||||
type FeatureItem = {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
interface FeaturesIconCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
features: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesIconCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
features,
|
||||
}: FeaturesIconCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesIconCards"
|
||||
aria-label="Features icon cards section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col w-content-width mx-auto gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{features.map((feature, index) => {
|
||||
const FeatureIcon = feature.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme"
|
||||
>
|
||||
<HoverPattern className="flex items-center justify-center aspect-square rounded-theme">
|
||||
<div className="relative z-10 flex items-center justify-center size-12 primary-button rounded-theme">
|
||||
<FeatureIcon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
</HoverPattern>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
|
||||
<p className="text-sm leading-tight">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesIconCards;
|
||||
147
src/components/sections/features/FeaturesLabeledList.tsx
Normal file
147
src/components/sections/features/FeaturesLabeledList.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
label: string;
|
||||
title: string;
|
||||
bullets: string[];
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
};
|
||||
|
||||
interface FeaturesLabeledListProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesLabeledList = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesLabeledListProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesLabeledList"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 w-content-width mx-auto">
|
||||
{items.map((item) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row gap-5 md:gap-8 p-5 md:p-8 card rounded-theme"
|
||||
>
|
||||
<div className="w-full md:w-1/2 flex md:justify-start">
|
||||
<h3 className="text-7xl font-medium leading-tight truncate">{item.label}</h3>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20 md:hidden" />
|
||||
|
||||
<div className="flex flex-col w-full md:w-1/2 gap-5">
|
||||
<h4 className="text-2xl md:text-3xl font-medium leading-tight">{item.title}</h4>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{item.bullets.map((bulletText, i) => (
|
||||
<span key={i} className="flex items-center gap-1">
|
||||
<span className="text-base">{bulletText}</span>
|
||||
{i < item.bullets.length - 1 && <span className="text-base">•</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mt-2">
|
||||
<a
|
||||
href={item.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{item.primaryButton.text}
|
||||
</a>
|
||||
<a
|
||||
href={item.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{item.secondaryButton.text}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesLabeledList;
|
||||
134
src/components/sections/features/FeaturesMediaCards.tsx
Normal file
134
src/components/sections/features/FeaturesMediaCards.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesMediaCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesMediaCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesMediaCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col gap-4 p-4 md:p-5 h-full card rounded-theme"
|
||||
>
|
||||
<div className="aspect-square rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
|
||||
<p className="text-sm leading-tight">{item.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesMediaCards;
|
||||
143
src/components/sections/features/FeaturesMediaCarousel.tsx
Normal file
143
src/components/sections/features/FeaturesMediaCarousel.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { motion } from "motion/react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
buttonIcon: LucideIcon;
|
||||
buttonHref?: string;
|
||||
buttonOnClick?: () => void;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesMediaCarouselProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesMediaCarousel = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesMediaCarouselProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesMediaCarousel"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col w-full gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Marquee speed={50} pauseOnHover>
|
||||
{items.map((item) => {
|
||||
const Icon = item.buttonIcon;
|
||||
return (
|
||||
<div key={item.title} className="relative overflow-hidden w-90 aspect-square md:aspect-3/2 rounded-theme shrink-0">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-foreground/60 to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-5 p-5">
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h3 className="text-2xl md:text-3xl font-medium leading-tight text-background">{item.title}</h3>
|
||||
<p className="text-sm md:text-base leading-tight text-background/75">{item.description}</p>
|
||||
</div>
|
||||
<a
|
||||
href={item.buttonHref ?? "#"}
|
||||
aria-label={item.buttonHref ? `Navigate to ${item.buttonHref}` : "Action button"}
|
||||
className="flex items-center justify-center shrink-0 size-8 primary-button rounded-theme"
|
||||
>
|
||||
<Icon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Marquee>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesMediaCarousel;
|
||||
163
src/components/sections/features/FeaturesProfileCards.tsx
Normal file
163
src/components/sections/features/FeaturesProfileCards.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { motion } from "motion/react";
|
||||
import { BadgeCheck } from "lucide-react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
avatarSrc: string;
|
||||
buttonText: string;
|
||||
buttonHref?: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesProfileCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesProfileCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesProfileCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesProfileCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="group relative overflow-hidden aspect-5/6 rounded-theme"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Profile video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<a
|
||||
href={item.buttonHref ?? "#"}
|
||||
className="absolute top-5 right-5 z-20 primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{item.buttonText}
|
||||
</a>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-linear-to-t from-foreground/80 to-transparent" />
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 p-3">
|
||||
<div className="relative flex flex-col gap-1 p-3">
|
||||
<div className="absolute inset-0 -z-10 card rounded-theme translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 shrink-0 overflow-hidden rounded-full secondary-button">
|
||||
<img src={item.avatarSrc} alt="" className="h-full w-full object-cover" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold leading-tight truncate text-background transition-colors duration-400 group-hover:text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<BadgeCheck className="size-5 shrink-0 text-background transition-colors duration-400 group-hover:text-foreground" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
|
||||
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesProfileCards;
|
||||
153
src/components/sections/features/FeaturesRevealCards.tsx
Normal file
153
src/components/sections/features/FeaturesRevealCards.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesRevealCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesRevealCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesRevealCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesRevealCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="group relative overflow-hidden aspect-6/7 rounded-theme"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute top-5 left-5 z-20">
|
||||
<div className="relative size-8 rounded-full bg-background flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-foreground transition-opacity duration-300 group-hover:opacity-0">{index + 1}</span>
|
||||
<Info className="absolute size-4 text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-linear-to-t from-foreground/80 to-transparent" />
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 p-3">
|
||||
<div className="relative flex flex-col gap-1 p-3">
|
||||
<div className="absolute inset-0 -z-10 card rounded-theme translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
|
||||
|
||||
<h3 className="text-2xl font-semibold leading-tight text-background transition-colors duration-400 group-hover:text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
|
||||
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesRevealCards;
|
||||
127
src/components/sections/features/FeaturesStatisticsCards.tsx
Normal file
127
src/components/sections/features/FeaturesStatisticsCards.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface FeaturesStatisticsCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesStatisticsCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesStatisticsCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesStatisticsCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col h-full card rounded-theme"
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-8 p-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-2xl md:text-3xl font-medium leading-tight truncate">{item.title}</h3>
|
||||
<p className="text-base md:text-lg leading-tight">{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 mt-auto">
|
||||
<div className="flex items-center min-w-0 flex-1 gap-2">
|
||||
<span className="shrink-0 size-3 rounded-full bg-foreground" />
|
||||
<span className="text-base truncate">{item.label}</span>
|
||||
</div>
|
||||
<span className="text-xl md:text-2xl font-medium">{item.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesStatisticsCards;
|
||||
158
src/components/sections/features/FeaturesTaggedCards.tsx
Normal file
158
src/components/sections/features/FeaturesTaggedCards.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
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 });
|
||||
|
||||
interface 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) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesTaggedCards"
|
||||
aria-label="Features section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col gap-5 h-full group"
|
||||
>
|
||||
<div className="relative aspect-square rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Feature video"
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
<span className="absolute top-5 right-5 card rounded-full px-3 py-1 text-sm">{item.tag}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 md:p-5 flex-1 card rounded-theme">
|
||||
<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 && (
|
||||
<a
|
||||
href={item.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{item.primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{item.secondaryButton && (
|
||||
<a
|
||||
href={item.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{item.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesTaggedCards;
|
||||
141
src/components/sections/features/FeaturesTimelineCards.tsx
Normal file
141
src/components/sections/features/FeaturesTimelineCards.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type FeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
interface FeaturesTimelineCardsProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const FeaturesTimelineCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: FeaturesTimelineCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="FeaturesTimelineCards"
|
||||
aria-label="Features timeline section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col w-content-width mx-auto gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col gap-8">
|
||||
<div className="absolute left-4 top-2 bottom-2 w-px bg-foreground/20" aria-hidden="true" />
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
|
||||
className="relative flex gap-5 pl-8"
|
||||
>
|
||||
<div className="absolute left-2 top-2 size-5 rounded-full primary-button flex items-center justify-center">
|
||||
<span className="text-2xs font-medium text-primary-cta-text">{index + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-5 md:gap-8 flex-1 p-4 md:p-5 card rounded-theme">
|
||||
<div className="flex flex-col gap-2 w-full md:w-1/2">
|
||||
<h3 className="text-3xl font-medium leading-tight text-balance">{item.title}</h3>
|
||||
<p className="text-base leading-tight text-balance">{item.description}</p>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 aspect-video rounded-theme overflow-hidden">
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Timeline video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesTimelineCards;
|
||||
56
src/components/sections/footer/FooterBasic.tsx
Normal file
56
src/components/sections/footer/FooterBasic.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterBasic = ({
|
||||
columns,
|
||||
leftText,
|
||||
rightText,
|
||||
}: {
|
||||
columns: FooterColumn[];
|
||||
leftText: string;
|
||||
rightText: string;
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterBasic"
|
||||
aria-label="Site footer"
|
||||
className="w-full pt-20 pb-8 border-t border-foreground/15"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="w-full flex flex-wrap justify-between gap-y-8 mb-8">
|
||||
{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>
|
||||
{column.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-base hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20" />
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-5">
|
||||
<span className="text-sm opacity-50">{leftText}</span>
|
||||
<span className="text-sm opacity-50">{rightText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBasic;
|
||||
51
src/components/sections/footer/FooterBrand.tsx
Normal file
51
src/components/sections/footer/FooterBrand.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterBrand = ({
|
||||
brand,
|
||||
columns,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterBrand"
|
||||
aria-label="Site footer"
|
||||
className="w-full py-8 mt-20 overflow-hidden primary-button text-primary-cta-text"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8 md:gap-8">
|
||||
<h2 className="text-7xl md:text-9xl font-medium leading-none">{brand}</h2>
|
||||
|
||||
<div className="flex flex-col gap-8 mb-8 md:flex-row md:justify-between">
|
||||
{columns.map((column, index) => (
|
||||
<div key={index} className="flex flex-col items-start gap-3">
|
||||
{column.items.map((item) => (
|
||||
<div key={item.label} className="flex items-center gap-2 text-base">
|
||||
<ChevronRight className="w-4 h-4" strokeWidth={3} aria-hidden="true" />
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-base text-primary-cta-text font-medium hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBrand;
|
||||
51
src/components/sections/footer/FooterBrandReveal.tsx
Normal file
51
src/components/sections/footer/FooterBrandReveal.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterBrandReveal = ({
|
||||
brand,
|
||||
columns,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterBrandReveal"
|
||||
aria-label="Site footer"
|
||||
className="w-full py-8 mt-20 overflow-hidden primary-button text-primary-cta-text"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<h2 className="text-7xl md:text-9xl font-medium leading-none">{brand}</h2>
|
||||
|
||||
<div className="flex flex-col gap-8 mb-8 md:flex-row md:justify-between">
|
||||
{columns.map((column, index) => (
|
||||
<div key={index} className="flex flex-col items-start gap-3">
|
||||
{column.items.map((item) => (
|
||||
<div key={item.label} className="flex items-center gap-2 text-base">
|
||||
<ChevronRight className="w-4 h-4" strokeWidth={3} aria-hidden="true" />
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-base text-primary-cta-text font-medium hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBrandReveal;
|
||||
53
src/components/sections/footer/FooterMinimal.tsx
Normal file
53
src/components/sections/footer/FooterMinimal.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
type SocialLink = {
|
||||
icon: string | LucideIcon;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const FooterMinimal = ({
|
||||
brand,
|
||||
copyright,
|
||||
socialLinks,
|
||||
}: {
|
||||
brand: string;
|
||||
copyright: string;
|
||||
socialLinks?: SocialLink[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterMinimal"
|
||||
aria-label="Site footer"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col w-content-width mx-auto px-8 pb-5 rounded-theme card">
|
||||
<h2 className="text-7xl md:text-9xl font-medium leading-none py-5">{brand}</h2>
|
||||
|
||||
<div className="h-px w-full mb-5 bg-foreground/50" />
|
||||
|
||||
<div className="flex flex-col gap-3 items-center justify-between md:flex-row">
|
||||
<span className="text-base opacity-75">{copyright}</span>
|
||||
{socialLinks && socialLinks.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
{socialLinks.map((link, index) => {
|
||||
const Icon = typeof link.icon === "string" ? null : link.icon;
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={link.href}
|
||||
className="flex items-center justify-center size-8 rounded-full primary-button text-primary-cta-text"
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4" strokeWidth={1.5} />}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterMinimal;
|
||||
70
src/components/sections/footer/FooterSimple.tsx
Normal file
70
src/components/sections/footer/FooterSimple.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
items: { label: string; href?: string; onClick?: () => void }[];
|
||||
};
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const FooterSimple = ({
|
||||
brand,
|
||||
columns,
|
||||
copyright,
|
||||
links,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
copyright: string;
|
||||
links: FooterLink[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterSimple"
|
||||
aria-label="Site footer"
|
||||
className="w-full py-8 mt-20 primary-button text-primary-cta-text"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
|
||||
<h2 className="text-4xl font-medium">{brand}</h2>
|
||||
|
||||
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
|
||||
{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>
|
||||
{column.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
|
||||
<span className="text-sm opacity-50">{copyright}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSimple;
|
||||
70
src/components/sections/footer/FooterSimpleCard.tsx
Normal file
70
src/components/sections/footer/FooterSimpleCard.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterSimpleCard = ({
|
||||
brand,
|
||||
columns,
|
||||
copyright,
|
||||
links,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
copyright: string;
|
||||
links: FooterLink[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterSimpleCard"
|
||||
aria-label="Site footer"
|
||||
className="w-full py-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto p-8 rounded-theme card">
|
||||
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
|
||||
<h2 className="text-4xl font-medium">{brand}</h2>
|
||||
|
||||
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
|
||||
{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>
|
||||
{column.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-base hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-8 border-t border-foreground/20">
|
||||
<span className="text-sm opacity-50">{copyright}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSimpleCard;
|
||||
96
src/components/sections/footer/FooterSimpleMedia.tsx
Normal file
96
src/components/sections/footer/FooterSimpleMedia.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
type FooterSimpleMediaProps = ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }) & {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
copyright: string;
|
||||
links: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterSimpleMedia = ({
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
brand,
|
||||
columns,
|
||||
copyright,
|
||||
links,
|
||||
}: FooterSimpleMediaProps) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterSimpleMedia"
|
||||
aria-label="Site footer"
|
||||
className="relative w-full mt-20 overflow-hidden"
|
||||
>
|
||||
<div className="w-full aspect-square md:aspect-16/6 mask-fade-top-long">
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Footer video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full py-8 primary-button text-primary-cta-text">
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
|
||||
<h2 className="text-4xl font-medium">{brand}</h2>
|
||||
|
||||
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
|
||||
{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>
|
||||
{column.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
|
||||
<span className="text-sm opacity-50">{copyright}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSimpleMedia;
|
||||
70
src/components/sections/footer/FooterSimpleReveal.tsx
Normal file
70
src/components/sections/footer/FooterSimpleReveal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
type FooterColumn = {
|
||||
title: string;
|
||||
items: { label: string; href?: string; onClick?: () => void }[];
|
||||
};
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const FooterSimpleReveal = ({
|
||||
brand,
|
||||
columns,
|
||||
copyright,
|
||||
links,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
copyright: string;
|
||||
links: FooterLink[];
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
data-webild-section="FooterSimpleReveal"
|
||||
aria-label="Site footer"
|
||||
className="w-full py-8 mt-20 primary-button text-primary-cta-text"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
|
||||
<h2 className="text-4xl font-medium">{brand}</h2>
|
||||
|
||||
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
|
||||
{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>
|
||||
{column.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
|
||||
<span className="text-sm opacity-50">{copyright}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSimpleReveal;
|
||||
140
src/components/sections/hero/HeroBillboard.tsx
Normal file
140
src/components/sections/hero/HeroBillboard.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBillboardProps = {
|
||||
tag?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
avatars?: { src: string }[];
|
||||
avatarsLabel?: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroBillboard = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
avatars,
|
||||
avatarsLabel,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboard"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
{avatars && avatars.length > 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex -space-x-2">
|
||||
{avatars.map((avatar, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={avatar.src}
|
||||
alt=""
|
||||
className="w-8 h-8 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{avatarsLabel ? (
|
||||
<span className="text-sm">{avatarsLabel}</span>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : tag ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
) : null}
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboard;
|
||||
100
src/components/sections/hero/HeroBillboardBrand.tsx
Normal file
100
src/components/sections/hero/HeroBillboardBrand.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBillboardBrandProps = {
|
||||
brand: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroBillboardBrand = ({
|
||||
brand,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBillboardBrandProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboardBrand"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-end gap-5">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-full text-9xl font-semibold text-balance text-right leading-none"
|
||||
>
|
||||
{brand}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="w-full md:w-1/2 text-lg md:text-2xl leading-tight text-balance text-right"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-end gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboardBrand;
|
||||
120
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
120
src/components/sections/hero/HeroBillboardCarousel.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { motion } from "motion/react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
type HeroBillboardCarouselProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
};
|
||||
|
||||
const HeroBillboardCarousel = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: HeroBillboardCarouselProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboardCarousel"
|
||||
aria-label="Hero section"
|
||||
className="relative flex flex-col items-center justify-center gap-8 w-full min-h-svh py-25"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="w-full mask-fade-x"
|
||||
>
|
||||
<Marquee speed={60} pauseOnHover={false}>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="shrink-0 w-60 md:w-80 aspect-4/5 card rounded-theme overflow-hidden p-2"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboardCarousel;
|
||||
112
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
112
src/components/sections/hero/HeroBillboardScroll.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBillboardScrollProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroBillboardScroll = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBillboardScrollProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboardScroll"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, rotateX: 15 }}
|
||||
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
|
||||
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video mt-8"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboardScroll;
|
||||
163
src/components/sections/hero/HeroBillboardTestimonial.tsx
Normal file
163
src/components/sections/hero/HeroBillboardTestimonial.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
handle: string;
|
||||
text: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type HeroBillboardTestimonialProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroBillboardTestimonial = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
testimonials,
|
||||
}: HeroBillboardTestimonialProps) => {
|
||||
const testimonial = testimonials[0];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboardTestimonial"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="relative card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-3/4 md:aspect-video"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
|
||||
<figure className="absolute bottom-6 left-6 right-6 md:right-auto md:bottom-8 md:left-8 md:max-w-sm card rounded-theme p-4 flex flex-col gap-3">
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<blockquote className="text-base leading-tight text-balance">
|
||||
{testimonial.text}
|
||||
</blockquote>
|
||||
|
||||
<figcaption className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{testimonial.name}</span>
|
||||
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboardTestimonial;
|
||||
126
src/components/sections/hero/HeroBillboardTiltedCarousel.tsx
Normal file
126
src/components/sections/hero/HeroBillboardTiltedCarousel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBillboardTiltedCarouselProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
};
|
||||
|
||||
const HeroBillboardTiltedCarousel = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: HeroBillboardTiltedCarouselProps) => {
|
||||
const tiltClasses = [
|
||||
"-rotate-6 -translate-y-3",
|
||||
"rotate-3 translate-y-3",
|
||||
"-rotate-3 -translate-y-2",
|
||||
"rotate-6 translate-y-4",
|
||||
"-rotate-6 -translate-y-3",
|
||||
"rotate-3 translate-y-3",
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBillboardTiltedCarousel"
|
||||
aria-label="Hero section"
|
||||
className="relative flex flex-col items-center justify-center gap-8 w-full min-h-svh py-25 overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex justify-center items-center gap-3 w-full max-w-[120vw] -mx-8"
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`shrink-0 w-50 md:w-60 aspect-4/5 card rounded-theme overflow-hidden p-2 transform ${tiltClasses[i % tiltClasses.length]}`}
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboardTiltedCarousel;
|
||||
101
src/components/sections/hero/HeroBrand.tsx
Normal file
101
src/components/sections/hero/HeroBrand.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBrandProps = {
|
||||
brand: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroBrand = ({
|
||||
brand,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBrandProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBrand"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 bottom-0 z-1 h-3/5 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-content-width mx-auto pb-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-5">
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-full md:w-1/2 text-lg md:text-2xl text-balance text-primary-cta-text leading-tight"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="font-semibold text-primary-cta-text text-9xl leading-none text-balance"
|
||||
>
|
||||
{brand}
|
||||
</motion.h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBrand;
|
||||
117
src/components/sections/hero/HeroBrandCarousel.tsx
Normal file
117
src/components/sections/hero/HeroBrandCarousel.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroBrandCarouselProps = {
|
||||
brand: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
};
|
||||
|
||||
const HeroBrandCarousel = ({
|
||||
brand,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: HeroBrandCarouselProps) => {
|
||||
const featured = items[0];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroBrandCarousel"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
|
||||
>
|
||||
{featured.videoSrc ? (
|
||||
<video
|
||||
src={featured.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={featured.imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 bottom-0 z-1 h-3/5 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-content-width mx-auto pb-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-5">
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-full md:w-1/2 text-lg md:text-2xl text-balance text-primary-cta-text leading-tight"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="font-semibold text-primary-cta-text text-9xl leading-none text-balance"
|
||||
>
|
||||
{brand}
|
||||
</motion.h1>
|
||||
|
||||
<div className="flex gap-3 pb-5">
|
||||
{items.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative h-1 w-full rounded-full overflow-hidden bg-primary-cta-text/20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-primary-cta-text rounded-full origin-left ${i === 0 ? "scale-x-100" : "scale-x-0"}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBrandCarousel;
|
||||
144
src/components/sections/hero/HeroCenteredLogos.tsx
Normal file
144
src/components/sections/hero/HeroCenteredLogos.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { motion } from "motion/react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
type HeroCenteredLogosProps = {
|
||||
avatars: { src: string }[];
|
||||
avatarText: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
logos: string[];
|
||||
hideMedia?: boolean;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroCenteredLogos = ({
|
||||
avatars,
|
||||
avatarText,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
logos,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
hideMedia = false,
|
||||
}: HeroCenteredLogosProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroCenteredLogos"
|
||||
aria-label="Hero section"
|
||||
className="relative h-svh w-full flex flex-col"
|
||||
>
|
||||
{!hideMedia && (
|
||||
<div className="absolute inset-0 z-0">
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-background/80" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative z-10 flex-1 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3 pt-8 w-content-width mx-auto text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex -space-x-2">
|
||||
{avatars.map((avatar, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={avatar.src}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm">{avatarText}</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="md:max-w-8/10 text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="relative z-10 w-content-width mx-auto pb-8 mask-fade-x"
|
||||
>
|
||||
<Marquee speed={30} pauseOnHover={false}>
|
||||
{logos.map((logo, i) => (
|
||||
<div key={i} className="shrink-0 px-4 py-2 card rounded-theme">
|
||||
<span className="text-xl font-semibold whitespace-nowrap opacity-75">
|
||||
{logo}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroCenteredLogos;
|
||||
137
src/components/sections/hero/HeroOverlay.tsx
Normal file
137
src/components/sections/hero/HeroOverlay.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroOverlayProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
avatars?: { src: string }[];
|
||||
avatarsLabel?: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroOverlay = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
avatars,
|
||||
avatarsLabel,
|
||||
}: HeroOverlayProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroOverlay"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 bottom-0 z-1 h-3/4 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-content-width mx-auto pb-8 md:pb-25">
|
||||
<div className="flex flex-col gap-3 w-full md:w-3/5 lg:w-1/2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-fit card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-primary-cta-text text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-lg md:text-xl text-primary-cta-text leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
{avatars && avatars.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.45, ease: "easeOut" }}
|
||||
className="mt-4 flex items-center gap-3"
|
||||
>
|
||||
<div className="flex -space-x-2">
|
||||
{avatars.map((avatar, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={avatar.src}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{avatarsLabel ? (
|
||||
<span className="text-sm text-primary-cta-text">{avatarsLabel}</span>
|
||||
) : null}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroOverlay;
|
||||
166
src/components/sections/hero/HeroOverlayTestimonial.tsx
Normal file
166
src/components/sections/hero/HeroOverlayTestimonial.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
handle: string;
|
||||
text: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type HeroOverlayTestimonialProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroOverlayTestimonial = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
testimonials,
|
||||
}: HeroOverlayTestimonialProps) => {
|
||||
const testimonial = testimonials[0];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroOverlayTestimonial"
|
||||
aria-label="Hero section"
|
||||
className="relative w-full h-svh overflow-hidden flex flex-col justify-start"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 top-0 z-1 h-3/4 bg-gradient-to-b from-foreground/60 via-foreground/30 to-transparent"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-content-width mx-auto pt-35">
|
||||
<div className="flex flex-col gap-3 w-full md:w-3/5 lg:w-1/2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-fit card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-primary-cta-text text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-lg md:text-xl text-primary-cta-text leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.figure
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.45, ease: "easeOut" }}
|
||||
className="absolute z-10 bottom-3 left-3 right-3 md:left-auto md:bottom-8 md:right-8 md:max-w-sm card rounded-theme p-4 flex flex-col gap-3"
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<blockquote className="text-base leading-tight text-balance">
|
||||
{testimonial.text}
|
||||
</blockquote>
|
||||
|
||||
<figcaption className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{testimonial.name}</span>
|
||||
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
|
||||
</div>
|
||||
</figcaption>
|
||||
</motion.figure>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroOverlayTestimonial;
|
||||
112
src/components/sections/hero/HeroSplit.tsx
Normal file
112
src/components/sections/hero/HeroSplit.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroSplitProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroSplit = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroSplitProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroSplit"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-10 md:gap-20 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center md:items-start gap-3 w-full md:w-1/2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full md:w-1/2 h-100 md:h-[65vh] md:max-h-[75svh]"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSplit;
|
||||
143
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
143
src/components/sections/hero/HeroSplitKpi.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type KpiItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type HeroSplitKpiProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
kpis: [KpiItem, KpiItem, KpiItem];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroSplitKpi = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
kpis,
|
||||
}: HeroSplitKpiProps) => {
|
||||
const kpiPositions = [
|
||||
"top-[5%] left-0",
|
||||
"top-[40%] right-0",
|
||||
"bottom-[5%] left-[5%]",
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroSplitKpi"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="flex flex-col items-center md:items-start gap-3">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full md:w-1/2 h-100 md:h-[65vh] md:max-h-[75svh]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="w-full h-full card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 scale-80"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{kpis.map((kpi, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.5, delay: 0.4 + i * 0.1, ease: "easeOut" }}
|
||||
className={`absolute flex flex-col items-center card rounded-theme backdrop-blur-sm p-4 ${kpiPositions[i]}`}
|
||||
>
|
||||
<p className="text-2xl md:text-4xl font-medium">{kpi.value}</p>
|
||||
<p className="text-sm md:text-base text-muted-foreground">{kpi.label}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSplitKpi;
|
||||
124
src/components/sections/hero/HeroSplitMediaGrid.tsx
Normal file
124
src/components/sections/hero/HeroSplitMediaGrid.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroSplitMediaGridProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
items: [
|
||||
{ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never },
|
||||
{ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }
|
||||
];
|
||||
};
|
||||
|
||||
const HeroSplitMediaGrid = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: HeroSplitMediaGridProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroSplitMediaGrid"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="flex flex-col items-center md:items-start gap-3">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="w-full md:w-1/2 grid grid-cols-2 gap-3"
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-80 md:h-[55vh] card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSplitMediaGrid;
|
||||
165
src/components/sections/hero/HeroSplitTestimonial.tsx
Normal file
165
src/components/sections/hero/HeroSplitTestimonial.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
handle: string;
|
||||
text: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type HeroSplitTestimonialProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const HeroSplitTestimonial = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
testimonials,
|
||||
}: HeroSplitTestimonialProps) => {
|
||||
const testimonial = testimonials[0];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroSplitTestimonial"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="flex flex-col items-center md:items-start gap-3">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="relative w-full md:w-1/2 aspect-3/4 md:aspect-auto md:h-[65vh] md:max-h-[75svh] card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5"
|
||||
>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
|
||||
<figure className="absolute bottom-6 left-6 right-6 md:left-auto md:max-w-1/2 card rounded-theme p-4 flex flex-col gap-3">
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<blockquote className="text-base leading-tight text-balance">
|
||||
{testimonial.text}
|
||||
</blockquote>
|
||||
|
||||
<figcaption className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{testimonial.name}</span>
|
||||
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSplitTestimonial;
|
||||
156
src/components/sections/hero/HeroSplitVerticalMarquee.tsx
Normal file
156
src/components/sections/hero/HeroSplitVerticalMarquee.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroSplitVerticalMarqueeProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
leftItems: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
rightItems: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
};
|
||||
|
||||
const HeroSplitVerticalMarquee = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
leftItems,
|
||||
rightItems,
|
||||
}: HeroSplitVerticalMarqueeProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroSplitVerticalMarquee"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="flex flex-col items-center md:items-start gap-3">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="w-full md:w-1/2 h-100 md:h-[75vh] flex gap-3 overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 overflow-hidden mask-fade-y">
|
||||
<div className="flex flex-col gap-3 animate-marquee-vertical">
|
||||
{[...leftItems, ...leftItems, ...leftItems, ...leftItems].map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="shrink-0 aspect-square card rounded-theme overflow-hidden p-2 xl:p-3 2xl:p-4"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden mask-fade-y">
|
||||
<div className="flex flex-col gap-3 animate-marquee-vertical-reverse">
|
||||
{[...rightItems, ...rightItems, ...rightItems, ...rightItems].map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="shrink-0 aspect-square card rounded-theme overflow-hidden p-2 xl:p-3 2xl:p-4"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSplitVerticalMarquee;
|
||||
162
src/components/sections/hero/HeroTiltedCards.tsx
Normal file
162
src/components/sections/hero/HeroTiltedCards.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type HeroTiltedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton: { text: string; href: string };
|
||||
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
|
||||
};
|
||||
|
||||
const HeroTiltedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
items,
|
||||
}: HeroTiltedCardsProps) => {
|
||||
const galleryStyles = [
|
||||
"-rotate-6 z-10 -translate-y-5",
|
||||
"rotate-6 z-20 translate-y-5 -ml-15",
|
||||
"-rotate-6 z-30 -translate-y-5 -ml-15",
|
||||
"rotate-6 z-40 translate-y-5 -ml-15",
|
||||
"-rotate-6 z-50 -translate-y-5 -ml-15",
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="HeroTiltedCards"
|
||||
aria-label="Hero section"
|
||||
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-8 w-full md:w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="hidden md:flex justify-center items-center w-full"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`relative w-1/4 aspect-4/5 card rounded-theme overflow-hidden p-2 transition-transform duration-500 ease-out hover:scale-110 ${galleryStyles[i % galleryStyles.length]}`}
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="md:hidden grid grid-cols-2 gap-3 w-content-width mx-auto"
|
||||
>
|
||||
{items.slice(0, 4).map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-4/5 card rounded-theme overflow-hidden p-2"
|
||||
>
|
||||
{item.videoSrc ? (
|
||||
<video
|
||||
src={item.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Hero media"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroTiltedCards;
|
||||
100
src/components/sections/legal/PolicyContent.tsx
Normal file
100
src/components/sections/legal/PolicyContent.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type ContentItem =
|
||||
| { type: "paragraph"; text: string }
|
||||
| { type: "list"; items: string[] }
|
||||
| { type: "numbered-list"; items: string[] };
|
||||
|
||||
type ContentSection = {
|
||||
heading: string;
|
||||
content: ContentItem[];
|
||||
};
|
||||
|
||||
const PolicyContent = ({
|
||||
title,
|
||||
subtitle,
|
||||
sections,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
sections: ContentSection[];
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PolicyContent"
|
||||
aria-label="Policy content"
|
||||
className="relative w-full pt-40 pb-20"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<div className="md:max-w-1/2 mx-auto flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="text-3xl md:text-4xl font-medium leading-tight text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
{subtitle && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||
className="text-sm opacity-50"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20" />
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
{sections.map((policySection, sectionIndex) => (
|
||||
<motion.div
|
||||
key={policySection.heading}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05 + sectionIndex * 0.05, ease: "easeOut" }}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<h2 className="text-xl md:text-2xl font-medium leading-tight">{policySection.heading}</h2>
|
||||
{policySection.content.map((item, i) => {
|
||||
if (item.type === "paragraph") {
|
||||
return (
|
||||
<p key={i} className="text-sm md:text-base opacity-75 leading-relaxed">
|
||||
{item.text}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (item.type === "numbered-list") {
|
||||
return (
|
||||
<ol key={i} className="flex flex-col gap-3 pl-5 text-sm md:text-base opacity-75 leading-relaxed list-decimal">
|
||||
{item.items.map((li, j) => (
|
||||
<li key={j}>{li}</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul key={i} className="flex flex-col gap-3 pl-5 text-sm md:text-base opacity-75 leading-relaxed list-disc">
|
||||
{item.items.map((li, j) => (
|
||||
<li key={j}>{li}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyContent;
|
||||
122
src/components/sections/metrics/MetricsFeatureCards.tsx
Normal file
122
src/components/sections/metrics/MetricsFeatureCards.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
title: string;
|
||||
features: string[];
|
||||
};
|
||||
|
||||
type MetricsFeatureCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsFeatureCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: MetricsFeatureCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsFeatureCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="flex flex-col justify-between gap-5 p-5 h-full card rounded-theme"
|
||||
>
|
||||
<div className="flex flex-col gap-0">
|
||||
<span className="text-7xl font-medium leading-none truncate">{metric.value}</span>
|
||||
<span className="text-xl truncate">{metric.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 pt-5 border-t border-accent">
|
||||
{metric.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-sm leading-tight">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsFeatureCards;
|
||||
129
src/components/sections/metrics/MetricsGradientCards.tsx
Normal file
129
src/components/sections/metrics/MetricsGradientCards.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Sparkles, type LucideIcon } from "lucide-react";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string | LucideIcon;
|
||||
};
|
||||
|
||||
type MetricsGradientCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsGradientCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: MetricsGradientCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsGradientCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="relative flex flex-col items-center justify-center gap-0 p-5 min-h-70 h-full card rounded-theme"
|
||||
>
|
||||
<span
|
||||
className="text-9xl font-medium leading-none text-center truncate"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to bottom, var(--color-foreground) 0%, var(--color-foreground) 20%, transparent 72%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
backgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}}
|
||||
>
|
||||
{metric.value}
|
||||
</span>
|
||||
<span className="mt-[-0.75em] text-4xl font-medium text-center truncate">
|
||||
{metric.title}
|
||||
</span>
|
||||
<p className="max-w-9/10 md:max-w-7/10 mt-2 text-base leading-tight text-center line-clamp-2">
|
||||
{metric.description}
|
||||
</p>
|
||||
<div className="absolute bottom-5 left-5 flex items-center justify-center size-10 primary-button rounded-full">
|
||||
<Sparkles className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsGradientCards;
|
||||
114
src/components/sections/metrics/MetricsIconCards.tsx
Normal file
114
src/components/sections/metrics/MetricsIconCards.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Sparkles, type LucideIcon } from "lucide-react";
|
||||
|
||||
type Metric = {
|
||||
icon: string | LucideIcon;
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type MetricsIconCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsIconCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: MetricsIconCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsIconCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="flex flex-col items-center justify-center gap-3 p-5 min-h-70 h-full card rounded-theme"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="flex items-center justify-center size-8 primary-button rounded-full">
|
||||
<Sparkles className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-xl truncate">{metric.title}</span>
|
||||
</div>
|
||||
<span className="text-7xl font-medium leading-none truncate">{metric.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsIconCards;
|
||||
130
src/components/sections/metrics/MetricsMediaCards.tsx
Normal file
130
src/components/sections/metrics/MetricsMediaCards.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
title: string;
|
||||
description: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type MetricsMediaCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: MetricsMediaCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsMediaCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 w-content-width mx-auto">
|
||||
{metrics.map((metric) => (
|
||||
<motion.div
|
||||
key={metric.value}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div className="flex flex-col justify-between gap-5 p-5 aspect-square card rounded-theme">
|
||||
<span className="text-5xl md:text-6xl font-medium leading-tight truncate">
|
||||
{metric.value}
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xl md:text-2xl font-medium truncate">{metric.title}</span>
|
||||
<div className="w-full h-px bg-accent" />
|
||||
<p className="text-base leading-tight truncate">{metric.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-theme overflow-hidden aspect-square">
|
||||
{metric.videoSrc ? (
|
||||
<video
|
||||
src={metric.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Metric video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={metric.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsMediaCards;
|
||||
77
src/components/sections/metrics/MetricsMinimalCards.tsx
Normal file
77
src/components/sections/metrics/MetricsMinimalCards.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type MetricsMinimalCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsMinimalCards = ({ tag, title, metrics }: MetricsMinimalCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsMinimalCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col gap-5">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 w-fit text-sm md:hidden"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-3xl md:text-5xl font-medium leading-tight text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-accent" />
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-8">
|
||||
<span className="hidden md:block card rounded-full px-3 py-1 mb-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-5 flex-1"
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="flex flex-col justify-between gap-5 p-5 md:p-8 aspect-video card rounded-theme"
|
||||
>
|
||||
<span className="text-7xl font-medium leading-none truncate">{metric.value}</span>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="w-full h-px bg-accent" />
|
||||
<p className="text-base md:text-lg leading-tight text-balance">
|
||||
{metric.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsMinimalCards;
|
||||
109
src/components/sections/metrics/MetricsSimpleCards.tsx
Normal file
109
src/components/sections/metrics/MetricsSimpleCards.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type MetricsSimpleCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
metrics: Metric[];
|
||||
};
|
||||
|
||||
const MetricsSimpleCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
metrics,
|
||||
}: MetricsSimpleCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="MetricsSimpleCards"
|
||||
aria-label="Metrics section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.value}
|
||||
className="flex flex-col justify-between gap-5 p-5 min-h-70 h-full card rounded-theme"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsSimpleCards;
|
||||
146
src/components/sections/pricing/PricingCenteredCards.tsx
Normal file
146
src/components/sections/pricing/PricingCenteredCards.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
};
|
||||
|
||||
type PricingCenteredCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingCenteredCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingCenteredCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingCenteredCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.tag}
|
||||
className="flex flex-col items-center gap-3 p-5 h-full card rounded-theme text-center"
|
||||
>
|
||||
<span className="card rounded-full px-5 py-2 text-sm">{plan.tag}</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-5xl font-medium">{plan.price}</span>
|
||||
<span className="text-base">{plan.description}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<a
|
||||
href={plan.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text w-full"
|
||||
>
|
||||
{plan.primaryButton.text}
|
||||
</a>
|
||||
{plan.secondaryButton && (
|
||||
<a
|
||||
href={plan.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text w-full"
|
||||
>
|
||||
{plan.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20" />
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-base text-left">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingCenteredCards;
|
||||
152
src/components/sections/pricing/PricingHighlightedCards.tsx
Normal file
152
src/components/sections/pricing/PricingHighlightedCards.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
highlight?: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
};
|
||||
|
||||
type PricingHighlightedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingHighlightedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingHighlightedCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingHighlightedCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.tag} className="flex flex-col h-full">
|
||||
{plan.highlight ? (
|
||||
<div className="primary-button rounded-theme px-5 py-2 mb-2 text-sm text-center text-primary-cta-text">
|
||||
{plan.highlight}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-5 py-2 mb-2 text-sm invisible">placeholder</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center gap-3 p-5 flex-1 card rounded-theme text-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-5xl font-medium">{plan.price}</span>
|
||||
<span className="text-xl font-medium">{plan.tag}</span>
|
||||
</div>
|
||||
|
||||
<div className="h-px w-full bg-foreground/20" />
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-base text-left">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 w-full mt-auto">
|
||||
<a
|
||||
href={plan.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text w-full"
|
||||
>
|
||||
{plan.primaryButton.text}
|
||||
</a>
|
||||
{plan.secondaryButton && (
|
||||
<a
|
||||
href={plan.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text w-full"
|
||||
>
|
||||
{plan.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingHighlightedCards;
|
||||
143
src/components/sections/pricing/PricingLayeredCards.tsx
Normal file
143
src/components/sections/pricing/PricingLayeredCards.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
features: string[];
|
||||
};
|
||||
|
||||
type PricingLayeredCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingLayeredCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingLayeredCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingLayeredCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.tag} className="flex flex-col gap-3 p-3 h-full card rounded-theme">
|
||||
<div className="flex flex-col gap-3 p-5 secondary-button rounded-theme">
|
||||
<span className="card rounded-full px-3 py-1 w-fit text-sm">{plan.tag}</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-5xl font-medium">{plan.price}</span>
|
||||
<span className="text-base">{plan.description}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<a
|
||||
href={plan.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text w-full"
|
||||
>
|
||||
{plan.primaryButton.text}
|
||||
</a>
|
||||
{plan.secondaryButton && (
|
||||
<a
|
||||
href={plan.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text w-full"
|
||||
>
|
||||
{plan.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-base leading-tight">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingLayeredCards;
|
||||
148
src/components/sections/pricing/PricingMediaCards.tsx
Normal file
148
src/components/sections/pricing/PricingMediaCards.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
period: string;
|
||||
features: string[];
|
||||
primaryButton: { text: string; href: string };
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type PricingMediaCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingMediaCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingMediaCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 w-content-width mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<motion.div
|
||||
key={plan.tag}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row gap-5 md:gap-8 p-5 card rounded-theme"
|
||||
>
|
||||
<div className="w-full md:w-1/2 aspect-square md:aspect-4/3 rounded-theme overflow-hidden">
|
||||
{plan.videoSrc ? (
|
||||
<video
|
||||
src={plan.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Plan video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={plan.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center gap-5 w-full md:w-1/2">
|
||||
<span className="card rounded-full px-3 py-1 w-fit text-sm">
|
||||
{plan.price}
|
||||
{plan.period}
|
||||
</span>
|
||||
<h3 className="text-4xl md:text-5xl font-medium truncate">{plan.tag}</h3>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-sm leading-tight">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={plan.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text w-fit mt-1"
|
||||
>
|
||||
{plan.primaryButton.text}
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingMediaCards;
|
||||
124
src/components/sections/pricing/PricingSimpleCards.tsx
Normal file
124
src/components/sections/pricing/PricingSimpleCards.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
};
|
||||
|
||||
type PricingSimpleCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingSimpleCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingSimpleCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingSimpleCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.tag} className="flex flex-col gap-3 p-5 h-full card rounded-theme">
|
||||
<span className="card rounded-full px-5 py-2 w-fit text-sm">{plan.tag}</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-5xl font-medium">{plan.price}</span>
|
||||
<span className="text-base">{plan.description}</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20" />
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-base">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingSimpleCards;
|
||||
153
src/components/sections/pricing/PricingSplitCards.tsx
Normal file
153
src/components/sections/pricing/PricingSplitCards.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type PricingPlan = {
|
||||
tag: string;
|
||||
price: string;
|
||||
period: string;
|
||||
description: string;
|
||||
primaryButton: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
featuresTitle: string;
|
||||
features: string[];
|
||||
};
|
||||
|
||||
type PricingSplitCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
plans: PricingPlan[];
|
||||
};
|
||||
|
||||
const PricingSplitCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
plans,
|
||||
}: PricingSplitCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="PricingSplitCards"
|
||||
aria-label="Pricing section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 w-content-width mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<motion.div
|
||||
key={plan.tag}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex flex-col md:flex-row gap-5 md:gap-8 p-5 md:p-8 card rounded-theme"
|
||||
>
|
||||
<div className="flex flex-col justify-between gap-5 w-full md:w-1/2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="card rounded-full px-3 py-1 w-fit text-sm">{plan.tag}</span>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-5xl md:text-6xl font-medium">{plan.price}</span>
|
||||
<span className="text-2xl">{plan.period}</span>
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl leading-tight text-balance">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<a
|
||||
href={plan.primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text w-full"
|
||||
>
|
||||
{plan.primaryButton.text}
|
||||
</a>
|
||||
{plan.secondaryButton && (
|
||||
<a
|
||||
href={plan.secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text w-full"
|
||||
>
|
||||
{plan.secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-foreground/20 md:hidden" />
|
||||
|
||||
<div className="flex flex-col gap-5 w-full md:w-1/2">
|
||||
<h3 className="text-xl font-medium">{plan.featuresTitle}</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{plan.features.map((feature) => (
|
||||
<div key={feature} className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
|
||||
<Check className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
<span className="text-base leading-tight">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingSplitCards;
|
||||
121
src/components/sections/product/ProductMediaCards.tsx
Normal file
121
src/components/sections/product/ProductMediaCards.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
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;
|
||||
}[];
|
||||
};
|
||||
|
||||
const ProductMediaCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
products,
|
||||
}: ProductMediaCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ProductMediaCards"
|
||||
aria-label="Products section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{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-theme cursor-pointer"
|
||||
>
|
||||
<div className="aspect-square rounded-theme overflow-hidden">
|
||||
<img src={product.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
</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-8 shrink-0 rounded-full primary-button">
|
||||
<ArrowUpRight className="w-4 h-4 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductMediaCards;
|
||||
140
src/components/sections/product/ProductQuantityCards.tsx
Normal file
140
src/components/sections/product/ProductQuantityCards.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Plus, Minus } from "lucide-react";
|
||||
|
||||
type ProductQuantityCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
products?: {
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
onAddToCart?: (quantity: number) => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
const ProductQuantityCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
products,
|
||||
}: ProductQuantityCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ProductQuantityCards"
|
||||
aria-label="Products section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{products?.map((product) => (
|
||||
<div key={product.name} className="h-full flex flex-col gap-5 p-5 card rounded-theme">
|
||||
<div className="aspect-square rounded-theme overflow-hidden">
|
||||
<img src={product.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-xl font-medium truncate">{product.name}</h3>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center size-8 rounded-full card cursor-pointer"
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<span className="w-fit text-base text-center font-medium">1</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center size-8 rounded-full card cursor-pointer"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => product.onAddToCart?.(1)}
|
||||
className="primary-button rounded-theme h-8 px-5 text-base text-primary-cta-text font-medium cursor-pointer"
|
||||
>
|
||||
{product.price}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductQuantityCards;
|
||||
134
src/components/sections/product/ProductRatingCards.tsx
Normal file
134
src/components/sections/product/ProductRatingCards.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
type ProductRatingCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
products?: {
|
||||
brand: string;
|
||||
name: string;
|
||||
price: string;
|
||||
rating: number;
|
||||
reviewCount: string;
|
||||
imageSrc: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
const ProductRatingCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
products,
|
||||
}: ProductRatingCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ProductRatingCards"
|
||||
aria-label="Products section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{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-theme cursor-pointer"
|
||||
>
|
||||
<div className="aspect-square rounded-theme overflow-hidden">
|
||||
<img src={product.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="secondary-button rounded-full w-fit px-2 py-0.5 text-sm text-secondary-cta-text">
|
||||
{product.brand}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-xl font-medium truncate">{product.name}</h3>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 text-accent">
|
||||
{Array.from({ length: product.rating }).map((_, i) => (
|
||||
<Star key={i} className="w-4 h-4 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm">({product.reviewCount})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl font-medium">{product.price}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductRatingCards;
|
||||
131
src/components/sections/product/ProductVariantCards.tsx
Normal file
131
src/components/sections/product/ProductVariantCards.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
type ProductVariantCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
products?: {
|
||||
name: string;
|
||||
variant: string;
|
||||
price: string;
|
||||
imageSrc: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
const ProductVariantCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
products,
|
||||
}: ProductVariantCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="ProductVariantCards"
|
||||
aria-label="Products section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{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-theme cursor-pointer"
|
||||
>
|
||||
<div className="relative aspect-square rounded-theme overflow-hidden">
|
||||
<img
|
||||
src={product.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
|
||||
<div className="flex items-center justify-center size-12 rounded-full primary-button opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
|
||||
<ArrowUpRight className="w-4 h-4 text-primary-cta-text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<h3 className="text-xl font-medium truncate leading-tight text-balance">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className="text-sm text-foreground/60">{product.variant}</p>
|
||||
</div>
|
||||
|
||||
<span className="text-xl font-medium shrink-0">{product.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductVariantCards;
|
||||
98
src/components/sections/social-proof/SocialProofMarquee.tsx
Normal file
98
src/components/sections/social-proof/SocialProofMarquee.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { motion } from "motion/react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
const SocialProofMarquee = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
names,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
names: string[];
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="SocialProofMarquee"
|
||||
aria-label="Social proof section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="px-3 py-1 mb-1 text-sm card rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
<Marquee speed={45}>
|
||||
{names.map((name, index) => (
|
||||
<div key={index} className="shrink-0 px-5 py-3 rounded-theme card">
|
||||
<span className="text-2xl font-semibold whitespace-nowrap opacity-75">{name}</span>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialProofMarquee;
|
||||
148
src/components/sections/team/TeamDetailedCards.tsx
Normal file
148
src/components/sections/team/TeamDetailedCards.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Link2, type LucideIcon } from "lucide-react";
|
||||
|
||||
type SocialLink = {
|
||||
icon: string | LucideIcon;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
description: string;
|
||||
socialLinks: SocialLink[];
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamDetailedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
const TeamDetailedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
members,
|
||||
}: TeamDetailedCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamDetailedCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<div key={member.name} className="relative aspect-4/5 rounded-theme overflow-hidden">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={member.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-5 left-5 right-5 flex flex-col gap-2 p-5 card rounded-theme">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className="text-2xl font-medium leading-tight truncate">{member.name}</span>
|
||||
<span className="secondary-button rounded-full px-3 py-1 text-xs leading-tight text-secondary-cta-text truncate">
|
||||
{member.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-base leading-tight">{member.description}</p>
|
||||
|
||||
<div className="flex gap-3 mt-1">
|
||||
{member.socialLinks.map((link) => (
|
||||
<a
|
||||
key={link.url}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center size-9 primary-button rounded-full"
|
||||
>
|
||||
<Link2 className="w-4 h-4 text-primary-cta-text" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamDetailedCards;
|
||||
122
src/components/sections/team/TeamGlassCards.tsx
Normal file
122
src/components/sections/team/TeamGlassCards.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamGlassCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
const TeamGlassCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
members,
|
||||
}: TeamGlassCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamGlassCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<div key={member.name} className="relative aspect-4/5 rounded-theme overflow-hidden">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={member.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-1/3 backdrop-blur-xl bg-gradient-to-b from-transparent to-foreground/40" />
|
||||
|
||||
<div className="absolute inset-x-5 bottom-5 flex flex-col text-background">
|
||||
<span className="text-2xl font-medium leading-tight truncate">{member.name}</span>
|
||||
<span className="text-base leading-tight truncate">{member.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamGlassCards;
|
||||
142
src/components/sections/team/TeamListCards.tsx
Normal file
142
src/components/sections/team/TeamListCards.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
detail: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamGroup = {
|
||||
title: string;
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
type TeamListCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
groups: TeamGroup[];
|
||||
};
|
||||
|
||||
const TeamListCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
groups,
|
||||
}: TeamListCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamListCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex flex-col gap-8"
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<div key={group.title} className="p-5 card rounded-theme">
|
||||
<h3 className="mb-3 text-2xl md:text-3xl font-medium">{group.title}</h3>
|
||||
|
||||
<div className="flex flex-col divide-y divide-accent border-t border-accent">
|
||||
{group.members.map((member) => (
|
||||
<div key={member.name} className="flex items-center gap-3 py-5 last:pb-0">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="relative size-12 md:size-16 shrink-0 overflow-hidden rounded-theme">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={member.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-lg md:text-xl font-medium leading-tight truncate">
|
||||
{member.name}
|
||||
</span>
|
||||
<span className="text-base leading-tight opacity-75 truncate">
|
||||
{member.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm md:text-lg font-medium shrink-0">{member.detail}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamListCards;
|
||||
92
src/components/sections/team/TeamMinimalCards.tsx
Normal file
92
src/components/sections/team/TeamMinimalCards.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamMinimalCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
const TeamMinimalCards = ({ tag, title, members }: TeamMinimalCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamMinimalCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-5 md:gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col gap-5">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="md:hidden card rounded-full px-3 py-1 w-fit text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-3xl md:text-5xl font-medium leading-tight text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-accent" />
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-8">
|
||||
<span className="hidden md:block card rounded-full px-3 py-1 mb-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-5 flex-1"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.name}
|
||||
className="flex flex-col gap-5 p-5 card rounded-theme overflow-hidden"
|
||||
>
|
||||
<div className="relative aspect-square md:aspect-5/4 rounded-theme overflow-hidden">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={member.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="w-full h-px bg-accent" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-medium leading-tight truncate">{member.name}</span>
|
||||
<span className="text-base leading-tight truncate">{member.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamMinimalCards;
|
||||
126
src/components/sections/team/TeamOverlayCards.tsx
Normal file
126
src/components/sections/team/TeamOverlayCards.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamOverlayCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
const TeamOverlayCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
members,
|
||||
}: TeamOverlayCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamOverlayCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<div key={member.name} className="relative aspect-4/5 card rounded-theme">
|
||||
<div className="relative w-full h-full rounded-theme overflow-hidden">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img src={member.imageSrc} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-b from-transparent to-foreground/60" />
|
||||
|
||||
<div className="absolute bottom-5 left-5 right-5 flex items-center justify-between gap-3 p-3 card rounded-theme">
|
||||
<span className="text-xl font-medium leading-tight truncate">{member.name}</span>
|
||||
<span className="primary-button rounded-full px-3 py-2 text-sm leading-tight text-primary-cta-text truncate">
|
||||
{member.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamOverlayCards;
|
||||
126
src/components/sections/team/TeamStackedCards.tsx
Normal file
126
src/components/sections/team/TeamStackedCards.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TeamStackedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
members: TeamMember[];
|
||||
};
|
||||
|
||||
const TeamStackedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
members,
|
||||
}: TeamStackedCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TeamStackedCards"
|
||||
aria-label="Team section"
|
||||
className="relative w-full py-20"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<a
|
||||
href={primaryButton.href}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<a
|
||||
href={secondaryButton.href}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
className="flex flex-wrap justify-center gap-y-5"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.name}
|
||||
className="flex flex-col items-center w-[55%] md:w-[28%] -mx-[4%] md:-mx-[2%] text-center"
|
||||
>
|
||||
<div className="p-3 mb-3 w-full aspect-square card rounded-theme overflow-hidden">
|
||||
{member.videoSrc ? (
|
||||
<video
|
||||
src={member.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label="Member video"
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={member.imageSrc}
|
||||
alt=""
|
||||
className="w-full h-full object-cover rounded-theme"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="w-4/5 text-2xl font-medium leading-tight truncate">{member.name}</span>
|
||||
<span className="w-4/5 text-base leading-tight opacity-75 truncate">{member.role}</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamStackedCards;
|
||||
116
src/components/sections/testimonial/TestimonialAvatarCard.tsx
Normal file
116
src/components/sections/testimonial/TestimonialAvatarCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Avatar = {
|
||||
name: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialAvatarCardProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
avatars: Avatar[];
|
||||
};
|
||||
|
||||
const TestimonialAvatarCard = ({
|
||||
tag,
|
||||
title,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
avatars,
|
||||
}: TestimonialAvatarCardProps) => {
|
||||
const visibleAvatars = avatars.slice(0, 5);
|
||||
const remainingCount = avatars.length - visibleAvatars.length;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialAvatarCard"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-theme flex flex-col items-center gap-5 py-8 px-8"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<span className="card rounded-full px-3 py-1 mb-1 text-sm">{tag}</span>
|
||||
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="md:max-w-7/10 text-3xl md:text-5xl font-medium leading-tight text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h3>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-1 -space-x-5">
|
||||
{visibleAvatars.map((avatar) =>
|
||||
avatar.videoSrc ? (
|
||||
<video
|
||||
key={avatar.name}
|
||||
src={avatar.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={avatar.name}
|
||||
className="size-14 md:size-20 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
key={avatar.name}
|
||||
src={avatar.imageSrc}
|
||||
alt={avatar.name}
|
||||
className="size-14 md:size-20 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{remainingCount > 0 && (
|
||||
<div className="card flex items-center justify-center size-14 md:size-20 rounded-full border-2 border-background">
|
||||
<span className="text-sm md:text-base font-medium">+{remainingCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialAvatarCard;
|
||||
145
src/components/sections/testimonial/TestimonialDetailedCards.tsx
Normal file
145
src/components/sections/testimonial/TestimonialDetailedCards.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
title: string;
|
||||
quote: string;
|
||||
name: string;
|
||||
role: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialDetailedCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialDetailedCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialDetailedCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialDetailedCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 gap-5 md:grid-cols-2"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="card rounded-theme grid grid-cols-1 md:grid-cols-2 overflow-hidden">
|
||||
<div className="flex flex-col justify-between gap-5 p-5 md:p-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-2xl md:text-3xl font-medium leading-tight">{testimonial.title}</h3>
|
||||
<blockquote className="text-base md:text-lg leading-tight opacity-75">
|
||||
“{testimonial.quote}”
|
||||
</blockquote>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="aspect-square overflow-hidden">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialDetailedCards;
|
||||
177
src/components/sections/testimonial/TestimonialMarqueeCards.tsx
Normal file
177
src/components/sections/testimonial/TestimonialMarqueeCards.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { motion } from "motion/react";
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
quote: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialMarqueeCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialMarqueeCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialMarqueeCardsProps) => {
|
||||
const half = Math.ceil(testimonials.length / 2);
|
||||
const topRow = testimonials.slice(0, half);
|
||||
const bottomRow = testimonials.slice(half);
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialMarqueeCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<Marquee speed={40}>
|
||||
{topRow.map((testimonial, index) => (
|
||||
<div key={`top-${testimonial.name}-${index}`} className="card rounded-theme shrink-0 w-72 md:w-80 p-5">
|
||||
<div className="flex flex-col justify-between gap-5 h-full">
|
||||
<p className="text-lg leading-tight line-clamp-3">{testimonial.quote}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
|
||||
<Marquee speed={40} reverse>
|
||||
{bottomRow.map((testimonial, index) => (
|
||||
<div key={`bottom-${testimonial.name}-${index}`} className="card rounded-theme shrink-0 w-72 md:w-80 p-5">
|
||||
<div className="flex flex-col justify-between gap-5 h-full">
|
||||
<p className="text-lg leading-tight line-clamp-3">{testimonial.quote}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialMarqueeCards;
|
||||
169
src/components/sections/testimonial/TestimonialMetricsCards.tsx
Normal file
169
src/components/sections/testimonial/TestimonialMetricsCards.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Star } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
company: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type TestimonialMetricsCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
metrics: [Metric, Metric, Metric];
|
||||
};
|
||||
|
||||
const TestimonialMetricsCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
metrics,
|
||||
}: TestimonialMetricsCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialMetricsCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="relative aspect-3/4 rounded-theme overflow-hidden">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="card rounded-theme absolute inset-x-5 bottom-5 flex flex-col gap-2 p-4">
|
||||
<div className="flex gap-1 mb-1">
|
||||
{Array.from({ length: testimonial.rating }).map((_, i) => (
|
||||
<Star key={i} className="size-5 text-accent fill-current" strokeWidth={1.5} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-2xl font-medium leading-tight">{testimonial.name}</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base leading-tight">{testimonial.role}</span>
|
||||
<span className="text-base leading-tight opacity-75">{testimonial.company}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: "easeOut" }}
|
||||
className="card rounded-theme flex flex-col md:flex-row items-center justify-between p-8"
|
||||
>
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={metric.label} className="flex flex-col md:flex-row items-center w-full md:flex-1">
|
||||
<div className="flex flex-col items-center flex-1 gap-1 text-center py-5 md:py-0">
|
||||
<span className="text-5xl font-medium">{metric.value}</span>
|
||||
<span className="text-base">{metric.label}</span>
|
||||
</div>
|
||||
{index < 2 && (
|
||||
<div className="w-full h-px md:h-20 md:w-px bg-foreground/20" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialMetricsCards;
|
||||
142
src/components/sections/testimonial/TestimonialOverlayCards.tsx
Normal file
142
src/components/sections/testimonial/TestimonialOverlayCards.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Star } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
company: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialOverlayCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialOverlayCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialOverlayCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialOverlayCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="relative aspect-3/4 rounded-theme overflow-hidden">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="card rounded-theme absolute inset-x-5 bottom-5 flex flex-col gap-2 p-4">
|
||||
<div className="flex gap-1 mb-1">
|
||||
{Array.from({ length: testimonial.rating }).map((_, i) => (
|
||||
<Star key={i} className="size-5 text-accent fill-current" strokeWidth={1.5} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-2xl font-medium leading-tight">{testimonial.name}</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base leading-tight">{testimonial.role}</span>
|
||||
<span className="text-base leading-tight opacity-75">{testimonial.company}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialOverlayCards;
|
||||
137
src/components/sections/testimonial/TestimonialQuoteCards.tsx
Normal file
137
src/components/sections/testimonial/TestimonialQuoteCards.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
quote: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialQuoteCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialQuoteCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialQuoteCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialQuoteCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="card rounded-theme flex flex-col gap-4 p-5">
|
||||
<div className="size-24 overflow-hidden rounded-theme">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-base leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-lg leading-tight">{testimonial.quote}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialQuoteCards;
|
||||
145
src/components/sections/testimonial/TestimonialRatingCards.tsx
Normal file
145
src/components/sections/testimonial/TestimonialRatingCards.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Star } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
quote: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialRatingCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialRatingCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialRatingCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialRatingCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-5"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="card rounded-theme flex flex-col justify-between gap-5 h-full p-5">
|
||||
<div className="flex flex-col items-start gap-5">
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: testimonial.rating }).map((_, i) => (
|
||||
<Star key={i} className="size-5 text-accent fill-current" strokeWidth={1.5} />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-lg leading-tight">{testimonial.quote}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.name}
|
||||
className="size-10 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialRatingCards;
|
||||
165
src/components/sections/testimonial/TestimonialSplitCards.tsx
Normal file
165
src/components/sections/testimonial/TestimonialSplitCards.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Testimonial = {
|
||||
tag: string;
|
||||
title: string;
|
||||
quote: string;
|
||||
name: string;
|
||||
date: string;
|
||||
} & ({ avatarImageSrc: string; avatarVideoSrc?: never } | { avatarVideoSrc: string; avatarImageSrc?: never })
|
||||
& ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialSplitCardsProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
};
|
||||
|
||||
const TestimonialSplitCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: TestimonialSplitCardsProps) => {
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialSplitCards"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card rounded-full px-3 py-1 mb-1 text-sm"
|
||||
>
|
||||
{tag}
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-3">
|
||||
{primaryButton && (
|
||||
<motion.a
|
||||
href={primaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<motion.a
|
||||
href={secondaryButton.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
|
||||
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</motion.a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-10%" }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="card rounded-theme flex flex-col md:grid md:grid-cols-2 overflow-hidden">
|
||||
<div className="flex flex-col justify-between gap-5 md:gap-8 p-5 md:p-8">
|
||||
<div className="flex flex-col gap-3 md:gap-5">
|
||||
<span className="card rounded-full px-3 py-1 w-fit text-sm">{testimonial.tag}</span>
|
||||
<h3 className="text-3xl md:text-4xl font-medium leading-tight">{testimonial.title}</h3>
|
||||
<p className="text-base md:text-lg leading-tight opacity-75">{testimonial.quote}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{testimonial.avatarVideoSrc ? (
|
||||
<video
|
||||
src={testimonial.avatarVideoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.name}
|
||||
className="size-12 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.avatarImageSrc}
|
||||
alt={testimonial.name}
|
||||
className="size-12 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative min-h-80 h-full md:aspect-square">
|
||||
{testimonial.videoSrc ? (
|
||||
<video
|
||||
src={testimonial.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={testimonial.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={testimonial.imageSrc}
|
||||
alt={testimonial.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialSplitCards;
|
||||
102
src/components/sections/testimonial/TestimonialTrustCard.tsx
Normal file
102
src/components/sections/testimonial/TestimonialTrustCard.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Star } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Avatar = {
|
||||
name: string;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
type TestimonialTrustCardProps = {
|
||||
quote: string;
|
||||
rating: number;
|
||||
author: string;
|
||||
avatars: Avatar[];
|
||||
};
|
||||
|
||||
const TestimonialTrustCard = ({
|
||||
quote,
|
||||
rating,
|
||||
author,
|
||||
avatars,
|
||||
}: TestimonialTrustCardProps) => {
|
||||
const visibleAvatars = avatars.slice(0, 6);
|
||||
const remainingCount = avatars.length - visibleAvatars.length;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-webild-section="TestimonialTrustCard"
|
||||
aria-label="Testimonials section"
|
||||
className="relative w-full py-16 md:py-24"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-5 w-content-width mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="flex gap-1"
|
||||
>
|
||||
{Array.from({ length: rating }).map((_, i) => (
|
||||
<Star key={i} className="size-6 text-accent fill-current" strokeWidth={1.5} />
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
|
||||
className="text-3xl md:text-5xl font-medium leading-tight text-center text-balance"
|
||||
>
|
||||
{quote}
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
className="text-xl text-center"
|
||||
>
|
||||
{author}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
|
||||
className="flex items-center justify-center -space-x-5"
|
||||
>
|
||||
{visibleAvatars.map((avatar) =>
|
||||
avatar.videoSrc ? (
|
||||
<video
|
||||
key={avatar.name}
|
||||
src={avatar.videoSrc}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
aria-label={avatar.name}
|
||||
className="size-12 md:size-16 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
key={avatar.name}
|
||||
src={avatar.imageSrc}
|
||||
alt={avatar.name}
|
||||
className="size-12 md:size-16 rounded-full border-2 border-background object-cover"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{remainingCount > 0 && (
|
||||
<div className="card flex items-center justify-center size-12 md:size-16 rounded-full border-2 border-background">
|
||||
<span className="text-sm md:text-base font-medium">+{remainingCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialTrustCard;
|
||||
53
src/components/tag/Tag.tsx
Normal file
53
src/components/tag/Tag.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useRive, Layout, Fit } from "@rive-app/react-canvas";
|
||||
import { useTagEffects } from "./useTagEffects";
|
||||
import { useRiveHoverInput } from "./useRiveHoverInput";
|
||||
|
||||
const STATE_MACHINE_NAME = "State Machine 1";
|
||||
const HOVER_INPUT_NAME = "Hover";
|
||||
|
||||
export const Tag = () => {
|
||||
const { shouldShow, handleMouseEnter, handleClick, buttonClassName } = useTagEffects();
|
||||
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/watermark-bob2.riv",
|
||||
stateMachines: STATE_MACHINE_NAME,
|
||||
autoplay: true,
|
||||
layout: new Layout({
|
||||
fit: Fit.Contain,
|
||||
}),
|
||||
});
|
||||
|
||||
const setHover = useRiveHoverInput(rive, STATE_MACHINE_NAME, HOVER_INPUT_NAME);
|
||||
|
||||
const handleTagClick = () => {
|
||||
window.open("https://webild.io", "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
handleMouseEnter();
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Webild tag"
|
||||
className={`fixed z-[99999] bottom-6 right-6 w-[160px] h-[92px] cursor-pointer ${buttonClassName}`}
|
||||
onClick={(e) => handleClick(e, handleTagClick)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<RiveComponent className="w-full h-full" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
30
src/components/tag/useRiveHoverInput.ts
Normal file
30
src/components/tag/useRiveHoverInput.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useStateMachineInput } from "@rive-app/react-canvas";
|
||||
|
||||
export function useRiveHoverInput(
|
||||
rive: unknown,
|
||||
stateMachineName: string,
|
||||
hoverInputName: string
|
||||
) {
|
||||
const hoverInput = useStateMachineInput(
|
||||
rive as never,
|
||||
stateMachineName,
|
||||
hoverInputName
|
||||
);
|
||||
|
||||
const hoverInputRef = useRef<typeof hoverInput | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
hoverInputRef.current = hoverInput ?? null;
|
||||
}, [hoverInput]);
|
||||
|
||||
return useCallback(
|
||||
(isHovering: boolean) => {
|
||||
const input = hoverInputRef.current;
|
||||
if (!input) return;
|
||||
input.value = isHovering;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
54
src/components/tag/useTagEffects.ts
Normal file
54
src/components/tag/useTagEffects.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useRef, useEffect, useCallback, useState } from "react";
|
||||
|
||||
export function useTagEffects<T extends HTMLElement = HTMLButtonElement>() {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [shouldShow, setShouldShow] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
audioRef.current = new Audio("https://webuild-dev.s3.eu-north-1.amazonaws.com/default/audio/click.mp3");
|
||||
audioRef.current.volume = 0.75;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.self !== window.top) {
|
||||
try {
|
||||
const parentHostname = window.top?.location.hostname;
|
||||
if (parentHostname?.includes('webild.io')) {
|
||||
setShouldShow(false);
|
||||
}
|
||||
} catch {
|
||||
setShouldShow(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const playSound = useCallback(() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play().catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (window.innerWidth > 768) {
|
||||
playSound();
|
||||
}
|
||||
}, [playSound]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<T>, onClick?: (e: React.MouseEvent<T>) => void) => {
|
||||
playSound();
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
},
|
||||
[playSound]
|
||||
);
|
||||
|
||||
return {
|
||||
shouldShow,
|
||||
handleMouseEnter,
|
||||
handleClick,
|
||||
buttonClassName: "transition-all duration-200 hover:-translate-y-[3px]",
|
||||
};
|
||||
}
|
||||
55
src/components/ui/Accordion.tsx
Normal file
55
src/components/ui/Accordion.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface AccordionProps {
|
||||
items: { title: string; content: string }[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Accordion = ({ items, className = "" }: AccordionProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className={cls("flex flex-col gap-3", className)}>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => setActiveIndex(activeIndex === index ? null : index)}
|
||||
className="p-3 2xl:p-4 rounded secondary-button cursor-pointer select-none"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-base md:text-lg font-medium leading-tight">{item.title}</h3>
|
||||
<div className="flex shrink-0 items-center justify-center size-7 md:size-8 rounded primary-button">
|
||||
<Plus
|
||||
className={cls(
|
||||
"size-3.5 md:size-4 text-primary-cta-text transition-transform duration-300",
|
||||
activeIndex === index && "rotate-45"
|
||||
)}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{activeIndex === index && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<p className="pt-1 text-sm leading-tight">{item.content}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accordion;
|
||||
46
src/components/ui/AnimatedBarChart.tsx
Normal file
46
src/components/ui/AnimatedBarChart.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const BARS = [
|
||||
{ height: 100, hoverHeight: 40 },
|
||||
{ height: 84, hoverHeight: 100 },
|
||||
{ height: 62, hoverHeight: 75 },
|
||||
{ height: 90, hoverHeight: 50 },
|
||||
{ height: 70, hoverHeight: 90 },
|
||||
{ height: 50, hoverHeight: 60 },
|
||||
{ height: 75, hoverHeight: 85 },
|
||||
{ height: 80, hoverHeight: 70 },
|
||||
];
|
||||
|
||||
const AnimatedBarChart = () => {
|
||||
const [active, setActive] = useState(2);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setActive((p) => (p + 1) % BARS.length), 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden md:block h-full w-full"
|
||||
style={{ maskImage: "linear-gradient(to bottom, black 40%, transparent)" }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className="flex items-end gap-4 h-full w-full">
|
||||
{BARS.map((bar, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative w-full rounded bg-background-accent transition-all duration-500"
|
||||
style={{ height: `${isHovered ? bar.hoverHeight : bar.height}%` }}
|
||||
>
|
||||
<div className={cls("absolute inset-0 rounded primary-button transition-opacity duration-500", active === i ? "opacity-100" : "opacity-0")} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedBarChart;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user