This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
292
src/components/ui/footer-animated.tsx
Normal file
292
src/components/ui/footer-animated.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user