0.95
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m56s

This commit is contained in:
2026-03-25 16:25:19 -04:00
parent 66495f7b17
commit 70d8d83691
137 changed files with 1668 additions and 5133 deletions

View File

@@ -1,165 +0,0 @@
import { BuildingOffice2Icon, EnvelopeIcon, PhoneIcon } from '@heroicons/react/24/outline'
interface Props {
phoneLabel: string
phoneValue: string
emailLabel: string
emailValue: string
addressLabel: string
addressValue: string
serviceAreaLabel: string
serviceArea: string
firstNameLabel?: string
lastNameLabel?: string
emailFieldLabel?: string
phoneFieldLabel?: string
messageLabel?: string
submitLabel?: string
}
export default function Contact({
phoneLabel,
phoneValue,
emailLabel,
emailValue,
addressLabel,
addressValue,
serviceAreaLabel,
serviceArea,
firstNameLabel = 'Prénom',
lastNameLabel = 'Nom',
emailFieldLabel = 'Courriel',
phoneFieldLabel = 'Téléphone',
messageLabel = 'Message',
submitLabel = 'Envoyer',
}: Props) {
const inputClass =
'block w-full rounded-md bg-white px-3.5 py-2 text-base text-brand-dark outline-1 -outline-offset-1 outline-brand-200 placeholder:text-brand-400 focus:outline-2 focus:-outline-offset-2 focus:outline-brand-600 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-brand-400 dark:focus:outline-brand-500'
const labelClass = 'block text-sm/6 font-semibold text-brand-dark dark:text-white'
return (
<div className="relative isolate bg-brand-50/50 dark:bg-brand-900">
<div className="mx-auto grid max-w-7xl grid-cols-1 lg:grid-cols-2">
<div className="relative px-6 pt-12 pb-20 lg:static">
<div className="mx-auto max-w-xl lg:mx-0 lg:max-w-lg">
<div className="absolute inset-y-0 left-0 -z-10 w-full overflow-hidden bg-brand-50 ring-1 ring-brand-900/10 lg:w-1/2 dark:bg-brand-900 dark:ring-white/10">
<svg
aria-hidden="true"
className="absolute inset-0 size-full mask-[radial-gradient(100%_100%_at_top_right,white,transparent)] stroke-brand-200 dark:stroke-white/10"
>
<defs>
<pattern
x="100%"
y={-1}
id="83fd4e5a-9d52-42fc-97b6-718e5d7ee527"
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path d="M130 200V.5M.5 .5H200" fill="none" />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} className="fill-white dark:fill-brand-900" />
<svg x="100%" y={-1} className="overflow-visible fill-brand-50 dark:fill-brand-800/20">
<path d="M-470.5 0h201v201h-201Z" strokeWidth={0} />
</svg>
<rect fill="url(#83fd4e5a-9d52-42fc-97b6-718e5d7ee527)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div
aria-hidden="true"
className="absolute top-[calc(100%-13rem)] -left-56 hidden transform-gpu blur-3xl lg:top-[calc(50%-7rem)] lg:left-[max(-14rem,calc(100%-59rem))] dark:block"
>
<div
style={{
clipPath:
'polygon(74.1% 56.1%, 100% 38.6%, 97.5% 73.3%, 85.5% 100%, 80.7% 98.2%, 72.5% 67.7%, 60.2% 37.8%, 52.4% 32.2%, 47.5% 41.9%, 45.2% 65.8%, 27.5% 23.5%, 0.1% 35.4%, 17.9% 0.1%, 27.6% 23.5%, 76.1% 2.6%, 74.1% 56.1%)',
}}
className="aspect-1155/678 w-288.75 bg-linear-to-br from-brand-300 to-brand-600 opacity-10 dark:opacity-20"
/>
</div>
</div>
<dl className="mt-10 space-y-4 text-base/7 text-brand-700 dark:text-brand-100">
<div className="flex gap-x-4">
<dt className="flex-none">
<span className="sr-only">{addressLabel}</span>
<BuildingOffice2Icon aria-hidden="true" className="h-7 w-6 text-brand-400" />
</dt>
<dd>
{addressValue}
<br />
<span className="text-sm text-brand-500">{serviceAreaLabel} {serviceArea}</span>
</dd>
</div>
<div className="flex gap-x-4">
<dt className="flex-none">
<span className="sr-only">{phoneLabel}</span>
<PhoneIcon aria-hidden="true" className="h-7 w-6 text-brand-400" />
</dt>
<dd>
<a href={`tel:${phoneValue.replace(/\D/g, '')}`} className="hover:text-brand-dark dark:hover:text-white">
{phoneValue}
</a>
</dd>
</div>
<div className="flex gap-x-4">
<dt className="flex-none">
<span className="sr-only">{emailLabel}</span>
<EnvelopeIcon aria-hidden="true" className="h-7 w-6 text-brand-400" />
</dt>
<dd>
<a href={`mailto:${emailValue}`} className="hover:text-brand-dark dark:hover:text-white">
{emailValue}
</a>
</dd>
</div>
</dl>
</div>
</div>
<form action="#" method="POST" className="px-6 pt-12 pb-20">
<div className="mx-auto max-w-xl lg:mr-0 lg:max-w-lg">
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
<div>
<label htmlFor="first-name" className={labelClass}>{firstNameLabel}</label>
<div className="mt-2.5">
<input id="first-name" name="first-name" type="text" autoComplete="given-name" className={inputClass} />
</div>
</div>
<div>
<label htmlFor="last-name" className={labelClass}>{lastNameLabel}</label>
<div className="mt-2.5">
<input id="last-name" name="last-name" type="text" autoComplete="family-name" className={inputClass} />
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="email" className={labelClass}>{emailFieldLabel}</label>
<div className="mt-2.5">
<input id="email" name="email" type="email" autoComplete="email" className={inputClass} />
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="phone-number" className={labelClass}>{phoneFieldLabel}</label>
<div className="mt-2.5">
<input id="phone-number" name="phone-number" type="tel" autoComplete="tel" className={inputClass} />
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="message" className={labelClass}>{messageLabel}</label>
<div className="mt-2.5">
<textarea id="message" name="message" rows={4} className={inputClass} defaultValue={''} />
</div>
</div>
</div>
<div className="mt-8 flex justify-end">
<button
type="submit"
className="rounded-md bg-brand-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-xs hover:bg-brand-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600 dark:bg-brand-500 dark:hover:bg-brand-400 dark:focus-visible:outline-brand-500"
>
{submitLabel}
</button>
</div>
</div>
</form>
</div>
</div>
)
}

View File

@@ -7,6 +7,26 @@ interface FullWidthWithBackgroundImageAndLargeContentProps {
imageAlt?: string;
}
import { motion } from "motion/react";
const fadeUp = (delay: number) => ({
initial: { opacity: 0, y: 18 },
whileInView: { opacity: 1, y: 0 },
viewport: { amount: 0.35, once: false },
transition: {
duration: 0.55,
ease: [0.22, 1, 0.36, 1] as const,
delay,
},
})
const bgReveal = {
initial: { opacity: 0.6, scale: 1.06 },
whileInView: { opacity: 1, scale: 1 },
viewport: { amount: 0.25, once: false },
transition: { duration: 0.9, ease: [0.22, 1, 0.36, 1] as const },
}
export default function FullWidthWithBackgroundImageAndLargeContent({
title,
subtitle,
@@ -20,7 +40,8 @@ export default function FullWidthWithBackgroundImageAndLargeContent({
<div className="relative bg-gray-900 min-h-[40rem] flex items-center justify-center">
{/* Decorative image and overlay */}
<div aria-hidden="true" className="absolute inset-0 overflow-hidden">
<img
<motion.img
{...bgReveal}
alt={imageAlt}
src={imageSrc}
className="size-full object-cover"
@@ -29,18 +50,25 @@ export default function FullWidthWithBackgroundImageAndLargeContent({
<div aria-hidden="true" className="absolute inset-0 bg-gray-900 opacity-50" />
<div className="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-24 text-center sm:py-32 lg:px-0">
<h2 id="cta-heading" className="text-3xl font-bold tracking-tight text-brand-50 sm:text-4xl lg:text-5xl">
<motion.h2
{...fadeUp(0)}
id="cta-heading"
className="text-3xl font-bold tracking-tight text-brand-50 sm:text-4xl lg:text-5xl"
>
{title}
</h2>
<p className="mt-4 text-lg text-brand-50/95 sm:text-xl">
</motion.h2>
<motion.p {...fadeUp(0.1)} className="mt-4 text-lg text-brand-50/95 sm:text-xl">
{subtitle}
</p>
<a
</motion.p>
<motion.a
{...fadeUp(0.2)}
whileHover={{ scale: 1.05, transition: { duration: 0.1 } }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
href={buttonHref}
className="mt-8 inline-block rounded-md border border-transparent bg-[var(--color-brand-600)] px-8 py-3 text-base font-medium text-white shadow-sm transition-colors hover:bg-[var(--color-brand-700)]"
>
{buttonText}
</a>
</motion.a>
</div>
</div>
</section>

View File

@@ -0,0 +1,292 @@
import { motion } from "motion/react";
type NavLink = { name: string; href: string };
type Props = {
siteName: string;
serviceArea: string;
phone: string;
email: string;
address: string;
facebook?: string;
instagram?: string;
linkedin?: string;
footer: {
tagline: string;
facebookLabel: string;
serviceAreasTitle: string;
serviceAreas: string[];
navTitle: string;
contactTitle: string;
rbq: string;
neq: string;
};
navLinks: NavLink[];
legalNotice: string;
};
const fadeUp = (delay: number) => ({
initial: { opacity: 0, y: 18 },
whileInView: { opacity: 1, y: 0 },
viewport: { amount: 0.3, once: false },
transition: {
duration: 0.55,
ease: [0.22, 1, 0.36, 1] as const,
delay,
},
});
const hoverPop = {
whileHover: { scale: 1.05, transition: { duration: 0.2 } },
transition: { type: "spring" as const, stiffness: 500, damping: 30 },
};
export default function FooterAnimated({
siteName,
serviceArea,
phone,
email,
address,
facebook,
instagram,
linkedin,
footer,
navLinks,
legalNotice,
}: Props) {
return (
<motion.footer
className="bg-brand-600 pt-14 px-8 text-white/85"
initial="initial"
whileInView="whileInView"
viewport={{ amount: 0.2, once: false }}
>
<div className="container-site grid grid-cols-1 gap-7 pb-12 min-[541px]:grid-cols-2 min-[541px]:gap-8 min-[901px]:grid-cols-[1.5fr_2fr_1fr_1fr] min-[901px]:gap-10">
<motion.div {...fadeUp(0)}>
<motion.a {...hoverPop} href="/" aria-label={siteName}>
<img
src="/images/logo-vignette.svg"
alt={siteName}
width={140}
height={52}
loading="lazy"
className="mb-4 block h-11 w-auto brightness-75 invert"
/>
</motion.a>
<p className="mb-2 text-sm font-normal text-white/75">
{footer.tagline}
</p>
<p className="mb-4 flex items-center gap-1.5 text-sm text-white/65">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
{serviceArea}
</p>
{(facebook || instagram || linkedin) && (
<div className="flex flex-wrap gap-2">
{facebook && (
<motion.a
{...hoverPop}
href={facebook}
className="inline-flex items-center gap-2 rounded border border-white/30 px-3.5 py-1.5 text-sm font-bold text-white/80 transition-colors hover:bg-white/12 hover:text-white"
target="_blank"
rel="noopener noreferrer"
aria-label={footer.facebookLabel}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
</svg>
{footer.facebookLabel}
</motion.a>
)}
{instagram && (
<motion.a
{...hoverPop}
href={instagram}
className="inline-flex items-center gap-2 rounded border border-white/30 px-3.5 py-1.5 text-sm font-bold text-white/80 transition-colors hover:bg-white/12 hover:text-white"
target="_blank"
rel="noopener noreferrer"
aria-label="Instagram"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
</svg>
Instagram
</motion.a>
)}
{linkedin && (
<motion.a
{...hoverPop}
href={linkedin}
className="inline-flex items-center gap-2 rounded border border-white/30 px-3.5 py-1.5 text-sm font-bold text-white/80 transition-colors hover:bg-white/12 hover:text-white"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.778v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
LinkedIn
</motion.a>
)}
</div>
)}
</motion.div>
<motion.div {...fadeUp(0.08)}>
<h3 className="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
{footer.serviceAreasTitle}
</h3>
<ul className="m-0 list-none columns-2 gap-x-6 p-0 [&>li]:mb-2">
{footer.serviceAreas.map((area) => (
<li
key={area}
className="text-[0.875rem] text-white/70 break-inside-avoid"
>
{area}
</li>
))}
</ul>
</motion.div>
<motion.div {...fadeUp(0.16)}>
<h3 className="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
{footer.navTitle}
</h3>
<ul className="m-0 flex list-none flex-col gap-2.5 p-0">
{navLinks.map((link) => (
<li key={link.href}>
<motion.a
{...hoverPop}
href={link.href}
className="inline-flex text-[0.9375rem] text-white/80 transition-colors hover:text-white"
>
{link.name}
</motion.a>
</li>
))}
</ul>
</motion.div>
<motion.div {...fadeUp(0.24)}>
<h3 className="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
{footer.contactTitle}
</h3>
<ul className="m-0 flex list-none flex-col gap-3 p-0">
<li>
<motion.a
{...hoverPop}
href={`tel:${phone.replace(/\D/g, "")}`}
className="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white"
>
<svg
className="mt-[3px] shrink-0"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2.18h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.18 6.18l.94-.94a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
{phone}
</motion.a>
</li>
<li>
<motion.a
{...hoverPop}
href={`mailto:${email}`}
className="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white"
>
<svg
className="mt-[3px] shrink-0"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
{email}
</motion.a>
</li>
<li className="flex items-start gap-2 text-[0.9375rem] text-white/70">
<svg
className="mt-[3px] shrink-0"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
{address}
</li>
</ul>
</motion.div>
</div>
<motion.div {...fadeUp(0.1)} className="border-t border-white/15 py-[1.125rem]">
<div className="container-site flex flex-wrap items-center justify-between gap-4 max-[540px]:flex-col max-[540px]:items-start max-[540px]:gap-2">
<p className="text-[0.8125rem] text-white/50">{legalNotice}</p>
<motion.p {...fadeUp(0.32)} className="text-[0.8125rem] text-white/50">
Réalisation par{" "}
<motion.a
{...hoverPop}
href="https://www.linkedin.com/in/mouadbouras/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex text-white/70 hover:text-white"
>
MBCube Consulting
</motion.a>
</motion.p>
<p className="flex items-center gap-2.5 text-xs tracking-[0.02em] text-white/40">
<span>{footer.rbq}</span>
<span aria-hidden="true">·</span>
<span>{footer.neq}</span>
</p>
</div>
</motion.div>
</motion.footer>
);
}

View File

@@ -3,10 +3,9 @@ import {
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { motion } from "motion/react";
import Autoplay from "embla-carousel-autoplay";
import general from "../../content/settings/general.json";
import logo from "@/assets/cachet-hero-logo.svg";
import general from "@/content/settings/general.json";
export type HeroContent = {
title: string;
@@ -16,6 +15,11 @@ export type HeroContent = {
ctaHref: string;
};
const logo = {
src: '/images/logo-text.svg',
alt: 'Cachet Peintres Décorateurs',
}
const slides = [
{ src: '/images/carrousel-1.webp', alt: "Peinture intérieure" },
{ src: '/images/carrousel-2.webp', alt: "Décoration murale" },
@@ -30,6 +34,16 @@ const autoplayPlugin = Autoplay({
stopOnMouseEnter: false,
});
const fadeUp = (delay: number) => ({
initial: { opacity: 0, y: 24 },
animate: { opacity: 1, y: 0 },
transition: {
duration: 0.6,
ease: [0.22, 1, 0.36, 1] as const,
delay,
},
});
export default function Hero({ content }: { content: HeroContent }) {
const { subtitle, cta, ctaHref } = content;
@@ -57,42 +71,33 @@ export default function Hero({ content }: { content: HeroContent }) {
<div className="relative z-10 mx-auto max-w-7xl px-6 lg:px-8 h-full flex items-center justify-center pt-14">
<div className="mx-auto max-w-4xl text-center">
{/* <div className="hidden sm:mb-8 sm:flex sm:justify-center">
<div className="flex flex-wrap items-center justify-center gap-2 gap-x-3.5 rounded-full px-4 py-2 text-sm/6 text-gray-300 ring-1 ring-white/20">
<span>RBQ {general.rbq}</span>
<span className="text-white/30" aria-hidden>
·
</span>
<span>{general.serviceArea}</span>
<span className="text-white/30" aria-hidden>
·
</span>
<span>{pillCta}</span>
</div>
</div> */}
{/* <h1
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-7xl"
style={{ fontFamily: "var(--font-display)" }}
<motion.div {...fadeUp(0.2)}>
<img
src={logo.src}
alt={general.siteName}
className="w-full h-auto"
/>
</motion.div>
<motion.p
{...fadeUp(0.45)}
className="p-4 mt-8 text-lg font-medium text-pretty text-white sm:text-xl/8 lg:text-2xl/8"
>
{title}
</h1>
<h2
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-6xl"
style={{ fontFamily: "var(--font-display)" }}
>
{title2}
</h2> */}
<img src={logo.src} alt={general.siteName} className="w-full h-auto" />
<p className="mt-8 text-lg font-medium text-pretty text-gray-300 sm:text-xl/8">
{subtitle}
</p>
<div className="mt-10 flex items-center justify-center">
<a
</motion.p>
<div
{...fadeUp(0.65)}
className="mt-10 flex items-center justify-center"
>
<motion.a
whileHover={{ scale: 1.075, transition: { duration: 0.1 } }}
transition={{ type: "spring", stiffness: 400, damping: 25, }}
href={ctaHref}
className="rounded-md bg-[var(--color-brand-600)] px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-[var(--color-brand-700)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-brand-600)]"
className="rounded-md bg-[var(--color-brand-600)] px-3.5 py-2.5 text-lg font-light text-white shadow-xs hover:bg-[var(--color-brand-700)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-brand-600)]"
>
{cta}
</a>
</motion.a>
</div>
</div>
</div>

View File

@@ -1,64 +0,0 @@
export interface Feature {
name: string
description: string
imageSrc: string
imageAlt: string
}
interface WithAlternatingSectionsProps {
features: Feature[]
title?: string
subtitle?: string
}
function classNames(...classes: (string | boolean | undefined)[]) {
return classes.filter(Boolean).join(' ')
}
export default function WithAlternatingSections({
features,
title = 'Nos services',
subtitle = 'Des solutions complètes en peinture et décoration pour transformer vos espaces.',
}: WithAlternatingSectionsProps) {
return (
<div className="bg-brand-light text-brand-600">
<div className="mx-auto max-w-2xl px-4 py-24 sm:px-6 sm:py-32 lg:max-w-7xl lg:px-8">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-4xl font-bold tracking-tight text-brand-600 sm:text-5xl">{title}</h2>
<p className="mt-4 text-lg text-brand-500">{subtitle}</p>
</div>
<div className="mt-16 space-y-16">
{features.map((feature, featureIdx) => (
<div
key={feature.name}
className="flex flex-col-reverse lg:grid lg:grid-cols-12 lg:items-center lg:gap-x-8"
>
<div
className={classNames(
featureIdx % 2 === 0 ? 'lg:col-start-1' : 'lg:col-start-8 xl:col-start-9',
'mt-6 lg:col-span-5 lg:row-start-1 lg:mt-0 xl:col-span-4',
)}
>
<h3 className="text-4xl font-semibold tracking-tight text-brand-600">{feature.name}</h3>
<p className="mt-2 text-base text-brand-800">{feature.description}</p>
</div>
<div
className={classNames(
featureIdx % 2 === 0 ? 'lg:col-start-6 xl:col-start-5' : 'lg:col-start-1',
'flex-auto lg:col-span-7 lg:row-start-1 xl:col-span-8',
)}
>
<img
alt={feature.imageAlt}
src={feature.imageSrc}
className="aspect-3/2 w-full rounded-lg bg-gray-100 object-cover"
/>
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -5,6 +5,18 @@ interface ContactHeroContent {
subtitle: string;
}
import { motion } from "motion/react";
const fadeUp = (delay: number) => ({
initial: { opacity: 0, y: 18 },
animate: { opacity: 1, y: 0 },
transition: {
duration: 0.6,
ease: [0.22, 1, 0.36, 1] as const,
delay,
},
});
export default function ContactHero({ content }: { content: ContactHeroContent }) {
const { imageSrc, imageAlt, title, subtitle } = content;
@@ -29,12 +41,19 @@ interface ContactHeroContent {
className="relative mx-auto flex max-w-7xl flex-col items-center justify-center text-center sm:px-6 lg:px-8"
>
<div className="mx-auto max-w-2xl lg:max-w-none">
<h2 id="sale-heading" className="text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
<motion.h2
{...fadeUp(0.15)}
id="sale-heading"
className="text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl"
>
{title}
</h2>
<p className="mx-auto mt-4 max-w-xl text-xl text-white">
</motion.h2>
<motion.p
{...fadeUp(0.3)}
className="mx-auto mt-4 max-w-xl text-xl text-white"
>
{subtitle}
</p>
</motion.p>
</div>
</section>

View File

@@ -1,86 +0,0 @@
interface TestimonialItem {
key: string;
src: string;
height: number;
}
interface Props {
sectionLabel: string;
title: string;
items: TestimonialItem[];
}
const iframeProps = {
width: 500,
scrolling: 'no',
frameBorder: 0,
allowFullScreen: true,
allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share',
style: { border: 'none', overflow: 'hidden' } as React.CSSProperties,
className: 'w-full rounded-2xl',
}
export default function Grid({ sectionLabel, title, items }: Props) {
const [featured, ...rest] = items
const groups = [rest.slice(0, 2), rest.slice(2)]
return (
<div className="relative isolate bg-brand-50 pt-24 pb-32 sm:pt-32 dark:bg-brand-50">
<div
aria-hidden="true"
className="absolute inset-x-0 top-1/2 -z-10 -translate-y-1/2 transform-gpu overflow-hidden opacity-30 blur-3xl"
>
<div
style={{
clipPath:
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
}}
className="ml-[max(50%,38rem)] aspect-1313/771 w-328.25 bg-linear-to-tr from-[var(--color-brand-50)] to-[var(--color-brand-200)]"
/>
</div>
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 -z-10 flex transform-gpu overflow-hidden pt-32 opacity-25 blur-3xl sm:pt-40 xl:justify-end"
>
<div
style={{
clipPath:
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
}}
className="-ml-88 aspect-1313/771 w-328.25 flex-none origin-top-right rotate-30 bg-linear-to-tr from-[var(--color-brand-200)] to-[var(--color-brand-50)] xl:mr-[calc(50%-12rem)] xl:ml-0"
/>
</div>
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-base/7 font-semibold text-brand-400 dark:text-brand-400">{sectionLabel}</h2>
<p className="mt-2 text-4xl font-semibold tracking-tight text-balance text-brand-600 sm:text-5xl dark:text-white">
{title}
</p>
</div>
<div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 grid-rows-1 gap-8 text-sm/6 text-gray-900 sm:mt-20 sm:grid-cols-2 xl:mx-0 xl:max-w-none xl:grid-flow-col xl:grid-cols-4 dark:text-gray-100">
<figure className="rounded-2xl bg-white shadow-lg ring-1 ring-gray-900/5 sm:col-span-2 xl:col-start-2 xl:row-end-1 dark:bg-gray-800/75 dark:shadow-none dark:ring-white/10">
<iframe {...iframeProps} src={featured.src} height={featured.height} />
</figure>
{groups.map((group, groupIdx) => (
<div key={groupIdx} className="space-y-8 xl:contents xl:space-y-0">
{group.map((t, colIdx) => {
const isFirstCol = groupIdx === 0 && colIdx === 0
const isLastCol = groupIdx === groups.length - 1 && colIdx === group.length - 1
return (
<div
key={t.key}
className={(isFirstCol || isLastCol) ? 'xl:row-span-2 space-y-8' : 'xl:row-start-1 space-y-8'}
>
<figure className="rounded-2xl bg-white shadow-lg ring-1 ring-gray-900/5 dark:bg-gray-800/75 dark:shadow-none dark:ring-white/10">
<iframe {...iframeProps} src={t.src} height={t.height} />
</figure>
</div>
)
})}
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -1,122 +0,0 @@
const iconPaths: Record<string, string> = {
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
clock: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z M12 6v6l4 2',
check: 'M20 6L9 17l-5-5',
team:
'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
location: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z M15 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0z',
}
function getIconPath(icon: string): string {
return iconPaths[icon] ?? iconPaths.check
}
export interface PourquoiCard {
icon: string
title: string
description: string
}
export interface PourquoiImages {
main: string
mainAlt: string
detail1: string
detail1Alt: string
detail2: string
detail2Alt: string
}
interface WithTieredImagesProps {
title: string
subtitle: string
cards: PourquoiCard[]
images: PourquoiImages
}
export default function WithTieredImages({
title,
subtitle,
cards,
images,
}: WithTieredImagesProps) {
return (
<div className="bg-[var(--color-brand-600)]">
<div className="mx-auto max-w-2xl px-4 py-24 sm:px-6 sm:py-32 lg:max-w-7xl lg:px-8">
<div className="grid grid-cols-1 items-center gap-x-8 gap-y-16 lg:grid-cols-2">
<div>
<div className="border-b border-white/30 pb-10">
<h2 className="text-lg font-medium text-white">{title}</h2>
<p className="mt-2 text-4xl font-bold tracking-tight text-white sm:text-5xl">
{subtitle}
</p>
</div>
<dl className="mt-10 space-y-10">
{cards.map((card) => (
<div key={card.title} className="flex gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-white text-[var(--color-brand-600)]">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
{card.icon === 'clock' ? (
<>
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</>
) : card.icon === 'team' ? (
<>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</>
) : card.icon === 'location' ? (
<>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</>
) : (
<path d={getIconPath(card.icon)} />
)}
</svg>
</div>
<div>
<dt className="text-base font-medium text-white">{card.title}</dt>
<dd className="mt-3 text-base text-white/90">{card.description}</dd>
</div>
</div>
))}
</dl>
</div>
<div>
<img
alt={images.mainAlt}
src={images.main}
className="aspect-square w-full rounded-lg bg-[var(--color-brand-100)] object-cover"
/>
<div className="mt-4 grid grid-cols-2 gap-4 sm:mt-6 sm:gap-6 lg:mt-8 lg:gap-8">
<img
alt={images.detail1Alt}
src={images.detail1}
className="aspect-square w-full rounded-lg bg-[var(--color-brand-100)] object-cover"
/>
<img
alt={images.detail2Alt}
src={images.detail2}
className="aspect-square w-full rounded-lg bg-[var(--color-brand-100)] object-cover"
/>
</div>
</div>
</div>
</div>
</div>
)
}