initial commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import imgInterieure from '@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp'
|
||||
import imgExterieure from '@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp'
|
||||
import imgDecoration from '@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg'
|
||||
import imgCommerciale from '@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp'
|
||||
import imgRenovation from '@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp'
|
||||
|
||||
export default function TwoRowBentoGridWithThreeColumnSecondRow() {
|
||||
return (
|
||||
<div className="bg-[var(--color-brand-600)] py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
|
||||
<h2
|
||||
className="text-base/7 font-semibold text-white"
|
||||
style={{ fontFamily: 'var(--font-body)' }}
|
||||
>
|
||||
Nos Services
|
||||
</h2>
|
||||
<p
|
||||
className="mt-2 max-w-lg text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl"
|
||||
style={{ fontFamily: 'var(--font-display)' }}
|
||||
>
|
||||
Des solutions complètes en peinture et décoration
|
||||
</p>
|
||||
<div className="mt-10 grid grid-cols-1 gap-4 sm:mt-16 lg:grid-cols-6 lg:grid-rows-2">
|
||||
|
||||
{/* Peinture intérieure — top left */}
|
||||
<div className="relative lg:col-span-3">
|
||||
<div className="absolute inset-0 rounded-xl bg-white max-lg:rounded-t-xl lg:rounded-tl-xl" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl max-lg:rounded-t-xl lg:rounded-tl-xl">
|
||||
<img
|
||||
alt="Peinture intérieure résidentielle"
|
||||
src={imgInterieure.src}
|
||||
className="h-80 object-cover object-left"
|
||||
/>
|
||||
<div className="p-10 pt-4">
|
||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
||||
Peinture intérieure
|
||||
</p>
|
||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
||||
Transformation complète de vos espaces intérieurs. Préparation des surfaces, application soignée et finitions impeccables pour tous types de pièces.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 max-lg:rounded-t-xl lg:rounded-tl-xl" />
|
||||
</div>
|
||||
|
||||
{/* Peinture extérieure — top right */}
|
||||
<div className="relative lg:col-span-3">
|
||||
<div className="absolute inset-0 rounded-xl bg-white lg:rounded-tr-xl" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl lg:rounded-tr-xl">
|
||||
<img
|
||||
alt="Peinture extérieure résidentielle"
|
||||
src={imgExterieure.src}
|
||||
className="h-80 object-cover object-left lg:object-right"
|
||||
/>
|
||||
<div className="p-10 pt-4">
|
||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
||||
Peinture extérieure
|
||||
</p>
|
||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
||||
Protection et embellissement de la façade de votre maison. Travaux résistants aux conditions climatiques du Québec pour une durabilité maximale.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 lg:rounded-tr-xl" />
|
||||
</div>
|
||||
|
||||
{/* Décoration intérieure — bottom left */}
|
||||
<div className="relative lg:col-span-2">
|
||||
<div className="absolute inset-0 rounded-xl bg-white lg:rounded-bl-xl" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl lg:rounded-bl-xl">
|
||||
<img
|
||||
alt="Décoration intérieure"
|
||||
src={imgDecoration.src}
|
||||
className="h-80 object-cover object-left"
|
||||
/>
|
||||
<div className="p-10 pt-4">
|
||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
||||
Décoration intérieure
|
||||
</p>
|
||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
||||
Conseils personnalisés en couleur et décoration. Effets décoratifs, murales, textures et finitions spécialisées.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 lg:rounded-bl-xl" />
|
||||
</div>
|
||||
|
||||
{/* Peinture commerciale — bottom center */}
|
||||
<div className="relative lg:col-span-2">
|
||||
<div className="absolute inset-0 rounded-xl bg-white" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl">
|
||||
<img
|
||||
alt="Peinture commerciale"
|
||||
src={imgCommerciale.src}
|
||||
className="h-80 object-cover"
|
||||
/>
|
||||
<div className="p-10 pt-4">
|
||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Commercial</h3>
|
||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
||||
Peinture commerciale
|
||||
</p>
|
||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
||||
Bureaux, commerces et espaces institutionnels. Travaux planifiés selon votre calendrier pour minimiser les perturbations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5" />
|
||||
</div>
|
||||
|
||||
{/* Rénovation complète — bottom right */}
|
||||
<div className="relative lg:col-span-2">
|
||||
<div className="absolute inset-0 rounded-xl bg-white max-lg:rounded-b-xl lg:rounded-br-xl" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl max-lg:rounded-b-xl lg:rounded-br-xl">
|
||||
<img
|
||||
alt="Rénovation complète"
|
||||
src={imgRenovation.src}
|
||||
className="h-80 object-cover"
|
||||
/>
|
||||
<div className="p-10 pt-4">
|
||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
||||
Rénovation complète
|
||||
</p>
|
||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
||||
Coordination complète de vos travaux de finition. De la peinture aux moulures, un seul entrepreneur pour tous vos projets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 max-lg:rounded-b-xl lg:rounded-br-xl" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
240
src/components/ui/carousel.tsx
Normal file
240
src/components/ui/carousel.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
useCarousel,
|
||||
}
|
||||
64
src/components/ui/cta-sections/WithImageTiles.tsx
Normal file
64
src/components/ui/cta-sections/WithImageTiles.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
export default function WithImageTiles() {
|
||||
return (
|
||||
<div className="overflow-hidden py-32" style={{ backgroundColor: 'var(--color-brand-600)' }}>
|
||||
<div className="mx-auto max-w-7xl px-6 lg:flex lg:px-8">
|
||||
<div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-12 gap-y-16 lg:mx-0 lg:max-w-none lg:min-w-full lg:flex-none lg:gap-y-8">
|
||||
<div className="lg:col-end-1 lg:w-full lg:max-w-lg lg:pb-8">
|
||||
<h2 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
|
||||
Prêt à transformer votre espace?
|
||||
</h2>
|
||||
<p className="mt-6 text-xl/8 text-green-100">
|
||||
Obtenez une soumission gratuite et sans engagement dès aujourd'hui.
|
||||
</p>
|
||||
<p className="mt-6 text-base/7 text-green-200">
|
||||
Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale,
|
||||
décoration intérieure et extérieure.
|
||||
</p>
|
||||
<div className="mt-10 flex">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
style={{ color: 'var(--color-brand-600)' }}
|
||||
>
|
||||
Demandez votre soumission
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-start justify-end gap-6 sm:gap-8 lg:contents">
|
||||
<div className="w-0 flex-auto lg:ml-auto lg:w-auto lg:flex-none lg:self-end">
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1670272502246-768d249768ca?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1152&q=80"
|
||||
className="aspect-7/5 w-148 max-w-none rounded-2xl object-cover max-sm:w-120" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="contents lg:col-span-2 lg:col-end-2 lg:ml-auto lg:flex lg:w-148 lg:items-start lg:justify-end lg:gap-x-8">
|
||||
<div className="order-first flex w-64 flex-none justify-end self-end max-sm:w-40 lg:w-auto">
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1605656816944-971cd5c1407f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=768&h=604&q=80"
|
||||
className="aspect-4/3 w-[24rem] max-w-none flex-none rounded-2xl object-cover" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-96 flex-auto justify-end lg:w-auto lg:flex-none">
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1568992687947-868a62a9f521?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1152&h=842&q=80"
|
||||
className="aspect-7/5 w-148 max-w-none flex-none rounded-2xl object-cover max-sm:w-120" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:w-0 sm:flex-auto lg:w-auto lg:flex-none">
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1612872087720-bb876e2e67d1?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=768&h=604&q=80"
|
||||
className="aspect-4/3 w-[24rem] max-w-none rounded-2xl object-cover" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
193
src/components/ui/heroes/SimpleBackgroundVideo.tsx
Normal file
193
src/components/ui/heroes/SimpleBackgroundVideo.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import { Dialog, DialogPanel } from '@headlessui/react'
|
||||
import heroVideoMp4 from '@/assets/videos/cachet-deco.mp4'
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { defaultLocale } from '../../../i18n/config'
|
||||
import { t } from '../../../i18n/utils'
|
||||
import general from '../../../content/settings/general.json'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Landing 1', href: '/landing-1' },
|
||||
{ name: 'Landing 2', href: '/landing-2' },
|
||||
{ name: 'Landing 3', href: '/landing-3' },
|
||||
]
|
||||
|
||||
export default function SimpleCenteredWithBackgroundImage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-900">
|
||||
<header className="absolute inset-x-0 top-0 z-50 bg-[var(--color-brand-600)]">
|
||||
<nav aria-label="Global" className="flex items-center justify-between p-2 lg:px-8">
|
||||
<div className="flex lg:flex-1">
|
||||
<a href="/" className="-m-1.5 p-1 m-1 bg-[white] rounded-full">
|
||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
||||
<img
|
||||
alt="Cachet Peintres Décorateurs"
|
||||
src="/images/logo.png"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-white"
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<Bars3Icon aria-hidden="true" className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:gap-x-12">
|
||||
{navigation.map((item) => (
|
||||
<a key={item.name} href={item.href} className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2">
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-semibold text-[var(--color-brand-600)] shadow-sm ring-1 ring-inset ring-white/30 hover:bg-white/90 transition-colors"
|
||||
>
|
||||
Soumission gratuite
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen} className="lg:hidden">
|
||||
<div className="fixed inset-0 z-50" />
|
||||
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<a href="/" className="-m-1.5 p-1.5">
|
||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
||||
<img
|
||||
alt="Cachet Peintres Décorateurs"
|
||||
src="/images/logo.png"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<XMarkIcon aria-hidden="true" className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 flow-root">
|
||||
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
||||
<div className="space-y-2 py-6">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="py-6">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-[var(--color-brand-600)] text-white ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)]"
|
||||
>
|
||||
Soumission gratuite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
</header>
|
||||
|
||||
<div className="relative isolate min-h-screen overflow-hidden pt-14">
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="absolute inset-0 -z-10 size-full object-cover"
|
||||
>
|
||||
<source src={heroVideoMp4} type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 -z-10 bg-black/50" />
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div className="grid min-h-[calc(100vh-3.5rem)] grid-cols-1 items-center gap-12 py-20 lg:grid-cols-[1fr_auto] lg:gap-12 lg:py-24">
|
||||
<div>
|
||||
<p className="mb-4 inline-block text-sm font-bold uppercase tracking-wider text-white/65">
|
||||
{general.tagline}
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-medium leading-tight text-white sm:text-5xl lg:text-[2.5rem]" style={{ fontFamily: 'var(--font-display)' }}>
|
||||
{t(defaultLocale, 'hero.title')}
|
||||
</h1>
|
||||
<p className="mb-9 max-w-[540px] text-xl leading-relaxed text-white/82">
|
||||
{t(defaultLocale, 'hero.subtitle')}
|
||||
</p>
|
||||
<div className="mb-8 flex flex-wrap gap-4">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-white px-8 py-3.5 text-lg font-semibold text-[var(--color-brand-600)] transition-colors hover:bg-[var(--color-brand-50)]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14,2 14,8 20,8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10,9 9,9 8,9" />
|
||||
</svg>
|
||||
{t(defaultLocale, 'hero.cta')}
|
||||
</a>
|
||||
<a
|
||||
href="/services"
|
||||
className="inline-flex items-center gap-2 rounded-md border-2 border-white px-6 py-3 text-base font-semibold text-white transition-colors hover:bg-white/10"
|
||||
>
|
||||
{t(defaultLocale, 'cta.learn_more')}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12,5 19,12 12,19" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 gap-x-3.5 text-sm text-white/65">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
<span>RBQ {general.rbq}</span>
|
||||
</div>
|
||||
<span className="text-white/30" aria-hidden>·</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<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>
|
||||
<span>{general.serviceArea}</span>
|
||||
</div>
|
||||
<span className="text-white/30" aria-hidden>·</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<span>Soumission gratuite</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden items-center justify-center lg:flex" aria-hidden>
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
alt=""
|
||||
width={320}
|
||||
height={320}
|
||||
loading="eager"
|
||||
className="h-[280px] w-[280px] object-contain opacity-[0.18] brightness-0 invert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
215
src/components/ui/heroes/SimpleCenteredWithImageSlider.tsx
Normal file
215
src/components/ui/heroes/SimpleCenteredWithImageSlider.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogPanel } from "@headlessui/react";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import { defaultLocale } from "../../../i18n/config";
|
||||
import { t } from "../../../i18n/utils";
|
||||
import general from "../../../content/settings/general.json";
|
||||
import photo1 from "@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp";
|
||||
import photo2 from "@/assets/photos/the-attic-of-a-home-with-white-ceilings-and-white-2023-11-27-05-27-44-utc.webp";
|
||||
import photo3 from "@/assets/photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp";
|
||||
import photo4 from "@/assets/photos/a-bedroom-with-white-walls-and-a-bed-and-a-2023-11-27-05-25-30-utc.webp";
|
||||
import photo5 from "@/assets/photos/empty-room-with-window-2023-11-27-05-08-12-utc.webp";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Landing 1", href: "/landing-1" },
|
||||
{ name: "Landing 2", href: "/landing-2" },
|
||||
{ name: "Landing 3", href: "/landing-3" },
|
||||
|
||||
];
|
||||
|
||||
const slides = [
|
||||
{ src: photo1.src, alt: "Peinture intérieure" },
|
||||
{ src: photo2.src, alt: "Décoration murale" },
|
||||
{ src: photo3.src, alt: "Finition de qualité" },
|
||||
{ src: photo4.src, alt: "Chambre" },
|
||||
{ src: photo5.src, alt: "Pièce lumineuse" },
|
||||
];
|
||||
|
||||
const autoplayPlugin = Autoplay({
|
||||
delay: 5000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: false,
|
||||
});
|
||||
|
||||
export default function SimpleCenteredWithImageSlider() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-900">
|
||||
<header className="absolute inset-x-0 top-0 z-50">
|
||||
<nav
|
||||
aria-label="Global"
|
||||
className="flex items-center justify-between p-6 lg:px-8"
|
||||
>
|
||||
<div className="flex lg:flex-1">
|
||||
<a href="/" className="flex items-center bg-[white] rounded-3xl shadow-md
|
||||
ps-0.5 pe-2 pt-1 pb-0.5
|
||||
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
||||
|
||||
">
|
||||
<div className="p-1 bg-[white] rounded-full ">
|
||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
||||
<img
|
||||
alt="Cachet Peintres Décorateurs"
|
||||
src="/images/logo.png"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">{general.brandName}</p>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-[var(--color-brand-100)] hover:text-[var(--color-brand-300)]"
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<Bars3Icon aria-hidden="true" className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:gap-x-12">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="rounded-md bg-[var(--color-brand-600)] px-4 py-2 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] transition-colors"
|
||||
>
|
||||
Soumission gratuite
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<Dialog
|
||||
open={mobileMenuOpen}
|
||||
onClose={setMobileMenuOpen}
|
||||
className="lg:hidden"
|
||||
>
|
||||
<div className="fixed inset-0 z-50" />
|
||||
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
||||
<div className="flex items-center justify-between sm:justify-end lg:justify-between">
|
||||
<a href="/" className="flex sm:hidden lg:flex items-center bg-[white] rounded-3xl
|
||||
ps-0.5 pe-2 pt-1 pb-0.5
|
||||
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
||||
|
||||
">
|
||||
<div className="p-1 bg-[white] rounded-full ">
|
||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
||||
<img
|
||||
alt="Cachet Peintres Décorateurs"
|
||||
src="/images/logo.png"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">{general.brandName}</p>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<XMarkIcon aria-hidden="true" className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 flow-root">
|
||||
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
||||
<div className="space-y-2 py-6">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="py-6">
|
||||
<a
|
||||
href="/soumission"
|
||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-[var(--color-brand-600)] text-white ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)]"
|
||||
>
|
||||
Soumission gratuite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
</header>
|
||||
|
||||
{/* Full-screen hero with background carousel */}
|
||||
<div className="relative isolate overflow-hidden h-screen min-h-[600px]">
|
||||
{/* Background carousel */}
|
||||
<Carousel
|
||||
className="absolute inset-0 -z-10 size-full [&_[data-slot='carousel-content']]:h-full"
|
||||
opts={{ loop: true, dragFree: false }}
|
||||
plugins={[autoplayPlugin]}
|
||||
>
|
||||
<CarouselContent className="-ml-0 h-full">
|
||||
{slides.map((slide, index) => (
|
||||
<CarouselItem key={index} className="pl-0 h-full relative">
|
||||
<img
|
||||
src={slide.src}
|
||||
alt={slide.alt}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
{/* Dark overlay for text legibility */}
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
{/* Hero text content */}
|
||||
<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-2xl 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>Soumission gratuite</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1
|
||||
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-7xl"
|
||||
style={{ fontFamily: "var(--font-display)" }}
|
||||
>
|
||||
{t(defaultLocale, "hero.title")}
|
||||
</h1>
|
||||
<p className="mt-8 text-lg font-medium text-pretty text-gray-300 sm:text-xl/8">
|
||||
{t(defaultLocale, "hero.subtitle")}
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center">
|
||||
<a
|
||||
href="/soumission"
|
||||
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)]"
|
||||
>
|
||||
{t(defaultLocale, "hero.cta")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
63
src/components/ui/product-features/WithFadingImage.tsx
Normal file
63
src/components/ui/product-features/WithFadingImage.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import imgBg from '@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp'
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: 'Certifié RBQ',
|
||||
description: 'Entrepreneur certifié par la Régie du Bâtiment du Québec (RBQ : 5839 8736 01). Travaux assurés et conformes aux normes.',
|
||||
},
|
||||
{
|
||||
name: 'Ponctualité et fiabilité',
|
||||
description: 'Nous respectons nos engagements. Les délais sont tenus et votre projet est livré à temps, selon les standards convenus.',
|
||||
},
|
||||
{
|
||||
name: 'Matériaux de qualité',
|
||||
description: 'Nous utilisons exclusivement des peintures et produits de première qualité pour un résultat durable et esthétique.',
|
||||
},
|
||||
{
|
||||
name: 'Équipe professionnelle',
|
||||
description: 'Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition.',
|
||||
},
|
||||
]
|
||||
|
||||
export default function WithFadingImage() {
|
||||
return (
|
||||
<div className="bg-[linear-gradient(to_bottom,white_0%,white_75%,var(--color-brand-50)_100%)]">
|
||||
<div aria-hidden="true" className="relative">
|
||||
<img
|
||||
alt="Peintres décorateurs au travail"
|
||||
src={imgBg.src}
|
||||
className="h-96 w-full object-cover object-center "
|
||||
/>
|
||||
<div className="absolute inset-0 bg-linear-to-t from-white" />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto -mt-12 max-w-7xl px-4 pb-16 sm:px-6 sm:pb-24 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl text-center lg:max-w-4xl">
|
||||
<h2
|
||||
className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"
|
||||
style={{ fontFamily: 'var(--font-display)' }}
|
||||
>
|
||||
Pourquoi choisir Cachet?
|
||||
</h2>
|
||||
<p className="mt-4 text-gray-500" style={{ fontFamily: 'var(--font-body)' }}>
|
||||
Une expertise reconnue au service de votre satisfaction
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 sm:gap-y-16 lg:max-w-none lg:grid-cols-4 lg:gap-x-8">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.name} className="border-t-2 border-[var(--color-brand-500)] pt-4">
|
||||
<dt
|
||||
className="font-semibold text-[var(--color-gray-600)]"
|
||||
style={{ fontFamily: 'var(--font-body)' }}
|
||||
>
|
||||
{feature.name}
|
||||
</dt>
|
||||
<dd className="mt-2 text-sm text-gray-500 leading-relaxed">{feature.description}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
122
src/components/ui/product-features/WithTieredImages.tsx
Normal file
122
src/components/ui/product-features/WithTieredImages.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
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 PorquoiCard {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PorquoiImages {
|
||||
main: string
|
||||
mainAlt: string
|
||||
detail1: string
|
||||
detail1Alt: string
|
||||
detail2: string
|
||||
detail2Alt: string
|
||||
}
|
||||
|
||||
interface WithTieredImagesProps {
|
||||
title: string
|
||||
subtitle: string
|
||||
cards: PorquoiCard[]
|
||||
images: PorquoiImages
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
interface FullWidthWithBackgroundImageAndLargeContentProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonText: string;
|
||||
buttonHref: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
export default function FullWidthWithBackgroundImageAndLargeContent({
|
||||
title,
|
||||
subtitle,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
imageSrc,
|
||||
imageAlt = '',
|
||||
}: FullWidthWithBackgroundImageAndLargeContentProps) {
|
||||
return (
|
||||
<section className="bg-white" aria-labelledby="cta-heading">
|
||||
<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
|
||||
alt={imageAlt}
|
||||
src={imageSrc}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-brand-50/95 sm:text-xl">
|
||||
{subtitle}
|
||||
</p>
|
||||
<a
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
124
src/components/ui/testimonials/Grid.tsx
Normal file
124
src/components/ui/testimonials/Grid.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
const featuredTestimonial = {
|
||||
body: 'Integer id nunc sit semper purus. Bibendum at lacus ut arcu blandit montes vitae auctor libero. Hac condimentum dignissim nibh vulputate ut nunc. Amet nibh orci mi venenatis blandit vel et proin. Non hendrerit in vel ac diam.',
|
||||
author: {
|
||||
name: 'Brenna Goyette',
|
||||
handle: 'brennagoyette',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=1024&h=1024&q=80',
|
||||
logoUrl: 'https://tailwindcss.com/plus-assets/img/logos/savvycal-logo-gray-900.svg',
|
||||
},
|
||||
}
|
||||
const testimonials = [
|
||||
[
|
||||
[
|
||||
{
|
||||
iframe: <iframe className="w-full h-full rounded-2xl " src="https://www.facebook.com/plugins/post.php?href=https%3A%2F%2Fwww.facebook.com%2Fjean.christophe.ali.2025%2Fposts%2Fpfbid02qsqq2Rn4vRx1gCG3q5e5M2ByPXH6zMux59fWvotfsQxpuWyUh46TZuwCND9iKpwnl&show_text=true&width=500" width="500" height="194" style={{ border: 'none', overflow: 'hidden' }} scrolling="no" frameBorder={0} allowFullScreen allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>,
|
||||
key: 'jean-christophe-ali',
|
||||
}
|
||||
],
|
||||
[{
|
||||
iframe: <iframe className="w-full h-full rounded-2xl " src="https://www.facebook.com/plugins/post.php?href=https%3A%2F%2Fwww.facebook.com%2Fcharlotte.lafrance.CL%2Fposts%2Fpfbid026Jw7aer2r84q27kgcZ1PKg8q2sqKgZc6nJspP42CTEAU6ac8FF4Ptj8ap8XGh6Wl&show_text=true&width=500" width="500" height="166" style={{ border: 'none', overflow: 'hidden' }} scrolling="no" frameBorder={0} allowFullScreen allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>,
|
||||
key: 'charlotte-lafrance',
|
||||
}
|
||||
],
|
||||
],
|
||||
[
|
||||
[{
|
||||
iframe: <iframe className="w-full h-full rounded-2xl " src="https://www.facebook.com/plugins/post.php?href=https%3A%2F%2Fwww.facebook.com%2Fcolin.griffin.165%2Fposts%2Fpfbid0iCA6hoF5X32gUJSBcDUiXP2hxhxzMmEiC5Att3CDDwFckrHa7yxyGiLBmXQhQBUFl&show_text=true&width=500" width="500" height="222" style={{ border: 'none', overflow: 'hidden' }} scrolling="no" frameBorder={0} allowFullScreen allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe> ,
|
||||
key: 'colin-griffin',
|
||||
}
|
||||
],
|
||||
[ {
|
||||
iframe: <iframe className="w-full h-full rounded-2xl" src="https://www.facebook.com/plugins/post.php?href=https%3A%2F%2Fwww.facebook.com%2Fhollowman.la.rock%2Fposts%2Fpfbid0hPR3RpaSLZtZkgpaEzKsNKAF4hUeKbDzkicAZ9jsbCpXKRRsYD63C32rw78uvRoml&show_text=true&width=500" width="500" height="170" style={{ border: 'none', overflow: 'hidden' }} scrolling="no" frameBorder={0} allowFullScreen allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>,
|
||||
key: 'hollowman-la-rock',
|
||||
}
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default function Grid() {
|
||||
return (
|
||||
<div className="relative isolate bg-brand-50 pt-24 pb-32 sm:pt-32 dark:bg-gray-900">
|
||||
<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">Témoignages</h2>
|
||||
<p className="mt-2 text-4xl font-semibold tracking-tight text-balance text-brand-600 sm:text-5xl dark:text-white">
|
||||
Se que nos clients disent de nous
|
||||
</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 className="w-full h-full rounded-2xl " src="https://www.facebook.com/plugins/post.php?href=https%3A%2F%2Fwww.facebook.com%2Fsylvie.gauthier.73594%2Fposts%2Fpfbid0DHM1833TLKru4cbkTnZq3KLNHrSR8B8rQAgGosGzAbpGMccvLffoNmCLgEo8GUNPl&show_text=true&width=500" width="500" height="208" style={{ border: 'none', overflow: 'hidden' }} scrolling="no" frameBorder={0} allowFullScreen allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>
|
||||
</figure>
|
||||
{testimonials.map((columnGroup, columnGroupIdx) => (
|
||||
<div key={columnGroupIdx} className="space-y-8 xl:contents xl:space-y-0">
|
||||
{columnGroup.map((column, columnIdx) => (
|
||||
<div
|
||||
key={columnIdx}
|
||||
className={classNames(
|
||||
(columnGroupIdx === 0 && columnIdx === 0) ||
|
||||
(columnGroupIdx === testimonials.length - 1 && columnIdx === columnGroup.length - 1)
|
||||
? 'xl:row-span-2'
|
||||
: 'xl:row-start-1',
|
||||
'space-y-8',
|
||||
)}
|
||||
>
|
||||
{column.map((testimonial) => (
|
||||
<figure
|
||||
key={testimonial.key}
|
||||
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"
|
||||
>
|
||||
{testimonial.iframe}
|
||||
{/* <blockquote className="text-gray-900 dark:text-white">
|
||||
<p>{`“${testimonial.body}”`}</p>
|
||||
</blockquote>
|
||||
<figcaption className="mt-6 flex items-center gap-x-4">
|
||||
<img
|
||||
alt=""
|
||||
src={testimonial.author.imageUrl}
|
||||
className="size-10 rounded-full bg-gray-50 dark:bg-gray-700"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">{testimonial.author.name}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">{`@${testimonial.author.handle}`}</div>
|
||||
</div>
|
||||
</figcaption> */}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user