scaffold
This commit is contained in:
92
src/components/CTABanner.astro
Normal file
92
src/components/CTABanner.astro
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
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-600);
|
||||
color: #ffffff;
|
||||
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%);
|
||||
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: #ffffff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cta-banner-subtitle {
|
||||
font-size: 1.0625rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.cta-banner-btn {
|
||||
background-color: #ffffff;
|
||||
color: var(--color-brand-600);
|
||||
font-size: 1rem;
|
||||
padding: 0.875rem 2rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cta-banner-btn:hover {
|
||||
background-color: var(--color-brand-50);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cta-banner-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.cta-banner-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
279
src/components/Footer.astro
Normal file
279
src/components/Footer.astro
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
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));
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container-site footer-grid">
|
||||
<!-- Brand column -->
|
||||
<div class="footer-brand">
|
||||
<a href="/" aria-label="Cachet Peintres Décorateurs">
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
alt="Cachet Peintres Décorateurs"
|
||||
width="140"
|
||||
height="52"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
<p class="footer-tagline">{t(lang, 'footer.tagline')}</p>
|
||||
<p class="footer-service-area">
|
||||
<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"/>
|
||||
</svg>
|
||||
{general.serviceArea}
|
||||
</p>
|
||||
{general.facebook && (
|
||||
<a
|
||||
href={general.facebook}
|
||||
class="footer-social"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<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
|
||||
</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>
|
||||
))}
|
||||
</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>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact column -->
|
||||
<div class="footer-col">
|
||||
<h3 class="footer-col-title">{t(lang, 'footer.contact.title')}</h3>
|
||||
<ul class="footer-contact-list">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
{general.address}
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{t(lang, '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>
|
||||
242
src/components/Header.astro
Normal file
242
src/components/Header.astro
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const currentPath = Astro.url.pathname;
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: t(lang, 'nav.accueil') },
|
||||
{ href: '/services', label: t(lang, 'nav.services') },
|
||||
{ href: '/contact', label: t(lang, 'nav.contact') },
|
||||
];
|
||||
|
||||
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>
|
||||
192
src/components/Hero.astro
Normal file
192
src/components/Hero.astro
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
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">
|
||||
<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>
|
||||
16
src/components/LanguagePicker.astro
Normal file
16
src/components/LanguagePicker.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
// 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>
|
||||
99
src/components/SEOHead.astro
Normal file
99
src/components/SEOHead.astro
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
import seoData from '../content/settings/seo.json';
|
||||
import general from '../content/settings/general.json';
|
||||
import { locales, defaultLocale } from '../i18n/config';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string;
|
||||
ogImage?: string;
|
||||
canonical?: string;
|
||||
noindex?: boolean;
|
||||
pageType?: 'website' | 'article';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = seoData.defaultDescription,
|
||||
keywords = seoData.keywords,
|
||||
ogImage = seoData.ogImage,
|
||||
canonical,
|
||||
noindex = false,
|
||||
pageType = 'website',
|
||||
} = Astro.props;
|
||||
|
||||
const siteUrl = 'https://cachetdeco.com';
|
||||
const fullTitle = title
|
||||
? seoData.titleTemplate.replace('%s', title)
|
||||
: seoData.defaultTitle;
|
||||
|
||||
const canonicalUrl = canonical
|
||||
? `${siteUrl}${canonical}`
|
||||
: `${siteUrl}${Astro.url.pathname}`;
|
||||
|
||||
const ogImageUrl = ogImage
|
||||
? ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`
|
||||
: `${siteUrl}/images/logo.png`;
|
||||
|
||||
// JSON-LD LocalBusiness structured data
|
||||
const structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LocalBusiness',
|
||||
'@id': siteUrl,
|
||||
name: general.siteName,
|
||||
legalName: general.legalName,
|
||||
url: siteUrl,
|
||||
telephone: general.phone,
|
||||
email: general.email,
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: 'Laval',
|
||||
addressRegion: 'QC',
|
||||
addressCountry: 'CA',
|
||||
},
|
||||
areaServed: general.serviceArea,
|
||||
description: seoData.defaultDescription,
|
||||
sameAs: [
|
||||
general.facebook,
|
||||
].filter(Boolean),
|
||||
image: ogImageUrl,
|
||||
priceRange: '$$',
|
||||
};
|
||||
---
|
||||
|
||||
<!-- Primary meta -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
{keywords && <meta name="keywords" content={keywords} />}
|
||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content={pageType} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={ogImageUrl} />
|
||||
<meta property="og:site_name" content={general.siteName} />
|
||||
<meta property="og:locale" content="fr_CA" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImageUrl} />
|
||||
|
||||
<!-- hreflang — French only for now; add en here when English pages exist -->
|
||||
<link rel="alternate" hreflang="fr" href={canonicalUrl} />
|
||||
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/images/logo.png" />
|
||||
|
||||
<!-- JSON-LD -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
|
||||
112
src/components/ServiceCard.astro
Normal file
112
src/components/ServiceCard.astro
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
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>
|
||||
90
src/components/ServicesPreview.astro
Normal file
90
src/components/ServicesPreview.astro
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
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>
|
||||
390
src/components/SoumissionForm.astro
Normal file
390
src/components/SoumissionForm.astro
Normal file
@@ -0,0 +1,390 @@
|
||||
---
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<div class="form-wrapper">
|
||||
<form
|
||||
id="soumission-form"
|
||||
class="soumission-form"
|
||||
novalidate
|
||||
aria-label={t(lang, 'soumission.title')}
|
||||
>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="nom" class="form-label">
|
||||
{t(lang, 'form.nom')}
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nom"
|
||||
name="nom"
|
||||
class="form-input"
|
||||
placeholder={t(lang, 'form.nom.placeholder')}
|
||||
required
|
||||
autocomplete="name"
|
||||
/>
|
||||
<span class="form-error" id="nom-error" role="alert" aria-live="polite"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="courriel" class="form-label">
|
||||
{t(lang, 'form.courriel')}
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="courriel"
|
||||
name="courriel"
|
||||
class="form-input"
|
||||
placeholder={t(lang, 'form.courriel.placeholder')}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
<span class="form-error" id="courriel-error" role="alert" aria-live="polite"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="telephone" class="form-label">
|
||||
{t(lang, 'form.telephone')}
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="telephone"
|
||||
name="telephone"
|
||||
class="form-input"
|
||||
placeholder={t(lang, 'form.telephone.placeholder')}
|
||||
required
|
||||
autocomplete="tel"
|
||||
/>
|
||||
<span class="form-error" id="telephone-error" role="alert" aria-live="polite"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type_service" class="form-label">
|
||||
{t(lang, 'form.type_service')}
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="type_service"
|
||||
name="type_service"
|
||||
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>
|
||||
</select>
|
||||
<span class="form-error" id="type_service-error" role="alert" aria-live="polite"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="adresse" class="form-label">
|
||||
{t(lang, 'form.adresse')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="adresse"
|
||||
name="adresse"
|
||||
class="form-input"
|
||||
placeholder={t(lang, 'form.adresse.placeholder')}
|
||||
autocomplete="street-address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message" class="form-label">
|
||||
{t(lang, 'form.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')}
|
||||
required
|
||||
rows="5"
|
||||
></textarea>
|
||||
<span class="form-error" id="message-error" role="alert" aria-live="polite"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary form-submit-btn" id="submit-btn">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<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>
|
||||
</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">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<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>
|
||||
</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">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t(lang, 'form.error.title')}</strong>
|
||||
<p>{t(lang, 'form.error.message')}</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>
|
||||
const form = document.getElementById('soumission-form') as HTMLFormElement | null;
|
||||
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement | null;
|
||||
const submitText = document.getElementById('submit-text');
|
||||
const successDiv = document.getElementById('form-success');
|
||||
const errorDiv = document.getElementById('form-error-msg');
|
||||
|
||||
function showError(fieldId: string, message: string) {
|
||||
const el = document.getElementById(`${fieldId}-error`);
|
||||
const input = document.getElementById(fieldId);
|
||||
if (el) el.textContent = message;
|
||||
if (input) input.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
['nom', 'courriel', 'telephone', 'type_service', 'message'].forEach(id => {
|
||||
const el = document.getElementById(`${id}-error`);
|
||||
const input = document.getElementById(id);
|
||||
if (el) el.textContent = '';
|
||||
if (input) input.removeAttribute('aria-invalid');
|
||||
});
|
||||
}
|
||||
|
||||
function validateForm(data: FormData): boolean {
|
||||
let valid = true;
|
||||
clearErrors();
|
||||
|
||||
const nom = data.get('nom') as string;
|
||||
const courriel = data.get('courriel') as string;
|
||||
const telephone = data.get('telephone') as string;
|
||||
const type_service = data.get('type_service') as string;
|
||||
const message = data.get('message') as string;
|
||||
|
||||
if (!nom?.trim()) {
|
||||
showError('nom', 'Le nom est requis.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!courriel?.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(courriel)) {
|
||||
showError('courriel', 'Une adresse courriel valide est requise.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!telephone?.trim()) {
|
||||
showError('telephone', 'Le numéro de téléphone est requis.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!type_service) {
|
||||
showError('type_service', 'Veuillez sélectionner un type de service.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!message?.trim()) {
|
||||
showError('message', 'Un message est requis.');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!submitBtn || !submitText || !successDiv || !errorDiv) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
if (!validateForm(formData)) return;
|
||||
|
||||
// Disable form during submission
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = 'Envoi en cours...';
|
||||
|
||||
try {
|
||||
const body = {
|
||||
nom: formData.get('nom'),
|
||||
courriel: formData.get('courriel'),
|
||||
telephone: formData.get('telephone'),
|
||||
type_service: formData.get('type_service'),
|
||||
adresse: formData.get('adresse'),
|
||||
message: formData.get('message'),
|
||||
};
|
||||
|
||||
const res = await fetch('/api/soumission', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Server error');
|
||||
|
||||
form.style.display = 'none';
|
||||
errorDiv.setAttribute('aria-hidden', 'true');
|
||||
errorDiv.style.display = 'none';
|
||||
successDiv.setAttribute('aria-hidden', 'false');
|
||||
successDiv.style.display = 'flex';
|
||||
|
||||
} catch {
|
||||
submitBtn.disabled = false;
|
||||
submitText.textContent = 'Envoyer ma demande';
|
||||
errorDiv.setAttribute('aria-hidden', 'false');
|
||||
errorDiv.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
17
src/content.config.ts
Normal file
17
src/content.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
import { z } from 'astro/zod';
|
||||
|
||||
const services = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/services' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
order: z.number().default(0),
|
||||
featured: z.boolean().default(false),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { services };
|
||||
25
src/content/services/fr/decoration-interieure.md
Normal file
25
src/content/services/fr/decoration-interieure.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Décoration intérieure
|
||||
description: Conseils personnalisés en couleur et décoration. Nos experts vous aident à créer l'ambiance parfaite pour chaque pièce de votre maison.
|
||||
icon: decoration
|
||||
order: 4
|
||||
featured: false
|
||||
---
|
||||
|
||||
Au-delà de la peinture, nos peintres décorateurs apportent leur expertise en design pour vous aider à créer des espaces qui vous ressemblent.
|
||||
|
||||
## Nos services de décoration
|
||||
|
||||
- Consultation couleur et tendances
|
||||
- Effets décoratifs et textures
|
||||
- Trompe-l'œil et murales
|
||||
- Application de papier peint
|
||||
- Conseil en éclairage et mise en valeur des espaces
|
||||
|
||||
## Techniques spécialisées
|
||||
|
||||
- Badigeon et enduit à la chaux
|
||||
- Effet béton ciré
|
||||
- Patines et vieillissements
|
||||
- Peintures texturées
|
||||
- Finitions métalliques
|
||||
26
src/content/services/fr/peinture-commerciale.md
Normal file
26
src/content/services/fr/peinture-commerciale.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Peinture commerciale
|
||||
description: Solutions de peinture pour bureaux, commerces et espaces institutionnels. Travaux réalisés en dehors des heures d'ouverture pour minimiser les perturbations.
|
||||
icon: commercial
|
||||
order: 3
|
||||
featured: true
|
||||
---
|
||||
|
||||
Nos équipes sont habituées à travailler dans des environnements commerciaux et institutionnels, avec flexibilité pour s'adapter à vos contraintes opérationnelles.
|
||||
|
||||
## Ce qui est inclus
|
||||
|
||||
- Planification et coordination selon votre calendrier
|
||||
- Travaux en dehors des heures d'ouverture si requis
|
||||
- Produits à faible émission de COV
|
||||
- Respect des normes de santé et sécurité
|
||||
- Délais garantis pour éviter les interruptions d'activité
|
||||
|
||||
## Types d'établissements
|
||||
|
||||
- Bureaux et espaces de travail
|
||||
- Commerces de détail
|
||||
- Restaurants et hôtels
|
||||
- Cliniques et établissements de santé
|
||||
- Écoles et institutions
|
||||
- Entrepôts et espaces industriels
|
||||
25
src/content/services/fr/peinture-exterieure.md
Normal file
25
src/content/services/fr/peinture-exterieure.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Peinture extérieure
|
||||
description: Protection et embellissement de la façade de votre maison. Travaux résistants aux conditions climatiques du Québec pour une durabilité maximale.
|
||||
icon: exterior
|
||||
order: 2
|
||||
featured: true
|
||||
---
|
||||
|
||||
La peinture extérieure protège votre investissement tout en rehaussant l'apparence de votre propriété. Nous utilisons des produits spécialement formulés pour résister aux hivers québécois.
|
||||
|
||||
## Ce qui est inclus
|
||||
|
||||
- Nettoyage haute pression des surfaces
|
||||
- Réparation des fissures et dommages
|
||||
- Application d'apprêt spécialisé
|
||||
- 2 couches de peinture extérieure haute performance
|
||||
- Protection et calfeutrage aux joints
|
||||
|
||||
## Nos spécialités
|
||||
|
||||
- Revêtements en bois, aluminium et vinyle
|
||||
- Briques et stucco
|
||||
- Balcons, vérandas et galeries
|
||||
- Clôtures et cabanons
|
||||
- Portes et fenêtres (cadres et contours)
|
||||
25
src/content/services/fr/peinture-interieure.md
Normal file
25
src/content/services/fr/peinture-interieure.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Peinture intérieure
|
||||
description: Transformation complète de vos espaces intérieurs. Préparation des surfaces, application soignée et finitions impeccables pour tous types de pièces.
|
||||
icon: interior
|
||||
order: 1
|
||||
featured: true
|
||||
---
|
||||
|
||||
Nos peintres décorateurs réalisent tous vos projets de peinture intérieure avec professionnalisme et souci du détail. De la préparation minutieuse des surfaces à la finition parfaite, chaque étape est effectuée avec rigueur.
|
||||
|
||||
## Ce qui est inclus
|
||||
|
||||
- Évaluation et préparation des surfaces (rebouchage, sablage, apprêt)
|
||||
- Application de 2 couches de peinture de qualité professionnelle
|
||||
- Protection des planchers, meubles et garnitures
|
||||
- Nettoyage complet après les travaux
|
||||
- Garantie sur la main-d'œuvre
|
||||
|
||||
## Nos spécialités
|
||||
|
||||
- Salons et salles à manger
|
||||
- Chambres à coucher
|
||||
- Cuisines et salles de bain
|
||||
- Sous-sols et espaces récréatifs
|
||||
- Couloirs et entrées
|
||||
24
src/content/services/fr/renovation-complete.md
Normal file
24
src/content/services/fr/renovation-complete.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Rénovation complète
|
||||
description: Coordination de projets de rénovation résidentielle. De la peinture aux finitions, un seul entrepreneur pour tous vos travaux de décoration.
|
||||
icon: renovation
|
||||
order: 5
|
||||
featured: false
|
||||
---
|
||||
|
||||
Pour les projets de rénovation importants, Cachet Peintres Décorateurs coordonne l'ensemble des travaux de finition, vous offrant un interlocuteur unique pour simplifier votre projet.
|
||||
|
||||
## Ce qui est inclus
|
||||
|
||||
- Évaluation complète du projet
|
||||
- Coordination des corps de métier
|
||||
- Peinture intérieure et extérieure
|
||||
- Travaux de finition et moulures
|
||||
- Supervision et contrôle qualité
|
||||
|
||||
## Projets réalisés
|
||||
|
||||
- Rénovations complètes de condos
|
||||
- Mises à jour de maisons avant vente
|
||||
- Rénovations post-construction
|
||||
- Remises en état locatifs
|
||||
14
src/content/settings/general.json
Normal file
14
src/content/settings/general.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"siteName": "Cachet Peintres Décorateurs",
|
||||
"legalName": "9500-5609 Québec Inc",
|
||||
"tagline": "Peintres Décorateurs professionnels",
|
||||
"taglineHero": "Qualité et expertise en peinture résidentielle et commerciale",
|
||||
"phone": "(450) 555-0199",
|
||||
"email": "info@cachetdeco.com",
|
||||
"address": "Laval, Québec",
|
||||
"serviceArea": "Laval, Rive-Nord, Montréal",
|
||||
"facebook": "https://www.facebook.com/cachetpeintres",
|
||||
"instagram": "",
|
||||
"rbq": "5839 8736 01",
|
||||
"neq": "9500-5609"
|
||||
}
|
||||
8
src/content/settings/seo.json
Normal file
8
src/content/settings/seo.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"defaultTitle": "Cachet Peintres Décorateurs | Laval & Rive-Nord",
|
||||
"titleTemplate": "%s | Cachet Peintres Décorateurs",
|
||||
"defaultDescription": "Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. RBQ 5839 8736 01. Soumission gratuite.",
|
||||
"keywords": "peintre Laval, peinture résidentielle Laval, peinture commerciale Rive-Nord, décoration intérieure Laval, peintres décorateurs Québec, peinture extérieure Laval, rénovation Rive-Nord, soumission peinture gratuite",
|
||||
"ogImage": "/images/og-default.jpg",
|
||||
"twitterCard": "summary_large_image"
|
||||
}
|
||||
9
src/i18n/config.ts
Normal file
9
src/i18n/config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const defaultLocale = 'fr' as const;
|
||||
export const locales = ['fr'] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
|
||||
export const routes: Record<string, Record<Locale, string>> = {
|
||||
services: { fr: 'services' },
|
||||
contact: { fr: 'contact' },
|
||||
soumission: { fr: 'soumission' },
|
||||
};
|
||||
92
src/i18n/ui.ts
Normal file
92
src/i18n/ui.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export const ui = {
|
||||
fr: {
|
||||
// Navigation
|
||||
'nav.accueil': 'Accueil',
|
||||
'nav.services': 'Services',
|
||||
'nav.contact': 'Contact',
|
||||
'nav.soumission': 'Soumission',
|
||||
|
||||
// CTA
|
||||
'cta.soumission': 'Demandez votre soumission',
|
||||
'cta.soumission.short': 'Soumission gratuite',
|
||||
'cta.contact': 'Nous contacter',
|
||||
'cta.learn_more': 'En savoir plus',
|
||||
|
||||
// Hero
|
||||
'hero.title': 'Peintres Décorateurs à votre service',
|
||||
'hero.subtitle': 'Travaux de peinture résidentielle et commerciale, décoration intérieure et extérieure. Qualité professionnelle sur la Rive-Nord et Laval.',
|
||||
'hero.cta': 'Demandez votre soumission gratuite',
|
||||
|
||||
// Services section
|
||||
'services.title': 'Nos Services',
|
||||
'services.subtitle': 'Des solutions complètes en peinture et décoration',
|
||||
'services.view_all': 'Voir tous nos services',
|
||||
'services.empty': 'Aucun service disponible pour le moment.',
|
||||
|
||||
// CTA Banner
|
||||
'cta_banner.title': 'Prêt à transformer votre espace?',
|
||||
'cta_banner.subtitle': 'Obtenez une soumission gratuite et sans engagement dès aujourd\'hui.',
|
||||
'cta_banner.button': 'Demandez votre soumission',
|
||||
|
||||
// Contact page
|
||||
'contact.title': 'Contactez-nous',
|
||||
'contact.subtitle': 'Nous sommes à votre disposition pour répondre à vos questions.',
|
||||
'contact.phone': 'Téléphone',
|
||||
'contact.email': 'Courriel',
|
||||
'contact.address': 'Adresse',
|
||||
'contact.hours': 'Heures d\'ouverture',
|
||||
'contact.hours.weekdays': 'Lundi – Vendredi : 7h00 – 18h00',
|
||||
'contact.hours.saturday': 'Samedi : 8h00 – 16h00',
|
||||
'contact.hours.sunday': 'Dimanche : Fermé',
|
||||
'contact.follow': 'Suivez-nous',
|
||||
'contact.map.title': 'Notre territoire de service',
|
||||
|
||||
// Soumission form
|
||||
'soumission.title': 'Demande de soumission',
|
||||
'soumission.subtitle': 'Remplissez le formulaire ci-dessous et nous vous contacterons dans les plus brefs délais.',
|
||||
'form.nom': 'Nom complet',
|
||||
'form.nom.placeholder': 'Jean Tremblay',
|
||||
'form.courriel': 'Courriel',
|
||||
'form.courriel.placeholder': 'jean@exemple.com',
|
||||
'form.telephone': 'Téléphone',
|
||||
'form.telephone.placeholder': '(450) 555-1234',
|
||||
'form.adresse': 'Adresse des travaux',
|
||||
'form.adresse.placeholder': '123 rue Principale, Laval, QC',
|
||||
'form.type_service': 'Type de service',
|
||||
'form.type_service.placeholder': 'Sélectionnez un service',
|
||||
'form.type_service.interieur': 'Peinture intérieure',
|
||||
'form.type_service.exterieur': 'Peinture extérieure',
|
||||
'form.type_service.commercial': 'Peinture commerciale',
|
||||
'form.type_service.decoration': 'Décoration intérieure',
|
||||
'form.type_service.autre': 'Autre',
|
||||
'form.message': 'Détails du projet',
|
||||
'form.message.placeholder': 'Décrivez votre projet (pièces à peindre, superficie approximative, délai souhaité...)',
|
||||
'form.envoyer': 'Envoyer ma demande',
|
||||
'form.sending': 'Envoi en cours...',
|
||||
'form.success.title': 'Demande envoyée avec succès!',
|
||||
'form.success.message': 'Merci pour votre demande. Nous vous contacterons dans les 24 à 48 heures.',
|
||||
'form.error.title': 'Erreur lors de l\'envoi',
|
||||
'form.error.message': 'Une erreur s\'est produite. Veuillez réessayer ou nous contacter directement.',
|
||||
'form.required': 'Champ requis',
|
||||
|
||||
// Footer
|
||||
'footer.tagline': 'Peintres Décorateurs professionnels',
|
||||
'footer.nav.title': 'Navigation',
|
||||
'footer.services.title': 'Nos services',
|
||||
'footer.contact.title': 'Contact',
|
||||
'footer.legal': '© {year} 9500-5609 Québec Inc. Tous droits réservés.',
|
||||
'footer.rbq': 'RBQ : 5839 8736 01',
|
||||
'footer.neq': 'NEQ : 9500-5609',
|
||||
'footer.droits': 'Tous droits réservés.',
|
||||
|
||||
// 404
|
||||
'404.title': 'Page introuvable',
|
||||
'404.message': 'La page que vous cherchez n\'existe pas ou a été déplacée.',
|
||||
'404.home': 'Retour à l\'accueil',
|
||||
|
||||
// SEO / meta
|
||||
'meta.site_name': 'Cachet Peintres Décorateurs',
|
||||
'meta.default_title': 'Cachet Peintres Décorateurs | Laval & Rive-Nord',
|
||||
'meta.default_description': 'Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.',
|
||||
},
|
||||
} as const;
|
||||
17
src/i18n/utils.ts
Normal file
17
src/i18n/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ui } from './ui';
|
||||
import { defaultLocale, type Locale } from './config';
|
||||
|
||||
export function getLangFromUrl(url: URL): Locale {
|
||||
const [, lang] = url.pathname.split('/');
|
||||
if (lang in ui) return lang as Locale;
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
export function t(lang: Locale, key: keyof typeof ui[typeof defaultLocale]): string {
|
||||
return (ui[lang] as Record<string, string>)?.[key] ?? ui[defaultLocale][key];
|
||||
}
|
||||
|
||||
export function getLocalizedUrl(lang: Locale, path: string): string {
|
||||
if (lang === defaultLocale) return path;
|
||||
return `/${lang}${path}`;
|
||||
}
|
||||
54
src/layouts/BaseLayout.astro
Normal file
54
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import SEOHead from '../components/SEOHead.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import { getLangFromUrl } from '../i18n/utils';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string;
|
||||
ogImage?: string;
|
||||
canonical?: string;
|
||||
noindex?: boolean;
|
||||
pageType?: 'website' | 'article';
|
||||
}
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const props = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<SEOHead {...props} />
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 1rem;
|
||||
z-index: 9999;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
border-radius: 0 0 4px 4px;
|
||||
transition: top 0.2s ease;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
64
src/pages/404.astro
Normal file
64
src/pages/404.astro
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, '404.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>
|
||||
<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')}
|
||||
</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>
|
||||
454
src/pages/contact.astro
Normal file
454
src/pages/contact.astro
Normal file
@@ -0,0 +1,454 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, '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>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'contact.title')}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'contact.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, '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">
|
||||
<svg width="22" height="22" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.phone')}</h3>
|
||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="contact-value contact-link">
|
||||
{general.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-card">
|
||||
<div class="contact-card-icon">
|
||||
<svg width="22" height="22" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.email')}</h3>
|
||||
<a href={`mailto:${general.email}`} class="contact-value contact-link">
|
||||
{general.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-card">
|
||||
<div class="contact-card-icon">
|
||||
<svg width="22" height="22" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t(lang, 'contact.address')}</h3>
|
||||
<p class="contact-value">{general.address}</p>
|
||||
<p class="contact-service-area">Zone desservie : {general.serviceArea}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hours -->
|
||||
<div class="contact-hours">
|
||||
<h3 class="contact-hours-title">{t(lang, 'contact.hours')}</h3>
|
||||
<ul class="hours-list">
|
||||
<li>
|
||||
<span class="hours-day">Lundi – Vendredi</span>
|
||||
<span class="hours-time">7h00 – 18h00</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="hours-day">Samedi</span>
|
||||
<span class="hours-time">8h00 – 16h00</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="hours-day">Dimanche</span>
|
||||
<span class="hours-time closed">Fermé</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Social -->
|
||||
{general.facebook && (
|
||||
<div class="contact-social">
|
||||
<h3 class="contact-social-title">{t(lang, 'contact.follow')}</h3>
|
||||
<a
|
||||
href={general.facebook}
|
||||
class="social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<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
|
||||
</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">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" 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>
|
||||
</div>
|
||||
<h2>Demandez votre soumission</h2>
|
||||
<p>Obtenez une estimation personnalisée et gratuite pour votre projet de peinture ou décoration.</p>
|
||||
<a href="/soumission" class="btn-primary contact-soumission-btn">
|
||||
{t(lang, 'cta.soumission')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Map placeholder -->
|
||||
<div class="contact-map">
|
||||
<h3 class="contact-map-title">{t(lang, '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>
|
||||
<span class="map-service-area">{general.serviceArea}</span>
|
||||
<a
|
||||
href={`https://maps.google.com/?q=Laval+Quebec`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-outline map-btn"
|
||||
>
|
||||
Voir sur Google Maps
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
139
src/pages/index.astro
Normal file
139
src/pages/index.astro
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
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';
|
||||
|
||||
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="/"
|
||||
>
|
||||
<Hero />
|
||||
<ServicesPreview />
|
||||
|
||||
<!-- Why choose us section -->
|
||||
<section class="why-section">
|
||||
<div class="container-site">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Pourquoi choisir Cachet?</h2>
|
||||
<p class="section-subtitle">Une expertise reconnue au service de votre satisfaction</p>
|
||||
</div>
|
||||
<div class="why-grid">
|
||||
<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">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Certifié RBQ</h3>
|
||||
<p>Entrepreneur certifié par la Régie du Bâtiment du Québec (RBQ : 5839 8736 01). Travaux assurés et conformes aux normes.</p>
|
||||
</div>
|
||||
<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">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Ponctualité et fiabilité</h3>
|
||||
<p>Nous respectons nos engagements. Les délais sont tenus et votre projet est livré à temps, selon les standards convenus.</p>
|
||||
</div>
|
||||
<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">
|
||||
<polyline points="20,6 9,17 4,12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Matériaux de qualité</h3>
|
||||
<p>Nous utilisons exclusivement des peintures et produits de première qualité pour un résultat durable et esthétique.</p>
|
||||
</div>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Équipe professionnelle</h3>
|
||||
<p>Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTABanner />
|
||||
</BaseLayout>
|
||||
|
||||
<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>
|
||||
233
src/pages/services.astro
Normal file
233
src/pages/services.astro
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import CTABanner from '../components/CTABanner.astro';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
||||
const allServices = await getCollection('services');
|
||||
const services = allServices
|
||||
.filter(s => s.id.startsWith('fr/'))
|
||||
.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>`,
|
||||
exterior: `<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="M2 22V12l10-10 10 10v10"/><path d="M15 22v-4a3 3 0 0 0-6 0v4"/><path d="M22 22H2"/></svg>`,
|
||||
commercial: `<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"><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="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"><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="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>`,
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, 'nav.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>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'nav.services')}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'services.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, 'services.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>
|
||||
</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>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p class="services-empty">{t(lang, 'services.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>
|
||||
258
src/pages/soumission.astro
Normal file
258
src/pages/soumission.astro
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import SoumissionForm from '../components/SoumissionForm.astro';
|
||||
import { getLangFromUrl, t } from '../i18n/utils';
|
||||
import general from '../content/settings/general.json';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t(lang, 'nav.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>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page">{t(lang, 'nav.soumission')}</span>
|
||||
</nav>
|
||||
<h1 class="page-title">{t(lang, 'soumission.title')}</h1>
|
||||
<p class="page-subtitle">{t(lang, '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>
|
||||
<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>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-contact">
|
||||
<h3>Ou contactez-nous directement</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"/>
|
||||
</svg>
|
||||
{general.phone}
|
||||
</a>
|
||||
<a href={`mailto:${general.email}`} class="sidebar-email">
|
||||
<svg width="18" height="18" 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>
|
||||
<p class="sidebar-legal">
|
||||
Numéro RBQ : {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>
|
||||
223
src/styles/global.css
Normal file
223
src/styles/global.css
Normal file
@@ -0,0 +1,223 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Custom fonts */
|
||||
@font-face {
|
||||
font-family: 'Cocogoose';
|
||||
src: url('/fonts/Cocogoose-Classic-Medium-trial.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geoform';
|
||||
src: url('/fonts/Geoform-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geoform';
|
||||
src: url('/fonts/Geoform.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Cachet brand palette — calibrated to logo */
|
||||
--color-brand-dark: #314732; /* Dark Green — primary headings, nav */
|
||||
--color-brand-mid: #7a8876; /* Light Green — accents, borders */
|
||||
--color-brand-light: #f0f4ef; /* Very light green tint — backgrounds */
|
||||
--color-brand-50: #f0f4ef;
|
||||
--color-brand-100: #d6e2d4;
|
||||
--color-brand-200: #b8cdb5;
|
||||
--color-brand-300: #97b393;
|
||||
--color-brand-400: #7a8876; /* Light Green */
|
||||
--color-brand-500: #5a6e58;
|
||||
--color-brand-600: #314732; /* Dark Green — CTA buttons */
|
||||
--color-brand-700: #263a27; /* Hover state */
|
||||
--color-brand-800: #1c2c1d;
|
||||
--color-brand-900: #131f13;
|
||||
|
||||
/* Neutral palette */
|
||||
--color-gray-light: #b7b6b6;
|
||||
--color-gray-dark: #444445;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 'Cocogoose', system-ui, sans-serif;
|
||||
--font-body: 'Geoform', system-ui, sans-serif;
|
||||
--font-sans: 'Geoform', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Base layer */
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-gray-dark);
|
||||
background-color: #ffffff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-brand-600);
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
h4, h5, h6 {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-600);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Component layer */
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.75rem;
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.9375rem;
|
||||
letter-spacing: 0.02em;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-brand-700);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.75rem;
|
||||
background-color: transparent;
|
||||
color: var(--color-brand-600);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.9375rem;
|
||||
letter-spacing: 0.02em;
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--color-brand-600);
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: var(--color-brand-600);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-outline-white {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.75rem;
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.9375rem;
|
||||
letter-spacing: 0.02em;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-outline-white:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||
color: var(--color-brand-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-dark);
|
||||
opacity: 0.8;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.container-site {
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-brand-600);
|
||||
margin-bottom: 0.375rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.6875rem 1rem;
|
||||
border: 1.5px solid var(--color-brand-mid);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-dark);
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--color-brand-600);
|
||||
box-shadow: 0 0 0 3px rgba(49, 71, 50, 0.12);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--color-gray-light);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user