cleanup and cms migration
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -1,64 +1,28 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import Layout from '@/layouts/base.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getLocale } from '@/lib/locale';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const locale = getLocale(Astro);
|
||||
const [entry] = await getCollection('notFound', (e) => e.id === `${locale}/main`);
|
||||
if (!entry) throw new Error(`Missing not-found content for locale: ${locale}`);
|
||||
|
||||
const notFound = entry.data;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, '404.title')}
|
||||
noindex={true}
|
||||
>
|
||||
<Layout title={notFound.title} noindex={true}>
|
||||
<section class="not-found-section">
|
||||
<div class="container-site not-found-inner">
|
||||
<div class="not-found-number" aria-hidden="true">404</div>
|
||||
<h1 class="not-found-title">{t(lang, '404.title')}</h1>
|
||||
<p class="not-found-message">{t(lang, '404.message')}</p>
|
||||
<h1 class="not-found-title">{notFound.title}</h1>
|
||||
<p class="not-found-message">{notFound.message}</p>
|
||||
<a href="/" class="btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<line x1="19" y1="12" x2="5" y2="12"/>
|
||||
<polyline points="12,19 5,12 12,5"/>
|
||||
</svg>
|
||||
{t(lang, '404.home')}
|
||||
{notFound.home}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.not-found-section {
|
||||
padding: 6rem 0;
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.not-found-inner {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.not-found-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(5rem, 15vw, 10rem);
|
||||
font-weight: 500;
|
||||
color: var(--color-brand-100);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.not-found-title {
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
color: var(--color-brand-600);
|
||||
}
|
||||
|
||||
.not-found-message {
|
||||
font-size: 1.0625rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.7;
|
||||
max-width: 420px;
|
||||
}
|
||||
</style>
|
||||
</Layout>
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import Layout from '@/layouts/base.astro';
|
||||
import CTABanner from '@/components/ui/cta-banner';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getLocale } from '@/lib/locale';
|
||||
import general from '../content/settings/general.json';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
return Astro.redirect('/404');
|
||||
const locale = getLocale(Astro);
|
||||
|
||||
const [contactEntry] = await getCollection('contact', (e) => e.id === `${locale}/main`);
|
||||
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||
if (!contactEntry || !ctaEntry || !navEntry) throw new Error(`Missing contact content for locale: ${locale}`);
|
||||
|
||||
const contact = contactEntry.data;
|
||||
const ctaData = ctaEntry.data;
|
||||
const homeLink = navEntry.data.links.find((l) => l.href === '/')?.name ?? contact.breadcrumbHome;
|
||||
|
||||
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, 'contact.title')}
|
||||
<Layout
|
||||
title={contact.title}
|
||||
description={`Contactez Cachet Peintres Décorateurs — ${general.phone} — ${general.email}. Nous servons Laval, la Rive-Nord et les environs.`}
|
||||
canonical="/contact"
|
||||
>
|
||||
<!-- Page header -->
|
||||
<section class="page-header">
|
||||
<div class="container-site">
|
||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
||||
<a href="/">{homeLink}</a>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'contact.title')}</span>
|
||||
<span aria-current="page">{contact.title}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'contact.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, 'contact.subtitle')}</p>
|
||||
<h1 class="page-title">{contact.title}</h1>
|
||||
<p class="page-subtitle">{contact.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="contact-section">
|
||||
<div class="container-site contact-grid">
|
||||
<!-- Contact info -->
|
||||
<div class="contact-info">
|
||||
<div class="contact-card">
|
||||
<div class="contact-card-icon">
|
||||
@@ -38,7 +46,7 @@ return Astro.redirect('/404');
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.phone')}</h3>
|
||||
<h3>{contact.phone}</h3>
|
||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="contact-value contact-link">
|
||||
{general.phone}
|
||||
</a>
|
||||
@@ -53,7 +61,7 @@ return Astro.redirect('/404');
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.email')}</h3>
|
||||
<h3>{contact.email}</h3>
|
||||
<a href={`mailto:${general.email}`} class="contact-value contact-link">
|
||||
{general.email}
|
||||
</a>
|
||||
@@ -68,35 +76,33 @@ return Astro.redirect('/404');
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.address')}</h3>
|
||||
<h3>{contact.address}</h3>
|
||||
<p class="contact-value">{general.address}</p>
|
||||
<p class="contact-service-area">Zone desservie : {general.serviceArea}</p>
|
||||
<p class="contact-service-area">{contact.serviceAreaLabel} {general.serviceArea}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hours -->
|
||||
<div class="contact-hours">
|
||||
<h3 class="contact-hours-title">{t(lang, 'contact.hours')}</h3>
|
||||
<h3 class="contact-hours-title">{contact.hours.title}</h3>
|
||||
<ul class="hours-list">
|
||||
<li>
|
||||
<span class="hours-day">Lundi – Vendredi</span>
|
||||
<span class="hours-time">7h00 – 18h00</span>
|
||||
<span class="hours-day">{contact.hours.weekdays.day}</span>
|
||||
<span class="hours-time">{contact.hours.weekdays.time}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="hours-day">Samedi</span>
|
||||
<span class="hours-time">8h00 – 16h00</span>
|
||||
<span class="hours-day">{contact.hours.saturday.day}</span>
|
||||
<span class="hours-time">{contact.hours.saturday.time}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="hours-day">Dimanche</span>
|
||||
<span class="hours-time closed">Fermé</span>
|
||||
<span class="hours-day">{contact.hours.sunday.day}</span>
|
||||
<span class="hours-time closed">{contact.hours.sunday.time}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Social -->
|
||||
{general.facebook && (
|
||||
<div class="contact-social">
|
||||
<h3 class="contact-social-title">{t(lang, 'contact.follow')}</h3>
|
||||
<h3 class="contact-social-title">{contact.follow}</h3>
|
||||
<a
|
||||
href={general.facebook}
|
||||
class="social-link"
|
||||
@@ -106,19 +112,17 @@ return Astro.redirect('/404');
|
||||
<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>
|
||||
Suivez-nous sur Facebook
|
||||
{contact.facebookCta}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Legal -->
|
||||
<div class="contact-legal">
|
||||
<p>RBQ : {general.rbq}</p>
|
||||
<p>NEQ : {general.neq} ({general.legalName})</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="contact-action-panel">
|
||||
<div class="contact-cta-card">
|
||||
<div class="contact-cta-icon">
|
||||
@@ -129,23 +133,22 @@ return Astro.redirect('/404');
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Demandez votre soumission</h2>
|
||||
<p>Obtenez une estimation personnalisée et gratuite pour votre projet de peinture ou décoration.</p>
|
||||
<h2>{contact.ctaCard.title}</h2>
|
||||
<p>{contact.ctaCard.description}</p>
|
||||
<a href="/soumission" class="btn-primary contact-soumission-btn">
|
||||
{t(lang, 'cta.soumission')}
|
||||
{contact.ctaCard.button}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Map placeholder -->
|
||||
<div class="contact-map">
|
||||
<h3 class="contact-map-title">{t(lang, 'contact.map.title')}</h3>
|
||||
<h3 class="contact-map-title">{contact.map.title}</h3>
|
||||
<div class="map-placeholder">
|
||||
<div class="map-placeholder-inner">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>
|
||||
<p>Laval & Rive-Nord, Québec</p>
|
||||
<p>{contact.map.region}</p>
|
||||
<span class="map-service-area">{general.serviceArea}</span>
|
||||
<a
|
||||
href={`https://maps.google.com/?q=Laval+Quebec`}
|
||||
@@ -153,7 +156,7 @@ return Astro.redirect('/404');
|
||||
rel="noopener noreferrer"
|
||||
class="btn-outline map-btn"
|
||||
>
|
||||
Voir sur Google Maps
|
||||
{contact.map.mapsButton}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,295 +165,13 @@ return Astro.redirect('/404');
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTABanner />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background-color: var(--color-brand-50);
|
||||
border-bottom: 1.5px solid var(--color-brand-100);
|
||||
padding: 3rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--color-brand-600);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover { opacity: 0.7; }
|
||||
|
||||
.breadcrumb span[aria-current] {
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.75rem, 4vw, 2.75rem);
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
padding: 4rem 0 5rem;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.1fr;
|
||||
gap: 4rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.375rem;
|
||||
background-color: var(--color-brand-50);
|
||||
border: 1.5px solid var(--color-brand-100);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.contact-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-card h3 {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
font-size: 1.0625rem;
|
||||
color: var(--color-gray-dark);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.contact-link {
|
||||
color: var(--color-brand-600);
|
||||
font-weight: 700;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.contact-link:hover { opacity: 0.75; }
|
||||
|
||||
.contact-service-area {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-brand-400);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-hours {
|
||||
padding: 1.375rem;
|
||||
background-color: var(--color-brand-50);
|
||||
border: 1.5px solid var(--color-brand-100);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.contact-hours-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
.hours-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hours-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.hours-day {
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hours-time {
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-600);
|
||||
}
|
||||
|
||||
.hours-time.closed {
|
||||
color: var(--color-gray-light);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.contact-social { margin-top: 0.25rem; }
|
||||
|
||||
.contact-social-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
color: #1877f2;
|
||||
font-weight: 700;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.social-link:hover { opacity: 0.8; }
|
||||
|
||||
.contact-legal {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.45;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Action panel */
|
||||
.contact-action-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.contact-cta-card {
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
padding: 2.5rem;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.contact-cta-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contact-cta-card h2 {
|
||||
font-size: clamp(1.25rem, 2.5vw, 1.625rem);
|
||||
color: #ffffff;
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.contact-cta-card p {
|
||||
font-size: 1rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contact-soumission-btn {
|
||||
background-color: #ffffff;
|
||||
color: var(--color-brand-600);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.contact-soumission-btn:hover {
|
||||
background-color: var(--color-brand-50);
|
||||
}
|
||||
|
||||
.map-placeholder {
|
||||
background-color: var(--color-brand-50);
|
||||
border: 1.5px solid var(--color-brand-100);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-placeholder-inner {
|
||||
padding: 3rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--color-brand-400);
|
||||
}
|
||||
|
||||
.map-placeholder-inner p {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-600);
|
||||
}
|
||||
|
||||
.map-service-area {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.map-btn {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.contact-map-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<CTABanner
|
||||
client:load
|
||||
title={ctaData.title}
|
||||
subtitle={ctaData.subtitle}
|
||||
buttonText={ctaData.button}
|
||||
buttonHref={ctaData.buttonHref}
|
||||
imageSrc={ctaImage.src}
|
||||
imageAlt={ctaData.imageAlt}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -1,30 +1,118 @@
|
||||
---
|
||||
import Hero from '../components/Hero.astro';
|
||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
||||
import WhyUs from '../components/WhyUs.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getLocale } from '@/lib/locale';
|
||||
import general from '../content/settings/general.json';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Testimonials from '../components/ui/testimonials/Grid';
|
||||
import Footer from '../components/Footer.astro';
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
import seo from '../content/settings/seo.json';
|
||||
import Layout from '@/layouts/base.astro';
|
||||
import Hero from '@/components/ui/hero';
|
||||
import Services from '@/components/ui/services';
|
||||
import WhyChooseUs from '@/components/ui/why-choose-us';
|
||||
import CTABanner from '@/components/ui/cta-banner';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import Testimonials from '@/components/ui/testimonials';
|
||||
|
||||
const locale = getLocale(Astro);
|
||||
|
||||
const [heroEntry] = await getCollection('hero', (e) => e.id === `${locale}/main`);
|
||||
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||
const [pourquoiEntry] = await getCollection('pourquoi', (e) => e.id === `${locale}/main`);
|
||||
|
||||
if (!heroEntry || !navEntry || !ctaEntry || !pourquoiEntry) {
|
||||
throw new Error(`Missing content collections for locale: ${locale}`);
|
||||
}
|
||||
|
||||
const heroData = heroEntry.data;
|
||||
const nav = navEntry.data;
|
||||
const ctaData = ctaEntry.data;
|
||||
const pourquoiData = pourquoiEntry.data;
|
||||
|
||||
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||
|
||||
const allServices = await getCollection('services', (s) => s.id.startsWith(`${locale}/`));
|
||||
const services = allServices.sort((a, b) => a.data.order - b.data.order);
|
||||
|
||||
const serviceImages: Record<string, { src: import('astro').ImageMetadata; alt: string }> = {
|
||||
interior: {
|
||||
src: (await import('@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp')).default,
|
||||
alt: 'Salon avec peinture intérieure soignée',
|
||||
},
|
||||
exterior: {
|
||||
src: (await import('@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp')).default,
|
||||
alt: 'Maison avec peinture extérieure',
|
||||
},
|
||||
commercial: {
|
||||
src: (await import('@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp')).default,
|
||||
alt: 'Espace commercial peint',
|
||||
},
|
||||
decoration: {
|
||||
src: (await import('@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg')).default,
|
||||
alt: 'Décoration intérieure et murale',
|
||||
},
|
||||
renovation: {
|
||||
src: (await import('@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp')).default,
|
||||
alt: 'Rénovation et finitions',
|
||||
},
|
||||
};
|
||||
|
||||
const features = services.map((service) => {
|
||||
const icon = service.data.icon ?? 'interior';
|
||||
const img = serviceImages[icon] ?? serviceImages.interior;
|
||||
const imageSrc = (img.src as import('astro').ImageMetadata).src;
|
||||
return {
|
||||
name: service.data.title,
|
||||
description: service.data.description,
|
||||
imageSrc,
|
||||
imageAlt: img.alt,
|
||||
};
|
||||
});
|
||||
|
||||
const heroContent = {
|
||||
pillCta: heroData.pillCta,
|
||||
title: heroData.title,
|
||||
subtitle: heroData.subtitle,
|
||||
cta: heroData.cta,
|
||||
ctaHref: heroData.ctaHref,
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
||||
<Layout
|
||||
title={seo.defaultTitle}
|
||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
||||
canonical="/"
|
||||
>
|
||||
<Header />
|
||||
<Hero />
|
||||
<ServicesPreview />
|
||||
<WhyUs />
|
||||
<Hero client:load content={heroContent} />
|
||||
<Services
|
||||
client:load
|
||||
features={features}
|
||||
title={nav.servicesSection.title}
|
||||
subtitle={nav.servicesSection.subtitle}
|
||||
/>
|
||||
<Testimonials client:load />
|
||||
<CTABanner />
|
||||
|
||||
<WhyChooseUs
|
||||
client:load
|
||||
title={pourquoiData.title}
|
||||
subtitle={pourquoiData.subtitle}
|
||||
cards={pourquoiData.cards}
|
||||
images={{
|
||||
main: (await import('@/assets/Website Content/Photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp')).default.src,
|
||||
mainAlt: 'Peintres décorateurs au travail',
|
||||
detail2: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp')).default.src,
|
||||
detail2Alt: 'Peintres décorateurs au travail',
|
||||
detail1: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-04-59-54-utc.webp')).default.src,
|
||||
detail1Alt: 'Peintres décorateurs au travail',
|
||||
}}
|
||||
/>
|
||||
<CTABanner
|
||||
client:load
|
||||
title={ctaData.title}
|
||||
subtitle={ctaData.subtitle}
|
||||
buttonText={ctaData.button}
|
||||
buttonHref={ctaData.buttonHref}
|
||||
imageSrc={ctaImage.src}
|
||||
imageAlt={ctaData.imageAlt}
|
||||
/>
|
||||
|
||||
<Footer />
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
</Layout>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import Hero from '../components/Hero.astro';
|
||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
||||
import WhyUs from '../components/WhyUs.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Testimonials from '../components/ui/testimonials/Grid';
|
||||
import Footer from '../components/Footer.astro';
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
||||
canonical="/"
|
||||
>
|
||||
<Header />
|
||||
<Hero />
|
||||
<ServicesPreview />
|
||||
<WhyUs />
|
||||
<Testimonials client:load />
|
||||
<CTABanner />
|
||||
<Footer />
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import Hero from '../components/Hero.astro';
|
||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SimpleBackgroundVideo from '../components/ui/heroes/SimpleBackgroundVideo';
|
||||
import TwoRowBentoGridWithThreeColumnSecondRow from '@/components/ui/bento-grids/TwoRowBentoGridWithThreeColumnSecondRow';
|
||||
import WithFadingImage from '@/components/ui/product-features/WithFadingImage';
|
||||
import Testimonials from '../components/ui/testimonials/Grid';
|
||||
import Footer from '../components/Footer.astro';
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
||||
canonical="/"
|
||||
>
|
||||
<SimpleBackgroundVideo client:load />
|
||||
<TwoRowBentoGridWithThreeColumnSecondRow client:load />
|
||||
<WithFadingImage client:load />
|
||||
<Testimonials client:load />
|
||||
<CTABanner />
|
||||
<Footer />
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SimpleCenteredWithImageSlider from '@/components/ui/heroes/SimpleCenteredWithImageSlider';
|
||||
import WithAlternatingSections from '@/components/ui/product-features/WithAlternatingSections';
|
||||
import { getCollection } from 'astro:content';
|
||||
import WithTieredImages from '@/components/ui/product-features/WithTieredImages';
|
||||
import FullWidthWithBackgroundImageAndLargeContent from '@/components/ui/promo-sections/FullWidthWithBackgroundImageAndLargeContent';
|
||||
import Footer from '../components/Footer.astro';
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
||||
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||
|
||||
const allServices = await getCollection('services');
|
||||
const services = allServices
|
||||
.filter(s => s.id.startsWith('fr/'))
|
||||
.sort((a, b) => a.data.order - b.data.order);
|
||||
|
||||
const serviceImages: Record<string, { src: import('astro').ImageMetadata; alt: string }> = {
|
||||
interior: {
|
||||
src: (await import('@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp')).default,
|
||||
alt: 'Salon avec peinture intérieure soignée',
|
||||
},
|
||||
exterior: {
|
||||
src: (await import('@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp')).default,
|
||||
alt: 'Maison avec peinture extérieure',
|
||||
},
|
||||
commercial: {
|
||||
src: (await import('@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp')).default,
|
||||
alt: 'Espace commercial peint',
|
||||
},
|
||||
decoration: {
|
||||
src: (await import('@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg')).default,
|
||||
alt: 'Décoration intérieure et murale',
|
||||
},
|
||||
renovation: {
|
||||
src: (await import('@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp')).default,
|
||||
alt: 'Rénovation et finitions',
|
||||
},
|
||||
};
|
||||
|
||||
const features = services.map(service => {
|
||||
const icon = service.data.icon ?? 'interior';
|
||||
const img = serviceImages[icon] ?? serviceImages.interior;
|
||||
const imageSrc = (img.src as import('astro').ImageMetadata).src;
|
||||
return {
|
||||
name: service.data.title,
|
||||
description: service.data.description,
|
||||
imageSrc,
|
||||
imageAlt: img.alt,
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
||||
canonical="/"
|
||||
>
|
||||
<SimpleCenteredWithImageSlider client:load />
|
||||
<WithAlternatingSections
|
||||
client:load
|
||||
features={features}
|
||||
title={t(lang, 'services.title')}
|
||||
subtitle={t(lang, 'services.subtitle')}
|
||||
/>
|
||||
<WithTieredImages client:load
|
||||
title="Pourquoi choisir Cachet?"
|
||||
subtitle="Une expertise reconnue au service de votre satisfaction"
|
||||
cards={[
|
||||
{
|
||||
icon: 'shield',
|
||||
title: '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.',
|
||||
},
|
||||
{
|
||||
icon: 'clock',
|
||||
title: 'Ponctualité et fiabilité',
|
||||
description: 'Nous respectons nos engagements. Les délais sont tenus et votre projet est livré à temps, selon les standards convenus.',
|
||||
},
|
||||
{
|
||||
icon: 'team',
|
||||
title: 'É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.',
|
||||
},
|
||||
{
|
||||
icon: 'location',
|
||||
title: 'Localisation',
|
||||
description: 'Nous sommes situés à Laval et sur la Rive-Nord. Nous pouvons vous accompagner dans vos projets de peinture et décoration.',
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
title: 'Excellence',
|
||||
description: 'Nous sommes une entreprise de peinture et décoration de qualité. Nous nous engageons à vous fournir un service de qualité.',
|
||||
},
|
||||
]}
|
||||
images={{
|
||||
main: (await import('@/assets/Website Content/Photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp')).default.src,
|
||||
mainAlt: 'Peintres décorateurs au travail',
|
||||
detail2: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp')).default.src,
|
||||
detail2Alt: 'Peintres décorateurs au travail',
|
||||
detail1: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-04-59-54-utc.webp')).default.src,
|
||||
detail1Alt: 'Peintres décorateurs au travail',
|
||||
}}
|
||||
/>
|
||||
<FullWidthWithBackgroundImageAndLargeContent
|
||||
client:load
|
||||
title={t(lang, 'cta_banner.title')}
|
||||
subtitle={t(lang, 'cta_banner.subtitle')}
|
||||
buttonText={t(lang, 'cta_banner.button')}
|
||||
buttonHref="/soumission"
|
||||
imageSrc={ctaImage.src}
|
||||
imageAlt="Intérieur peint avec soin par Cachet Peintres Décorateurs"
|
||||
/>
|
||||
<Footer />
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import Layout from '@/layouts/base.astro';
|
||||
import CTABanner from '@/components/ui/cta-banner';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import { getLocale } from '@/lib/locale';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const locale = getLocale(Astro);
|
||||
|
||||
const allServices = await getCollection('services');
|
||||
const services = allServices
|
||||
.filter(s => s.id.startsWith('fr/'))
|
||||
.sort((a, b) => a.data.order - b.data.order);
|
||||
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||
if (!navEntry || !ctaEntry) throw new Error(`Missing navigation or cta for locale: ${locale}`);
|
||||
|
||||
const nav = navEntry.data;
|
||||
const ctaData = ctaEntry.data;
|
||||
|
||||
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||
|
||||
const allServices = await getCollection('services', (s) => s.id.startsWith(`${locale}/`));
|
||||
const services = allServices.sort((a, b) => a.data.order - b.data.order);
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
interior: `<svg width="32" height="32" 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>`,
|
||||
@@ -19,218 +26,70 @@ const icons: Record<string, string> = {
|
||||
renovation: `<svg width="32" height="32" 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>`,
|
||||
};
|
||||
|
||||
return Astro.redirect('/404');
|
||||
const homeLink = nav.links.find((l) => l.href === '/')?.name ?? 'Accueil';
|
||||
|
||||
const serviceArticles = await Promise.all(
|
||||
services.map(async (service) => {
|
||||
const { Content } = await render(service);
|
||||
const iconSvg = service.data.icon ? (icons[service.data.icon] ?? icons.interior) : icons.interior;
|
||||
const serviceId = service.id.replace(`${locale}/`, '');
|
||||
return { Content, iconSvg, serviceId, service };
|
||||
}),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, 'nav.services')}
|
||||
<Layout
|
||||
title={nav.pageTitles.services}
|
||||
description="Découvrez tous nos services de peinture et décoration à Laval et sur la Rive-Nord : peinture intérieure et extérieure, commerciale, décoration d'intérieur et rénovation."
|
||||
canonical="/services"
|
||||
>
|
||||
<!-- Page header -->
|
||||
<section class="page-header">
|
||||
<div class="container-site">
|
||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
||||
<a href="/">{homeLink}</a>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'nav.services')}</span>
|
||||
<span aria-current="page">{nav.pageTitles.services}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'services.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, 'services.subtitle')}</p>
|
||||
<h1 class="page-title">{nav.servicesSection.title}</h1>
|
||||
<p class="page-subtitle">{nav.servicesSection.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services listing -->
|
||||
<section class="services-listing">
|
||||
<div class="container-site">
|
||||
{services.length > 0 ? (
|
||||
services.map(async (service) => {
|
||||
const { Content } = await render(service);
|
||||
const iconSvg = service.data.icon ? (icons[service.data.icon] ?? icons['interior']) : icons['interior'];
|
||||
const serviceId = service.id.replace('fr/', '');
|
||||
return (
|
||||
<article class="service-detail" id={serviceId}>
|
||||
<div class="service-detail-header">
|
||||
<div class="service-detail-icon" set:html={iconSvg} />
|
||||
<div>
|
||||
<h2 class="service-detail-title">{service.data.title}</h2>
|
||||
<p class="service-detail-lead">{service.data.description}</p>
|
||||
</div>
|
||||
{serviceArticles.length > 0 ? (
|
||||
serviceArticles.map(({ Content, iconSvg, serviceId, service }) => (
|
||||
<article class="service-detail" id={serviceId}>
|
||||
<div class="service-detail-header">
|
||||
<div class="service-detail-icon" set:html={iconSvg} />
|
||||
<div>
|
||||
<h2 class="service-detail-title">{service.data.title}</h2>
|
||||
<p class="service-detail-lead">{service.data.description}</p>
|
||||
</div>
|
||||
<div class="service-detail-content prose">
|
||||
<Content />
|
||||
</div>
|
||||
<div class="service-detail-cta">
|
||||
<a href="/soumission" class="btn-primary">
|
||||
{t(lang, 'cta.soumission.short')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})
|
||||
</div>
|
||||
<div class="service-detail-content prose">
|
||||
<Content />
|
||||
</div>
|
||||
<div class="service-detail-cta">
|
||||
<a href="/soumission" class="btn-primary">
|
||||
{nav.cta.label}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p class="services-empty">{t(lang, 'services.empty')}</p>
|
||||
<p class="services-empty">{nav.servicesSection.empty}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTABanner />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background-color: var(--color-brand-50);
|
||||
border-bottom: 1.5px solid var(--color-brand-100);
|
||||
padding: 3rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--color-brand-600);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.breadcrumb span[aria-current] {
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.75rem, 4vw, 2.75rem);
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.75;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.services-listing {
|
||||
padding: 4rem 0 5rem;
|
||||
}
|
||||
|
||||
.service-detail {
|
||||
padding: 3rem 0;
|
||||
border-bottom: 1.5px solid var(--color-brand-100);
|
||||
}
|
||||
|
||||
.service-detail:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.service-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.service-detail-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.service-detail-title {
|
||||
font-size: clamp(1.375rem, 3vw, 1.75rem);
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.service-detail-lead {
|
||||
font-size: 1.0625rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.8;
|
||||
line-height: 1.6;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.service-detail-content {
|
||||
max-width: 720px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Prose styles */
|
||||
.prose :global(h2) {
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-brand-600);
|
||||
font-family: var(--font-display);
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.prose :global(ul) {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.prose :global(li) {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.prose :global(li)::before {
|
||||
content: '—';
|
||||
color: var(--color-brand-400);
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prose :global(p) {
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.8;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.service-detail-cta {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.services-empty {
|
||||
text-align: center;
|
||||
padding: 5rem 0;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.service-detail-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.prose :global(ul) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<CTABanner
|
||||
client:load
|
||||
title={ctaData.title}
|
||||
subtitle={ctaData.subtitle}
|
||||
buttonText={ctaData.button}
|
||||
buttonHref={ctaData.buttonHref}
|
||||
imageSrc={ctaImage.src}
|
||||
imageAlt={ctaData.imageAlt}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -1,69 +1,63 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import SoumissionForm from '../components/SoumissionForm.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import Layout from '@/layouts/base.astro';
|
||||
import SoumissionForm from '@/components/submission-form.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getLocale } from '@/lib/locale';
|
||||
import general from '../content/settings/general.json';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
return Astro.redirect('/404');
|
||||
const locale = getLocale(Astro);
|
||||
|
||||
const [soumissionEntry] = await getCollection('soumission', (e) => e.id === `${locale}/main`);
|
||||
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||
if (!soumissionEntry || !navEntry) throw new Error(`Missing soumission content for locale: ${locale}`);
|
||||
|
||||
const soumission = soumissionEntry.data;
|
||||
const nav = navEntry.data;
|
||||
const homeLink = nav.links.find((l) => l.href === '/')?.name ?? soumission.breadcrumbHome;
|
||||
const soumissionNav = nav.links.find((l) => l.href === '/soumission');
|
||||
---
|
||||
<BaseLayout
|
||||
title={t(lang, 'nav.soumission')}
|
||||
|
||||
<Layout
|
||||
title={nav.pageTitles.soumission}
|
||||
description="Demandez votre soumission gratuite pour vos travaux de peinture ou décoration à Laval et sur la Rive-Nord. Cachet Peintres Décorateurs vous répond rapidement."
|
||||
canonical="/soumission"
|
||||
>
|
||||
<!-- Page header -->
|
||||
<section class="page-header">
|
||||
<div class="container-site">
|
||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
||||
<a href="/">{homeLink}</a>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'nav.soumission')}</span>
|
||||
<span aria-current="page">{soumissionNav?.name ?? soumission.pageTitle}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'soumission.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, 'soumission.subtitle')}</p>
|
||||
<h1 class="page-title">{soumission.title}</h1>
|
||||
<p class="page-subtitle">{soumission.subtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="soumission-section">
|
||||
<div class="container-site soumission-grid">
|
||||
<!-- Form -->
|
||||
<div class="soumission-form-col">
|
||||
<SoumissionForm />
|
||||
</div>
|
||||
|
||||
<!-- Side info -->
|
||||
<aside class="soumission-sidebar">
|
||||
<div class="sidebar-card">
|
||||
<h2 class="sidebar-title">Comment ça fonctionne</h2>
|
||||
<h2 class="sidebar-title">{soumission.sidebar.processTitle}</h2>
|
||||
<ol class="process-list">
|
||||
<li class="process-step">
|
||||
<div class="process-num">1</div>
|
||||
<div>
|
||||
<h3>Remplissez le formulaire</h3>
|
||||
<p>Décrivez votre projet en quelques mots — type de travaux, superficie et délai souhaité.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="process-step">
|
||||
<div class="process-num">2</div>
|
||||
<div>
|
||||
<h3>Nous vous contactons</h3>
|
||||
<p>Un de nos experts vous rappelle dans les 24–48 heures pour planifier une visite gratuite.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="process-step">
|
||||
<div class="process-num">3</div>
|
||||
<div>
|
||||
<h3>Soumission détaillée</h3>
|
||||
<p>Vous recevez une soumission claire et sans engagement, avec tous les détails du projet.</p>
|
||||
</div>
|
||||
</li>
|
||||
{soumission.sidebar.steps.map((step, i) => (
|
||||
<li class="process-step">
|
||||
<div class="process-num">{i + 1}</div>
|
||||
<div>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-contact">
|
||||
<h3>Ou contactez-nous directement</h3>
|
||||
<h3>{soumission.sidebar.directContactTitle}</h3>
|
||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="sidebar-phone">
|
||||
<svg width="18" height="18" 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"/>
|
||||
@@ -78,183 +72,11 @@ return Astro.redirect('/404');
|
||||
{general.email}
|
||||
</a>
|
||||
<p class="sidebar-legal">
|
||||
Numéro RBQ : {general.rbq}<br />
|
||||
{soumission.sidebar.rbqLabel} {general.rbq}<br />
|
||||
{general.legalName}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background-color: var(--color-brand-50);
|
||||
border-bottom: 1.5px solid var(--color-brand-100);
|
||||
padding: 3rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-brand-400);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--color-brand-600);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover { opacity: 0.7; }
|
||||
|
||||
.breadcrumb span[aria-current] {
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.75rem, 4vw, 2.75rem);
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.75;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.soumission-section {
|
||||
padding: 4rem 0 5rem;
|
||||
}
|
||||
|
||||
.soumission-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
gap: 4rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.soumission-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
background-color: var(--color-brand-50);
|
||||
border: 1.5px solid var(--color-brand-100);
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.process-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.process-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.process-num {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-step h3 {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.process-step p {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.75;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.sidebar-contact {
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.sidebar-contact h3 {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar-phone,
|
||||
.sidebar-email {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-phone:hover,
|
||||
.sidebar-email:hover { opacity: 0.8; }
|
||||
|
||||
.sidebar-legal {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.soumission-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.soumission-sidebar {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user