cleanup and cms migration
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-03-23 20:45:24 -04:00
parent 2ba6f43489
commit 5121ba1852
47 changed files with 1275 additions and 3411 deletions

View File

@@ -1,93 +0,0 @@
---
import { getLangFromUrl, t } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
---
<section class="cta-banner-section">
<div class="cta-banner-bg" aria-hidden="true"></div>
<div class="container-site cta-banner-inner">
<div class="cta-banner-text">
<h2 class="cta-banner-title">{t(lang, 'cta_banner.title')}</h2>
<p class="cta-banner-subtitle">{t(lang, 'cta_banner.subtitle')}</p>
</div>
<div class="cta-banner-action">
<a href="/soumission" class="btn-primary cta-banner-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<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"/>
</svg>
{t(lang, 'cta_banner.button')}
</a>
</div>
</div>
</section>
<style>
.cta-banner-section {
position: relative;
background-color: var(--color-brand-50);
color: var(--color-brand-600);
padding: 4rem 0;
overflow: hidden;
}
.cta-banner-bg {
position: absolute;
inset: 0;
/* background: linear-gradient(135deg, var(--color-brand-700) 0%, var(--color-brand-600) 50%, var(--color-brand-500) 100%); */
background-color: var(--color-brand-50);
opacity: 0.6;
pointer-events: none;
}
.cta-banner-inner {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
flex-wrap: wrap;
}
.cta-banner-title {
font-family: var(--font-display);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 500;
color: var(--color-brand-600);
margin-bottom: 0.5rem;
}
.cta-banner-subtitle {
font-size: 1.0625rem;
color: var(--color-brand-500);
max-width: 520px;
}
.cta-banner-btn {
background-color: var(--color-brand-600);
color: #ffffff;
font-size: 1rem;
padding: 0.875rem 2rem;
white-space: nowrap;
flex-shrink: 0;
}
.cta-banner-btn:hover {
background-color: var(--color-brand-700);
}
@media (max-width: 640px) {
.cta-banner-inner {
flex-direction: column;
align-items: flex-start;
}
.cta-banner-btn {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -1,42 +1,36 @@
---
import { getLangFromUrl, t } from '../i18n/utils';
import { getCollection } from 'astro:content';
import { getLocale } from '@/lib/locale';
import general from '../content/settings/general.json';
const lang = getLangFromUrl(Astro.url);
const locale = getLocale(Astro);
const [footerEntry] = await getCollection('footer', (e) => e.id === `${locale}/main`);
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
if (!footerEntry || !navEntry) throw new Error(`Missing footer or navigation for locale: ${locale}`);
const footer = footerEntry.data;
const navLinks = navEntry.data.links;
const year = new Date().getFullYear();
const navLinks = [
{ href: '/', label: t(lang, 'nav.accueil') },
{ href: '/services', label: t(lang, 'nav.services') },
{ href: '/contact', label: t(lang, 'nav.contact') },
{ href: '/soumission', label: t(lang, 'nav.soumission') },
];
const serviceLinks = [
{ label: 'Peinture intérieure', href: '/services#peinture-interieure' },
{ label: 'Peinture extérieure', href: '/services#peinture-exterieure' },
{ label: 'Peinture commerciale', href: '/services#peinture-commerciale' },
{ label: 'Décoration intérieure', href: '/services#decoration-interieure' },
];
const legalNotice = t(lang, 'footer.legal').replace('{year}', String(year));
const legalNotice = footer.legal.replace('{year}', String(year));
---
<footer class="site-footer">
<div class="container-site footer-grid">
<!-- Brand column -->
<div class="footer-brand">
<a href="/" aria-label="Cachet Peintres Décorateurs">
<footer class="bg-brand-600 pt-14 px-8 text-white/85">
<div
class="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_1fr_1fr_1fr] min-[901px]:gap-12"
>
<div>
<a href="/" aria-label={general.siteName}>
<img
src="/images/logo.png"
alt="Cachet Peintres Décorateurs"
alt={general.siteName}
width="140"
height="52"
loading="lazy"
class="mb-4 block h-11 w-auto brightness-0 invert"
/>
</a>
<p class="footer-tagline">{t(lang, 'footer.tagline')}</p>
<p class="footer-service-area">
<p class="mb-2 text-sm font-normal text-white/75">{footer.tagline}</p>
<p class="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" stroke-width="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"/>
@@ -46,62 +40,65 @@ const legalNotice = t(lang, 'footer.legal').replace('{year}', String(year));
{general.facebook && (
<a
href={general.facebook}
class="footer-social"
class="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="Facebook"
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>
Facebook
{footer.facebookLabel}
</a>
)}
</div>
<!-- Navigation column -->
<div class="footer-col">
<h3 class="footer-col-title">{t(lang, 'footer.nav.title')}</h3>
<ul>
{navLinks.map(link => (
<li><a href={link.href} class="footer-link">{link.label}</a></li>
<div>
<h3 class="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 class="m-0 flex list-none flex-col gap-2.5 p-0">
{navLinks.map((link) => (
<li><a href={link.href} class="text-[0.9375rem] text-white/80 transition-colors hover:text-white">{link.name}</a></li>
))}
</ul>
</div>
<!-- Services column -->
<div class="footer-col">
<h3 class="footer-col-title">{t(lang, 'footer.services.title')}</h3>
<ul>
{serviceLinks.map(link => (
<li><a href={link.href} class="footer-link">{link.label}</a></li>
<div>
<h3 class="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
{footer.servicesTitle}
</h3>
<ul class="m-0 flex list-none flex-col gap-2.5 p-0">
{footer.serviceLinks.map((link) => (
<li><a href={link.href} class="text-[0.9375rem] text-white/80 transition-colors hover:text-white">{link.label}</a></li>
))}
</ul>
</div>
<!-- Contact column -->
<div class="footer-col">
<h3 class="footer-col-title">{t(lang, 'footer.contact.title')}</h3>
<ul class="footer-contact-list">
<div>
<h3 class="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 class="m-0 flex list-none flex-col gap-3 p-0">
<li>
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="footer-link footer-contact-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white">
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>
{general.phone}
</a>
</li>
<li>
<a href={`mailto:${general.email}`} class="footer-link footer-contact-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<a href={`mailto:${general.email}`} class="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white">
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>
{general.email}
</a>
</li>
<li class="footer-contact-item footer-address">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<li class="flex items-start gap-2 text-[0.9375rem] text-white/70">
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>
@@ -111,169 +108,16 @@ const legalNotice = t(lang, 'footer.legal').replace('{year}', String(year));
</div>
</div>
<!-- Bottom bar -->
<div class="footer-bottom">
<div class="container-site footer-bottom-inner">
<p class="footer-copyright">{legalNotice}</p>
<p class="footer-legal-nums">
<span>{t(lang, 'footer.rbq')}</span>
<div class="border-t border-white/15 py-[1.125rem]">
<div
class="container-site flex flex-wrap items-center justify-between gap-4 max-[540px]:flex-col max-[540px]:items-start max-[540px]:gap-2"
>
<p class="text-[0.8125rem] text-white/50">{legalNotice}</p>
<p class="flex items-center gap-2.5 text-xs tracking-[0.02em] text-white/40">
<span>{footer.rbq}</span>
<span aria-hidden="true">·</span>
<span>{t(lang, 'footer.neq')}</span>
<span>{footer.neq}</span>
</p>
</div>
</div>
</footer>
<style>
.site-footer {
background-color: var(--color-brand-600);
color: rgba(255, 255, 255, 0.85);
padding-top: 3.5rem;
}
.footer-grid {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
gap: 3rem;
padding-bottom: 3rem;
}
.footer-brand img {
height: 44px;
width: auto;
display: block;
filter: brightness(0) invert(1);
margin-bottom: 1rem;
}
.footer-tagline {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.75);
margin-bottom: 0.5rem;
font-weight: 400;
}
.footer-service-area {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.65);
margin-bottom: 1rem;
}
.footer-social {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.4rem 0.875rem;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.footer-social:hover {
background-color: rgba(255, 255, 255, 0.12);
color: #ffffff;
}
.footer-col-title {
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 1.125rem;
}
.footer-col ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.footer-link {
font-size: 0.9375rem;
color: rgba(255, 255, 255, 0.8);
transition: color 0.2s ease;
}
.footer-link:hover {
color: #ffffff;
}
.footer-contact-list {
gap: 0.75rem !important;
}
.footer-contact-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.9375rem;
color: rgba(255, 255, 255, 0.8);
}
.footer-contact-item svg {
flex-shrink: 0;
margin-top: 3px;
}
.footer-address {
color: rgba(255, 255, 255, 0.7);
}
.footer-bottom {
border-top: 1px solid rgba(255, 255, 255, 0.15);
padding: 1.125rem 0;
}
.footer-bottom-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-copyright {
font-size: 0.8125rem;
color: rgba(255, 255, 255, 0.5);
}
.footer-legal-nums {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.02em;
}
@media (max-width: 900px) {
.footer-grid {
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
}
@media (max-width: 540px) {
.footer-grid {
grid-template-columns: 1fr;
gap: 1.75rem;
}
.footer-bottom-inner {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
</style>

View File

@@ -1,242 +0,0 @@
---
import { getLangFromUrl, t } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const currentPath = Astro.url.pathname;
const navLinks = [
{ href: '/landing-1', label: 'Landing 1' },
{ href: '/landing-2', label: 'Landing 2' },
{ href: '/landing-3', label: 'Landing 3' },
];
function isActive(href: string): boolean {
if (href === '/') return currentPath === '/';
return currentPath.startsWith(href);
}
---
<header class="site-header">
<div class="container-site header-inner">
<!-- Logo -->
<a href="/" class="header-logo" aria-label="Cachet Peintres Décorateurs — Accueil">
<img
src="/images/logo.png"
alt="Cachet Peintres Décorateurs"
width="160"
height="60"
loading="eager"
/>
</a>
<!-- Desktop nav -->
<nav class="header-nav" aria-label="Navigation principale">
<ul>
{navLinks.map(link => (
<li>
<a
href={link.href}
class:list={['nav-link', { active: isActive(link.href) }]}
>
{link.label}
</a>
</li>
))}
</ul>
</nav>
<!-- CTA -->
<div class="header-cta">
<a href="/soumission" class="btn-primary">
{t(lang, 'cta.soumission.short')}
</a>
</div>
<!-- Mobile menu button -->
<button
class="mobile-menu-btn"
aria-label="Menu"
aria-expanded="false"
aria-controls="mobile-menu"
id="mobile-menu-btn"
>
<span class="sr-only">Ouvrir le menu</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
</div>
<!-- Mobile menu -->
<div class="mobile-menu" id="mobile-menu" aria-hidden="true">
<nav aria-label="Navigation mobile">
<ul>
{navLinks.map(link => (
<li>
<a
href={link.href}
class:list={['mobile-nav-link', { active: isActive(link.href) }]}
>
{link.label}
</a>
</li>
))}
<li>
<a href="/soumission" class="mobile-cta-btn">
{t(lang, 'cta.soumission.short')}
</a>
</li>
</ul>
</nav>
</div>
</header>
<style>
.site-header {
position: sticky;
top: 0;
z-index: 50;
background-color: #ffffff;
border-bottom: 2px solid var(--color-brand-100);
box-shadow: 0 1px 8px rgba(49, 71, 50, 0.08);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
gap: 1rem;
}
.header-logo img {
height: 48px;
width: auto;
display: block;
object-fit: contain;
}
.header-nav ul {
display: flex;
align-items: center;
gap: 2rem;
list-style: none;
margin: 0;
padding: 0;
}
.nav-link {
font-family: var(--font-body);
font-weight: 700;
font-size: 0.9375rem;
color: var(--color-gray-dark);
letter-spacing: 0.02em;
padding: 0.25rem 0;
border-bottom: 2px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.nav-link:hover,
.nav-link.active {
color: var(--color-brand-600);
border-bottom-color: var(--color-brand-600);
}
.mobile-menu-btn {
display: none;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: none;
border: none;
color: var(--color-brand-600);
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.mobile-menu-btn:hover {
background-color: var(--color-brand-50);
}
.mobile-menu {
display: none;
background-color: #ffffff;
border-top: 1px solid var(--color-brand-100);
padding: 1rem 0 1.5rem;
}
.mobile-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.mobile-nav-link {
display: block;
padding: 0.875rem 1.5rem;
font-family: var(--font-body);
font-weight: 700;
font-size: 1rem;
color: var(--color-gray-dark);
border-left: 3px solid transparent;
transition: color 0.2s ease, background-color 0.2s ease;
}
.mobile-nav-link:hover,
.mobile-nav-link.active {
color: var(--color-brand-600);
background-color: var(--color-brand-50);
border-left-color: var(--color-brand-600);
}
.mobile-cta-btn {
display: block;
margin: 1rem 1.5rem 0;
padding: 0.875rem 1.5rem;
background-color: var(--color-brand-600);
color: #ffffff;
font-family: var(--font-body);
font-weight: 700;
font-size: 1rem;
text-align: center;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.mobile-cta-btn:hover {
background-color: var(--color-brand-700);
}
@media (max-width: 768px) {
.header-nav,
.header-cta {
display: none;
}
.mobile-menu-btn {
display: flex;
}
.mobile-menu.open {
display: block;
}
}
</style>
<script>
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');
btn?.addEventListener('click', () => {
const isOpen = menu?.classList.toggle('open');
btn.setAttribute('aria-expanded', String(isOpen));
menu?.setAttribute('aria-hidden', String(!isOpen));
});
</script>

View File

@@ -1,191 +0,0 @@
---
import { getLangFromUrl, t } from '../i18n/utils';
import general from '../content/settings/general.json';
const lang = getLangFromUrl(Astro.url);
---
<section class="hero-section">
<div class="hero-bg" aria-hidden="true"></div>
<div class="container-site hero-content min-h-[60vh]">
<div class="hero-text">
<p class="hero-eyebrow">{general.tagline}</p>
<h1 class="hero-title">{t(lang, 'hero.title')}</h1>
<p class="hero-subtitle">{t(lang, 'hero.subtitle')}</p>
<div class="hero-actions">
<a href="/soumission" class="btn-primary hero-cta-primary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<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(lang, 'hero.cta')}
</a>
<a href="/services" class="btn-outline-white">
{t(lang, 'cta.learn_more')}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12,5 19,12 12,19"/>
</svg>
</a>
</div>
<div class="hero-trust">
<div class="trust-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<span>RBQ 5839 8736 01</span>
</div>
<div class="trust-separator" aria-hidden="true">·</div>
<div class="trust-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>
<span>{general.serviceArea}</span>
</div>
<div class="trust-separator" aria-hidden="true">·</div>
<div class="trust-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>Soumission gratuite</span>
</div>
</div>
</div>
<div class="hero-logo-wrap" aria-hidden="true">
<img
src="/images/logo.png"
alt=""
width="320"
height="320"
loading="eager"
class="hero-logo"
/>
</div>
</div>
</section>
<style>
.hero-section {
position: relative;
background-color: var(--color-brand-600);
color: #ffffff;
padding: 5rem 0 4rem;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 70% 50%, rgba(122, 136, 118, 0.25) 0%, transparent 60%),
linear-gradient(135deg, var(--color-brand-700) 0%, var(--color-brand-600) 100%);
pointer-events: none;
}
.hero-content {
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 3rem;
}
.hero-eyebrow {
display: inline-block;
font-size: 0.8125rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.65);
margin-bottom: 1rem;
}
.hero-title {
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 3.25rem);
font-weight: 500;
color: #ffffff;
line-height: 1.1;
margin-bottom: 1.25rem;
}
.hero-subtitle {
font-size: 1.125rem;
color: rgba(255, 255, 255, 0.82);
line-height: 1.65;
max-width: 540px;
margin-bottom: 2.25rem;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
}
.hero-cta-primary {
background-color: #ffffff;
color: var(--color-brand-600);
font-size: 1rem;
padding: 0.875rem 2rem;
}
.hero-cta-primary:hover {
background-color: var(--color-brand-50);
}
.hero-trust {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem 0.875rem;
}
.trust-item {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
color: rgba(255, 255, 255, 0.65);
}
.trust-separator {
color: rgba(255, 255, 255, 0.3);
}
.hero-logo-wrap {
display: flex;
align-items: center;
justify-content: center;
}
.hero-logo {
width: 280px;
height: 280px;
object-fit: contain;
opacity: 0.18;
filter: brightness(0) invert(1);
}
@media (max-width: 768px) {
.hero-section {
padding: 3.5rem 0 3rem;
}
.hero-content {
grid-template-columns: 1fr;
}
.hero-logo-wrap {
display: none;
}
.hero-title {
font-size: clamp(1.75rem, 6vw, 2.5rem);
}
}
</style>

View File

@@ -1,16 +0,0 @@
---
// Language picker — currently hidden (French only)
// Uncomment and use when English is added:
// import { locales, defaultLocale } from '../i18n/config';
// import { getLangFromUrl } from '../i18n/utils';
// const lang = getLangFromUrl(Astro.url);
---
<!-- LanguagePicker hidden until English is enabled -->
<!-- When adding English:
1. Add 'en' to locales in src/i18n/config.ts and astro.config.mjs
2. Create src/pages/en/ directory with translated pages
3. Remove the 'hidden' attribute below and uncomment the picker -->
<div hidden aria-hidden="true">
<!-- Language picker placeholder -->
</div>

View File

@@ -1,112 +0,0 @@
---
interface Props {
title: string;
description: string;
icon?: string;
slug: string;
featured?: boolean;
}
const { title, description, icon, slug, featured = false } = Astro.props;
const icons: Record<string, string> = {
interior: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>`,
exterior: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 22V12l10-10 10 10v10"/><path d="M15 22v-4a3 3 0 0 0-6 0v4"/><path d="M22 22H2"/></svg>`,
commercial: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>`,
decoration: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
renovation: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
};
const iconSvg = icon ? (icons[icon] ?? icons['interior']) : icons['interior'];
const serviceId = slug.replace('fr/', '');
---
<article class:list={['service-card', { featured }]} id={serviceId}>
<div class="service-card-icon" aria-hidden="true" set:html={iconSvg} />
<div class="service-card-body">
<h3 class="service-card-title">{title}</h3>
<p class="service-card-desc">{description}</p>
<a href={`/services#${serviceId}`} class="service-card-link">
En savoir plus
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12,5 19,12 12,19"/>
</svg>
</a>
</div>
</article>
<style>
.service-card {
background-color: #ffffff;
border: 1.5px solid var(--color-brand-100);
border-radius: 8px;
padding: 1.75rem;
display: flex;
flex-direction: column;
gap: 1rem;
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}
.service-card:hover {
box-shadow: 0 8px 24px rgba(49, 71, 50, 0.1);
transform: translateY(-2px);
border-color: var(--color-brand-400);
}
.service-card.featured {
border-color: var(--color-brand-300);
background-color: var(--color-brand-50);
}
.service-card-icon {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-brand-600);
color: #ffffff;
border-radius: 8px;
flex-shrink: 0;
}
.service-card-body {
display: flex;
flex-direction: column;
gap: 0.625rem;
flex: 1;
}
.service-card-title {
font-family: var(--font-display);
font-size: 1.125rem;
font-weight: 500;
color: var(--color-brand-600);
line-height: 1.25;
}
.service-card-desc {
font-size: 0.9375rem;
color: var(--color-gray-dark);
line-height: 1.6;
opacity: 0.85;
flex: 1;
}
.service-card-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 700;
color: var(--color-brand-600);
transition: gap 0.2s ease;
margin-top: auto;
padding-top: 0.25rem;
}
.service-card-link:hover {
gap: 0.625rem;
}
</style>

View File

@@ -1,90 +0,0 @@
---
import { getCollection } from 'astro:content';
import { getLangFromUrl, t } from '../i18n/utils';
import ServiceCard from './ServiceCard.astro';
const lang = getLangFromUrl(Astro.url);
const allServices = await getCollection('services');
const frServices = allServices
.filter(s => s.id.startsWith('fr/'))
.sort((a, b) => a.data.order - b.data.order)
.slice(0, 3);
---
<section class="services-preview-section">
<div class="container-site">
<div class="section-header">
<h2 class="section-title">{t(lang, 'services.title')}</h2>
<p class="section-subtitle">{t(lang, 'services.subtitle')}</p>
</div>
{frServices.length > 0 ? (
<div class="services-grid">
{frServices.map(service => (
<ServiceCard
title={service.data.title}
description={service.data.description}
icon={service.data.icon}
slug={service.id}
featured={service.data.featured}
/>
))}
</div>
) : (
<p class="services-empty">{t(lang, 'services.empty')}</p>
)}
<div class="services-cta">
<a href="/services" class="btn-outline">
{t(lang, 'services.view_all')}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12,5 19,12 12,19"/>
</svg>
</a>
</div>
</div>
</section>
<style>
.services-preview-section {
padding: 5rem 0;
background-color: #ffffff;
}
.section-header {
text-align: center;
margin-bottom: 3rem;
}
.services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.services-empty {
text-align: center;
color: var(--color-gray-dark);
opacity: 0.6;
padding: 3rem 0;
}
.services-cta {
margin-top: 2.5rem;
text-align: center;
}
@media (max-width: 900px) {
.services-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 540px) {
.services-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,123 +0,0 @@
---
import pourquoi from '../content/settings/pourquoi.json';
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;
}
---
<section class="why-section">
<div class="container-site">
<div class="section-header">
<h2 class="section-title">{pourquoi.title}</h2>
<p class="section-subtitle">{pourquoi.subtitle}</p>
</div>
<div class="why-grid">
{pourquoi.cards.map((card) => (
<div class="why-card">
<div class="why-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>
<h3>{card.title}</h3>
<p>{card.description}</p>
</div>
))}
</div>
</div>
</section>
<style>
.why-section {
padding: 5rem 0;
background-color: var(--color-brand-50);
}
.section-header {
text-align: center;
margin-bottom: 3rem;
}
.why-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
}
.why-card {
background-color: #ffffff;
border: 1.5px solid var(--color-brand-100);
border-radius: 8px;
padding: 1.75rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.why-icon {
width: 48px;
height: 48px;
background-color: var(--color-brand-600);
color: #ffffff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.why-card h3 {
font-family: var(--font-body);
font-size: 1rem;
font-weight: 700;
color: var(--color-brand-600);
}
.why-card p {
font-size: 0.9rem;
color: var(--color-gray-dark);
opacity: 0.8;
line-height: 1.6;
}
@media (max-width: 900px) {
.why-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 540px) {
.why-grid {
grid-template-columns: 1fr;
}
}
</style>

141
src/components/header.tsx Normal file
View File

@@ -0,0 +1,141 @@
import { useState } from "react";
import { Dialog, DialogPanel } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import general from "../content/settings/general.json";
export default function Header({
navigation,
ctaLabel,
ctaHref,
}: {
navigation: { name: string; href: string }[];
ctaLabel: string;
ctaHref: string;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<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">{general.siteName}</span>
<img
alt={general.siteName}
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={ctaHref}
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"
>
{ctaLabel}
</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">{general.siteName}</span>
<img
alt={general.siteName}
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={ctaHref}
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)]"
>
{ctaLabel}
</a>
</div>
</div>
</div>
</DialogPanel>
</Dialog>
</header>
);
}

View File

@@ -1,7 +1,6 @@
---
import seoData from '../content/settings/seo.json';
import general from '../content/settings/general.json';
import { locales, defaultLocale } from '../i18n/config';
interface Props {
title?: string;

View File

@@ -1,7 +1,14 @@
---
import { getLangFromUrl, t } from '../i18n/utils';
import { getCollection } from 'astro:content';
import { getLocale } from '@/lib/locale';
const lang = getLangFromUrl(Astro.url);
const locale = getLocale(Astro);
const [entry] = await getCollection('soumission', (e) => e.id === `${locale}/main`);
if (!entry) throw new Error(`Missing soumission content for locale: ${locale}`);
const s = entry.data;
const f = s.form;
const v = f.validation;
---
<div class="form-wrapper">
@@ -9,12 +16,12 @@ const lang = getLangFromUrl(Astro.url);
id="soumission-form"
class="soumission-form"
novalidate
aria-label={t(lang, 'soumission.title')}
aria-label={f.ariaLabel}
>
<div class="form-row">
<div class="form-group">
<label for="nom" class="form-label">
{t(lang, 'form.nom')}
{f.nom}
<span class="form-required" aria-hidden="true">*</span>
</label>
<input
@@ -22,7 +29,7 @@ const lang = getLangFromUrl(Astro.url);
id="nom"
name="nom"
class="form-input"
placeholder={t(lang, 'form.nom.placeholder')}
placeholder={f.nomPlaceholder}
required
autocomplete="name"
/>
@@ -31,7 +38,7 @@ const lang = getLangFromUrl(Astro.url);
<div class="form-group">
<label for="courriel" class="form-label">
{t(lang, 'form.courriel')}
{f.courriel}
<span class="form-required" aria-hidden="true">*</span>
</label>
<input
@@ -39,7 +46,7 @@ const lang = getLangFromUrl(Astro.url);
id="courriel"
name="courriel"
class="form-input"
placeholder={t(lang, 'form.courriel.placeholder')}
placeholder={f.courrielPlaceholder}
required
autocomplete="email"
/>
@@ -50,7 +57,7 @@ const lang = getLangFromUrl(Astro.url);
<div class="form-row">
<div class="form-group">
<label for="telephone" class="form-label">
{t(lang, 'form.telephone')}
{f.telephone}
<span class="form-required" aria-hidden="true">*</span>
</label>
<input
@@ -58,7 +65,7 @@ const lang = getLangFromUrl(Astro.url);
id="telephone"
name="telephone"
class="form-input"
placeholder={t(lang, 'form.telephone.placeholder')}
placeholder={f.telephonePlaceholder}
required
autocomplete="tel"
/>
@@ -67,7 +74,7 @@ const lang = getLangFromUrl(Astro.url);
<div class="form-group">
<label for="type_service" class="form-label">
{t(lang, 'form.type_service')}
{f.typeService}
<span class="form-required" aria-hidden="true">*</span>
</label>
<select
@@ -76,12 +83,12 @@ const lang = getLangFromUrl(Astro.url);
class="form-input form-select"
required
>
<option value="">{t(lang, 'form.type_service.placeholder')}</option>
<option value="interieur">{t(lang, 'form.type_service.interieur')}</option>
<option value="exterieur">{t(lang, 'form.type_service.exterieur')}</option>
<option value="commercial">{t(lang, 'form.type_service.commercial')}</option>
<option value="decoration">{t(lang, 'form.type_service.decoration')}</option>
<option value="autre">{t(lang, 'form.type_service.autre')}</option>
<option value="">{f.typeServicePlaceholder}</option>
<option value="interieur">{f.typeServiceOptions.interieur}</option>
<option value="exterieur">{f.typeServiceOptions.exterieur}</option>
<option value="commercial">{f.typeServiceOptions.commercial}</option>
<option value="decoration">{f.typeServiceOptions.decoration}</option>
<option value="autre">{f.typeServiceOptions.autre}</option>
</select>
<span class="form-error" id="type_service-error" role="alert" aria-live="polite"></span>
</div>
@@ -89,28 +96,28 @@ const lang = getLangFromUrl(Astro.url);
<div class="form-group">
<label for="adresse" class="form-label">
{t(lang, 'form.adresse')}
{f.adresse}
</label>
<input
type="text"
id="adresse"
name="adresse"
class="form-input"
placeholder={t(lang, 'form.adresse.placeholder')}
placeholder={f.adressePlaceholder}
autocomplete="street-address"
/>
</div>
<div class="form-group">
<label for="message" class="form-label">
{t(lang, 'form.message')}
{f.message}
<span class="form-required" aria-hidden="true">*</span>
</label>
<textarea
id="message"
name="message"
class="form-input form-textarea"
placeholder={t(lang, 'form.message.placeholder')}
placeholder={f.messagePlaceholder}
required
rows="5"
></textarea>
@@ -122,11 +129,10 @@ const lang = getLangFromUrl(Astro.url);
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22,2 15,22 11,13 2,9"/>
</svg>
<span id="submit-text">{t(lang, 'form.envoyer')}</span>
<span id="submit-text">{f.envoyer}</span>
</button>
</form>
<!-- Success message -->
<div class="form-success" id="form-success" aria-live="polite" aria-hidden="true">
<div class="form-success-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
@@ -134,11 +140,10 @@ const lang = getLangFromUrl(Astro.url);
<polyline points="22,4 12,14.01 9,11.01"/>
</svg>
</div>
<h3>{t(lang, 'form.success.title')}</h3>
<p>{t(lang, 'form.success.message')}</p>
<h3>{f.successTitle}</h3>
<p>{f.successMessage}</p>
</div>
<!-- Error message -->
<div class="form-error-msg" id="form-error-msg" aria-live="assertive" aria-hidden="true">
<div class="form-error-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
@@ -148,141 +153,13 @@ const lang = getLangFromUrl(Astro.url);
</svg>
</div>
<div>
<strong>{t(lang, 'form.error.title')}</strong>
<p>{t(lang, 'form.error.message')}</p>
<strong>{f.errorTitle}</strong>
<p>{f.errorMessage}</p>
</div>
</div>
</div>
<style>
.form-wrapper {
width: 100%;
}
.soumission-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0;
}
.form-required {
color: #c0392b;
margin-left: 2px;
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23444445' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.875rem center;
padding-right: 2.5rem;
cursor: pointer;
}
.form-textarea {
resize: vertical;
min-height: 120px;
}
.form-error {
display: block;
font-size: 0.8125rem;
color: #c0392b;
margin-top: 0.25rem;
min-height: 1.125rem;
}
.form-submit-btn {
width: 100%;
justify-content: center;
padding: 1rem;
font-size: 1.0625rem;
margin-top: 0.5rem;
}
.form-success,
.form-error-msg {
display: none;
border-radius: 8px;
padding: 2rem;
text-align: center;
}
.form-success[aria-hidden="false"],
.form-error-msg[aria-hidden="false"] {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.form-success {
background-color: var(--color-brand-50);
border: 1.5px solid var(--color-brand-300);
color: var(--color-brand-600);
}
.form-success-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background-color: var(--color-brand-600);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.form-success h3 {
font-size: 1.25rem;
color: var(--color-brand-600);
}
.form-success p {
color: var(--color-gray-dark);
opacity: 0.8;
}
.form-error-msg {
background-color: #fef2f2;
border: 1.5px solid #fca5a5;
color: #b91c1c;
flex-direction: row;
text-align: left;
gap: 1rem;
}
.form-error-icon {
flex-shrink: 0;
color: #b91c1c;
}
.form-error-msg p {
margin-top: 0.25rem;
font-size: 0.9rem;
opacity: 0.85;
}
@media (max-width: 540px) {
.form-row {
grid-template-columns: 1fr;
}
}
</style>
<script>
<script define:vars={{ v, sendingLabel: f.sending, submitLabel: f.envoyer }}>
const form = document.getElementById('soumission-form') as HTMLFormElement | null;
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement | null;
const submitText = document.getElementById('submit-text');
@@ -297,7 +174,7 @@ const lang = getLangFromUrl(Astro.url);
}
function clearErrors() {
['nom', 'courriel', 'telephone', 'type_service', 'message'].forEach(id => {
['nom', 'courriel', 'telephone', 'type_service', 'message'].forEach((id) => {
const el = document.getElementById(`${id}-error`);
const input = document.getElementById(id);
if (el) el.textContent = '';
@@ -316,27 +193,27 @@ const lang = getLangFromUrl(Astro.url);
const message = data.get('message') as string;
if (!nom?.trim()) {
showError('nom', 'Le nom est requis.');
showError('nom', v.nom);
valid = false;
}
if (!courriel?.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(courriel)) {
showError('courriel', 'Une adresse courriel valide est requise.');
showError('courriel', v.courriel);
valid = false;
}
if (!telephone?.trim()) {
showError('telephone', 'Le numéro de téléphone est requis.');
showError('telephone', v.telephone);
valid = false;
}
if (!type_service) {
showError('type_service', 'Veuillez sélectionner un type de service.');
showError('type_service', v.typeService);
valid = false;
}
if (!message?.trim()) {
showError('message', 'Un message est requis.');
showError('message', v.message);
valid = false;
}
@@ -352,9 +229,8 @@ const lang = getLangFromUrl(Astro.url);
if (!validateForm(formData)) return;
// Disable form during submission
submitBtn.disabled = true;
submitText.textContent = 'Envoi en cours...';
submitText.textContent = sendingLabel;
try {
const body = {
@@ -379,10 +255,9 @@ const lang = getLangFromUrl(Astro.url);
errorDiv.style.display = 'none';
successDiv.setAttribute('aria-hidden', 'false');
successDiv.style.display = 'flex';
} catch {
submitBtn.disabled = false;
submitText.textContent = 'Envoyer ma demande';
submitText.textContent = submitLabel;
errorDiv.setAttribute('aria-hidden', 'false');
errorDiv.style.display = 'flex';
}

View File

@@ -1,139 +0,0 @@
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>
)
}

View File

@@ -1,64 +0,0 @@
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">&rarr;</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>
)
}

View File

@@ -0,0 +1,98 @@
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import Autoplay from "embla-carousel-autoplay";
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";
export type HeroContent = {
pillCta: string;
title: string;
subtitle: string;
cta: string;
ctaHref: string;
};
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 Hero({ content }: { content: HeroContent }) {
const { pillCta, title, subtitle, cta, ctaHref } = content;
return (
<div className="bg-white dark:bg-gray-900">
<div className="relative isolate overflow-hidden h-screen min-h-[600px]">
<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"
/>
<div className="absolute inset-0 bg-black/50" />
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<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>{pillCta}</span>
</div>
</div>
<h1
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-7xl"
style={{ fontFamily: "var(--font-display)" }}
>
{title}
</h1>
<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
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)]"
>
{cta}
</a>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,193 +0,0 @@
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 block sm:hidden">
<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>
)
}

View File

@@ -1,173 +0,0 @@
import { useState } from 'react'
import { Dialog, DialogPanel } from '@headlessui/react'
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
const navigation = [
{ name: 'Accueil', href: '/' },
{ name: 'Services', href: '/services' },
{ name: 'Contact', href: '/contact' },
]
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">
<nav aria-label="Global" className="flex items-center justify-between p-6 lg:px-8">
<div className="flex lg:flex-1">
<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>
</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-gray-500 dark:text-gray-400"
>
<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-gray-900 dark: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-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 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-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Soumission gratuite
</a>
</div>
</div>
</div>
</DialogPanel>
</Dialog>
</header>
<div className="relative isolate overflow-hidden pt-14">
<img
alt=""
src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2830&q=80&blend=111827&sat=-100&exp=15&blend-mode=multiply"
className="absolute inset-0 -z-10 size-full object-cover not-dark:hidden"
/>
<img
alt=""
src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2830&q=80&blend=fff&sat=-100&exp=15&blend-mode=overlay"
className="absolute inset-0 -z-10 size-full object-cover opacity-10 dark:hidden"
/>
<div
aria-hidden="true"
className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80"
>
<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="relative left-[calc(50%-11rem)] aspect-1155/678 w-144.5 -translate-x-1/2 rotate-30 bg-linear-to-tr from-[#ff80b5] to-[#9089fc] opacity-20 sm:left-[calc(50%-30rem)] sm:w-288.75"
/>
</div>
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
<div className="hidden sm:mb-8 sm:flex sm:justify-center">
<div className="relative rounded-full px-3 py-1 text-sm/6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20 dark:text-gray-400 dark:ring-white/10 dark:hover:ring-white/20">
Announcing our next round of funding.{' '}
<a href="#" className="font-semibold text-indigo-600 dark:text-indigo-400">
<span aria-hidden="true" className="absolute inset-0" />
Read more <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
<div className="text-center">
<h1 className="text-5xl font-semibold tracking-tight text-balance text-gray-900 sm:text-7xl dark:text-white">
Data to enrich your online business
</h1>
<p className="mt-8 text-lg font-medium text-pretty text-gray-600 sm:text-xl/8 dark:text-gray-400">
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet
fugiat veniam occaecat.
</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href="#"
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
Get started
</a>
<a href="#" className="text-sm/6 font-semibold text-gray-900 dark:text-white">
Learn more <span aria-hidden="true"></span>
</a>
</div>
</div>
</div>
</div>
<div
aria-hidden="true"
className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
>
<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="relative left-[calc(50%+3rem)] aspect-1155/678 w-144.5 -translate-x-1/2 bg-linear-to-tr from-[#ff80b5] to-[#9089fc] opacity-20 sm:left-[calc(50%+36rem)] sm:w-288.75"
/>
</div>
</div>
</div>
)
}

View File

@@ -1,215 +0,0 @@
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>
);
}

View File

@@ -1,63 +0,0 @@
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>
)
}

View File

@@ -42,7 +42,7 @@ function classNames(...classes: string[]) {
export default function Grid() {
return (
<div className="relative isolate bg-brand-50 pt-24 pb-32 sm:pt-32 dark:bg-gray-900">
<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"