cleanup and cms migration
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -39,6 +39,283 @@ collections:
|
|||||||
- { name: "featured", label: "Mis en vedette", widget: "boolean", default: false, i18n: duplicate }
|
- { name: "featured", label: "Mis en vedette", widget: "boolean", default: false, i18n: duplicate }
|
||||||
- { name: "body", label: "Contenu détaillé", widget: "markdown", i18n: true }
|
- { name: "body", label: "Contenu détaillé", widget: "markdown", i18n: true }
|
||||||
|
|
||||||
|
- name: "navigation"
|
||||||
|
label: "Navigation"
|
||||||
|
label_singular: "Navigation"
|
||||||
|
folder: "src/content/navigation"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- name: "links"
|
||||||
|
label: "Liens"
|
||||||
|
widget: "list"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "name", label: "Libellé", widget: "string" }
|
||||||
|
- { name: "href", label: "URL", widget: "string" }
|
||||||
|
- name: "cta"
|
||||||
|
label: "Bouton CTA (header)"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "label", label: "Libellé", widget: "string" }
|
||||||
|
- { name: "href", label: "Lien", widget: "string" }
|
||||||
|
- name: "servicesSection"
|
||||||
|
label: "Bloc Services (accueil)"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string" }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "string" }
|
||||||
|
- { name: "viewAll", label: "Voir tout (libellé)", widget: "string" }
|
||||||
|
- { name: "empty", label: "Message liste vide", widget: "string" }
|
||||||
|
- name: "pageTitles"
|
||||||
|
label: "Titres de page (meta)"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "services", label: "Page Services", widget: "string" }
|
||||||
|
- { name: "soumission", label: "Page Soumission", widget: "string" }
|
||||||
|
|
||||||
|
- name: "hero"
|
||||||
|
label: "Hero (accueil)"
|
||||||
|
label_singular: "Hero"
|
||||||
|
folder: "src/content/hero"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "pillCta", label: "Texte pastille (CTA)", widget: "string", i18n: true }
|
||||||
|
- { name: "title", label: "Titre principal", widget: "string", i18n: true }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "text", i18n: true }
|
||||||
|
- { name: "cta", label: "Bouton principal", widget: "string", i18n: true }
|
||||||
|
- { name: "ctaHref", label: "Lien du bouton", widget: "string", i18n: duplicate }
|
||||||
|
|
||||||
|
- name: "cta"
|
||||||
|
label: "Bannière CTA"
|
||||||
|
label_singular: "Bannière CTA"
|
||||||
|
folder: "src/content/cta"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string", i18n: true }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "text", i18n: true }
|
||||||
|
- { name: "button", label: "Libellé du bouton", widget: "string", i18n: true }
|
||||||
|
- { name: "buttonHref", label: "Lien du bouton", widget: "string", i18n: duplicate }
|
||||||
|
- { name: "imageAlt", label: "Texte alternatif image", widget: "string", i18n: true }
|
||||||
|
|
||||||
|
- name: "contact"
|
||||||
|
label: "Page Contact"
|
||||||
|
label_singular: "Contact"
|
||||||
|
folder: "src/content/contact"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string", i18n: true }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "text", i18n: true }
|
||||||
|
- { name: "phone", label: "Libellé téléphone", widget: "string", i18n: true }
|
||||||
|
- { name: "email", label: "Libellé courriel", widget: "string", i18n: true }
|
||||||
|
- { name: "address", label: "Libellé adresse", widget: "string", i18n: true }
|
||||||
|
- { name: "serviceAreaLabel", label: "Libellé zone desservie", widget: "string", i18n: true }
|
||||||
|
- name: "hours"
|
||||||
|
label: "Heures"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre section", widget: "string" }
|
||||||
|
- name: "weekdays"
|
||||||
|
label: "Semaine"
|
||||||
|
widget: "object"
|
||||||
|
fields:
|
||||||
|
- { name: "day", label: "Jours", widget: "string" }
|
||||||
|
- { name: "time", label: "Heures", widget: "string" }
|
||||||
|
- name: "saturday"
|
||||||
|
label: "Samedi"
|
||||||
|
widget: "object"
|
||||||
|
fields:
|
||||||
|
- { name: "day", label: "Jour", widget: "string" }
|
||||||
|
- { name: "time", label: "Heures", widget: "string" }
|
||||||
|
- name: "sunday"
|
||||||
|
label: "Dimanche"
|
||||||
|
widget: "object"
|
||||||
|
fields:
|
||||||
|
- { name: "day", label: "Jour", widget: "string" }
|
||||||
|
- { name: "time", label: "Heures", widget: "string" }
|
||||||
|
- { name: "follow", label: "Réseaux (titre)", widget: "string", i18n: true }
|
||||||
|
- { name: "facebookCta", label: "Lien Facebook (texte)", widget: "string", i18n: true }
|
||||||
|
- name: "map"
|
||||||
|
label: "Carte"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string" }
|
||||||
|
- { name: "region", label: "Région", widget: "string" }
|
||||||
|
- { name: "mapsButton", label: "Bouton Google Maps", widget: "string" }
|
||||||
|
- name: "ctaCard"
|
||||||
|
label: "Encadré soumission"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string" }
|
||||||
|
- { name: "description", label: "Description", widget: "text" }
|
||||||
|
- { name: "button", label: "Bouton", widget: "string" }
|
||||||
|
- { name: "breadcrumbHome", label: "Fil d'Ariane — Accueil", widget: "string", i18n: true }
|
||||||
|
|
||||||
|
- name: "soumission"
|
||||||
|
label: "Page Soumission & formulaire"
|
||||||
|
label_singular: "Soumission"
|
||||||
|
folder: "src/content/soumission"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "pageTitle", label: "Titre court (breadcrumb)", widget: "string", i18n: true }
|
||||||
|
- { name: "title", label: "Titre H1", widget: "string", i18n: true }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "text", i18n: true }
|
||||||
|
- name: "form"
|
||||||
|
label: "Formulaire"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "ariaLabel", label: "Aria-label formulaire", widget: "string" }
|
||||||
|
- { name: "nom", label: "Libellé nom", widget: "string" }
|
||||||
|
- { name: "nomPlaceholder", label: "Placeholder nom", widget: "string" }
|
||||||
|
- { name: "courriel", label: "Libellé courriel", widget: "string" }
|
||||||
|
- { name: "courrielPlaceholder", label: "Placeholder courriel", widget: "string" }
|
||||||
|
- { name: "telephone", label: "Libellé téléphone", widget: "string" }
|
||||||
|
- { name: "telephonePlaceholder", label: "Placeholder téléphone", widget: "string" }
|
||||||
|
- { name: "adresse", label: "Libellé adresse travaux", widget: "string" }
|
||||||
|
- { name: "adressePlaceholder", label: "Placeholder adresse", widget: "string" }
|
||||||
|
- { name: "typeService", label: "Libellé type de service", widget: "string" }
|
||||||
|
- { name: "typeServicePlaceholder", label: "Placeholder sélection", widget: "string" }
|
||||||
|
- name: "typeServiceOptions"
|
||||||
|
label: "Options type de service"
|
||||||
|
widget: "object"
|
||||||
|
fields:
|
||||||
|
- { name: "interieur", label: "Intérieur", widget: "string" }
|
||||||
|
- { name: "exterieur", label: "Extérieur", widget: "string" }
|
||||||
|
- { name: "commercial", label: "Commercial", widget: "string" }
|
||||||
|
- { name: "decoration", label: "Décoration", widget: "string" }
|
||||||
|
- { name: "autre", label: "Autre", widget: "string" }
|
||||||
|
- { name: "message", label: "Libellé message", widget: "string" }
|
||||||
|
- { name: "messagePlaceholder", label: "Placeholder message", widget: "text" }
|
||||||
|
- { name: "envoyer", label: "Bouton envoyer", widget: "string" }
|
||||||
|
- { name: "sending", label: "État envoi", widget: "string" }
|
||||||
|
- { name: "successTitle", label: "Succès — titre", widget: "string" }
|
||||||
|
- { name: "successMessage", label: "Succès — message", widget: "text" }
|
||||||
|
- { name: "errorTitle", label: "Erreur — titre", widget: "string" }
|
||||||
|
- { name: "errorMessage", label: "Erreur — message", widget: "text" }
|
||||||
|
- name: "validation"
|
||||||
|
label: "Messages validation"
|
||||||
|
widget: "object"
|
||||||
|
fields:
|
||||||
|
- { name: "nom", label: "Nom", widget: "string" }
|
||||||
|
- { name: "courriel", label: "Courriel", widget: "string" }
|
||||||
|
- { name: "telephone", label: "Téléphone", widget: "string" }
|
||||||
|
- { name: "typeService", label: "Type service", widget: "string" }
|
||||||
|
- { name: "message", label: "Message", widget: "string" }
|
||||||
|
- name: "sidebar"
|
||||||
|
label: "Barre latérale"
|
||||||
|
widget: "object"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "processTitle", label: "Titre processus", widget: "string" }
|
||||||
|
- name: "steps"
|
||||||
|
label: "Étapes"
|
||||||
|
widget: "list"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string" }
|
||||||
|
- { name: "description", label: "Description", widget: "text" }
|
||||||
|
- { name: "directContactTitle", label: "Contact direct (titre)", widget: "string" }
|
||||||
|
- { name: "rbqLabel", label: "Libellé RBQ", widget: "string" }
|
||||||
|
- { name: "breadcrumbHome", label: "Fil d'Ariane — Accueil", widget: "string", i18n: true }
|
||||||
|
|
||||||
|
- name: "footer"
|
||||||
|
label: "Pied de page"
|
||||||
|
label_singular: "Footer"
|
||||||
|
folder: "src/content/footer"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "tagline", label: "Slogan", widget: "string", i18n: true }
|
||||||
|
- { name: "navTitle", label: "Titre colonne navigation", widget: "string", i18n: true }
|
||||||
|
- { name: "servicesTitle", label: "Titre colonne services", widget: "string", i18n: true }
|
||||||
|
- { name: "contactTitle", label: "Titre colonne contact", widget: "string", i18n: true }
|
||||||
|
- { name: "legal", label: "Mention légale (utiliser {year})", widget: "string", i18n: true }
|
||||||
|
- { name: "rbq", label: "Ligne RBQ", widget: "string", i18n: true }
|
||||||
|
- { name: "neq", label: "Ligne NEQ", widget: "string", i18n: true }
|
||||||
|
- { name: "facebookLabel", label: "Libellé Facebook", widget: "string", i18n: true }
|
||||||
|
- name: "serviceLinks"
|
||||||
|
label: "Liens services (pied)"
|
||||||
|
widget: "list"
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- { name: "label", label: "Libellé", widget: "string" }
|
||||||
|
- { name: "href", label: "URL", widget: "string" }
|
||||||
|
|
||||||
|
- name: "not_found"
|
||||||
|
label: "Page 404"
|
||||||
|
label_singular: "404"
|
||||||
|
folder: "src/content/not-found"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre", widget: "string", i18n: true }
|
||||||
|
- { name: "message", label: "Message", widget: "text", i18n: true }
|
||||||
|
- { name: "home", label: "Lien retour accueil", widget: "string", i18n: true }
|
||||||
|
|
||||||
|
- name: "pourquoi"
|
||||||
|
label: "Pourquoi nous choisir"
|
||||||
|
label_singular: "Pourquoi"
|
||||||
|
folder: "src/content/pourquoi"
|
||||||
|
format: json
|
||||||
|
create: false
|
||||||
|
delete: false
|
||||||
|
i18n: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
fields:
|
||||||
|
- { name: "title", label: "Titre de section", widget: "string", i18n: true }
|
||||||
|
- { name: "subtitle", label: "Sous-titre", widget: "string", i18n: true }
|
||||||
|
- name: "cards"
|
||||||
|
label: "Cartes d'avantages"
|
||||||
|
widget: "list"
|
||||||
|
min: 1
|
||||||
|
max: 8
|
||||||
|
i18n: true
|
||||||
|
fields:
|
||||||
|
- name: "icon"
|
||||||
|
label: "Icône"
|
||||||
|
widget: "select"
|
||||||
|
options:
|
||||||
|
- { value: "shield", label: "Bouclier (certification)" }
|
||||||
|
- { value: "clock", label: "Horloge (ponctualité)" }
|
||||||
|
- { value: "check", label: "Crochet (qualité)" }
|
||||||
|
- { value: "team", label: "Équipe (personnes)" }
|
||||||
|
- { value: "star", label: "Étoile (excellence)" }
|
||||||
|
- { value: "location", label: "Localisation (territoire)" }
|
||||||
|
- { name: "title", label: "Titre", widget: "string" }
|
||||||
|
- { name: "description", label: "Description", widget: "text" }
|
||||||
|
|
||||||
- name: "settings"
|
- name: "settings"
|
||||||
label: "Paramètres du site"
|
label: "Paramètres du site"
|
||||||
files:
|
files:
|
||||||
@@ -68,28 +345,3 @@ collections:
|
|||||||
- { name: "defaultDescription", label: "Description par défaut", widget: "text" }
|
- { name: "defaultDescription", label: "Description par défaut", widget: "text" }
|
||||||
- { name: "keywords", label: "Mots-clés (séparés par virgule)", widget: "string" }
|
- { name: "keywords", label: "Mots-clés (séparés par virgule)", widget: "string" }
|
||||||
- { name: "ogImage", label: "Image OG par défaut", widget: "image", required: false }
|
- { name: "ogImage", label: "Image OG par défaut", widget: "image", required: false }
|
||||||
|
|
||||||
- label: "Section Pourquoi nous choisir"
|
|
||||||
name: "pourquoi"
|
|
||||||
file: "src/content/settings/pourquoi.json"
|
|
||||||
fields:
|
|
||||||
- { name: "title", label: "Titre de section", widget: "string" }
|
|
||||||
- { name: "subtitle", label: "Sous-titre de section", widget: "string" }
|
|
||||||
- name: "cards"
|
|
||||||
label: "Cartes d'avantages"
|
|
||||||
widget: "list"
|
|
||||||
min: 1
|
|
||||||
max: 6
|
|
||||||
fields:
|
|
||||||
- name: "icon"
|
|
||||||
label: "Icône"
|
|
||||||
widget: "select"
|
|
||||||
options:
|
|
||||||
- { value: "shield", label: "Bouclier (certification)" }
|
|
||||||
- { value: "clock", label: "Horloge (ponctualité)" }
|
|
||||||
- { value: "check", label: "Crochet (qualité)" }
|
|
||||||
- { value: "team", label: "Équipe (personnes)" }
|
|
||||||
- { value: "star", label: "Étoile (excellence)" }
|
|
||||||
- { value: "location", label: "Localisation (territoire)" }
|
|
||||||
- { name: "title", label: "Titre", widget: "string" }
|
|
||||||
- { name: "description", label: "Description", widget: "text" }
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
---
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="cta-banner-section">
|
|
||||||
<div class="cta-banner-bg" aria-hidden="true"></div>
|
|
||||||
<div class="container-site cta-banner-inner">
|
|
||||||
<div class="cta-banner-text">
|
|
||||||
<h2 class="cta-banner-title">{t(lang, 'cta_banner.title')}</h2>
|
|
||||||
<p class="cta-banner-subtitle">{t(lang, 'cta_banner.subtitle')}</p>
|
|
||||||
</div>
|
|
||||||
<div class="cta-banner-action">
|
|
||||||
<a href="/soumission" class="btn-primary cta-banner-btn">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
||||||
<polyline points="14,2 14,8 20,8"/>
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
|
||||||
</svg>
|
|
||||||
{t(lang, 'cta_banner.button')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.cta-banner-section {
|
|
||||||
position: relative;
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
padding: 4rem 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-bg {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
/* background: linear-gradient(135deg, var(--color-brand-700) 0%, var(--color-brand-600) 50%, var(--color-brand-500) 100%); */
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
opacity: 0.6;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-inner {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-subtitle {
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
color: var(--color-brand-500);
|
|
||||||
max-width: 520px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-btn {
|
|
||||||
background-color: var(--color-brand-600);
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.875rem 2rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-btn:hover {
|
|
||||||
background-color: var(--color-brand-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.cta-banner-inner {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,42 +1,36 @@
|
|||||||
---
|
---
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
import general from '../content/settings/general.json';
|
import general from '../content/settings/general.json';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
|
const [footerEntry] = await getCollection('footer', (e) => e.id === `${locale}/main`);
|
||||||
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!footerEntry || !navEntry) throw new Error(`Missing footer or navigation for locale: ${locale}`);
|
||||||
|
|
||||||
|
const footer = footerEntry.data;
|
||||||
|
const navLinks = navEntry.data.links;
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
const legalNotice = footer.legal.replace('{year}', String(year));
|
||||||
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">
|
<footer class="bg-brand-600 pt-14 px-8 text-white/85">
|
||||||
<div class="container-site footer-grid">
|
<div
|
||||||
<!-- Brand column -->
|
class="container-site grid grid-cols-1 gap-7 pb-12 min-[541px]:grid-cols-2 min-[541px]:gap-8 min-[901px]:grid-cols-[1.5fr_1fr_1fr_1fr] min-[901px]:gap-12"
|
||||||
<div class="footer-brand">
|
>
|
||||||
<a href="/" aria-label="Cachet Peintres Décorateurs">
|
<div>
|
||||||
|
<a href="/" aria-label={general.siteName}>
|
||||||
<img
|
<img
|
||||||
src="/images/logo.png"
|
src="/images/logo.png"
|
||||||
alt="Cachet Peintres Décorateurs"
|
alt={general.siteName}
|
||||||
width="140"
|
width="140"
|
||||||
height="52"
|
height="52"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
class="mb-4 block h-11 w-auto brightness-0 invert"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<p class="footer-tagline">{t(lang, 'footer.tagline')}</p>
|
<p class="mb-2 text-sm font-normal text-white/75">{footer.tagline}</p>
|
||||||
<p class="footer-service-area">
|
<p class="mb-4 flex items-center gap-1.5 text-sm text-white/65">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
<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"/>
|
<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"/>
|
<circle cx="12" cy="10" r="3"/>
|
||||||
@@ -46,62 +40,65 @@ const legalNotice = t(lang, 'footer.legal').replace('{year}', String(year));
|
|||||||
{general.facebook && (
|
{general.facebook && (
|
||||||
<a
|
<a
|
||||||
href={general.facebook}
|
href={general.facebook}
|
||||||
class="footer-social"
|
class="inline-flex items-center gap-2 rounded border border-white/30 px-3.5 py-1.5 text-sm font-bold text-white/80 transition-colors hover:bg-white/12 hover:text-white"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="Facebook"
|
aria-label={footer.facebookLabel}
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
<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"/>
|
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Facebook
|
{footer.facebookLabel}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation column -->
|
<div>
|
||||||
<div class="footer-col">
|
<h3 class="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
|
||||||
<h3 class="footer-col-title">{t(lang, 'footer.nav.title')}</h3>
|
{footer.navTitle}
|
||||||
<ul>
|
</h3>
|
||||||
{navLinks.map(link => (
|
<ul class="m-0 flex list-none flex-col gap-2.5 p-0">
|
||||||
<li><a href={link.href} class="footer-link">{link.label}</a></li>
|
{navLinks.map((link) => (
|
||||||
|
<li><a href={link.href} class="text-[0.9375rem] text-white/80 transition-colors hover:text-white">{link.name}</a></li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services column -->
|
<div>
|
||||||
<div class="footer-col">
|
<h3 class="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
|
||||||
<h3 class="footer-col-title">{t(lang, 'footer.services.title')}</h3>
|
{footer.servicesTitle}
|
||||||
<ul>
|
</h3>
|
||||||
{serviceLinks.map(link => (
|
<ul class="m-0 flex list-none flex-col gap-2.5 p-0">
|
||||||
<li><a href={link.href} class="footer-link">{link.label}</a></li>
|
{footer.serviceLinks.map((link) => (
|
||||||
|
<li><a href={link.href} class="text-[0.9375rem] text-white/80 transition-colors hover:text-white">{link.label}</a></li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact column -->
|
<div>
|
||||||
<div class="footer-col">
|
<h3 class="mb-[1.125rem] font-[family-name:var(--font-body)] text-[0.8125rem] font-bold uppercase tracking-[0.08em] text-white/50">
|
||||||
<h3 class="footer-col-title">{t(lang, 'footer.contact.title')}</h3>
|
{footer.contactTitle}
|
||||||
<ul class="footer-contact-list">
|
</h3>
|
||||||
|
<ul class="m-0 flex list-none flex-col gap-3 p-0">
|
||||||
<li>
|
<li>
|
||||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="footer-link footer-contact-item">
|
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2.18h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.18 6.18l.94-.94a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
<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>
|
</svg>
|
||||||
{general.phone}
|
{general.phone}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`mailto:${general.email}`} class="footer-link footer-contact-item">
|
<a href={`mailto:${general.email}`} class="flex items-start gap-2 text-[0.9375rem] text-white/80 transition-colors hover:text-white">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
<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"/>
|
<polyline points="22,6 12,13 2,6"/>
|
||||||
</svg>
|
</svg>
|
||||||
{general.email}
|
{general.email}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="footer-contact-item footer-address">
|
<li class="flex items-start gap-2 text-[0.9375rem] text-white/70">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
<svg class="mt-[3px] shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
<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"/>
|
<circle cx="12" cy="10" r="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -111,169 +108,16 @@ const legalNotice = t(lang, 'footer.legal').replace('{year}', String(year));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom bar -->
|
<div class="border-t border-white/15 py-[1.125rem]">
|
||||||
<div class="footer-bottom">
|
<div
|
||||||
<div class="container-site footer-bottom-inner">
|
class="container-site flex flex-wrap items-center justify-between gap-4 max-[540px]:flex-col max-[540px]:items-start max-[540px]:gap-2"
|
||||||
<p class="footer-copyright">{legalNotice}</p>
|
>
|
||||||
<p class="footer-legal-nums">
|
<p class="text-[0.8125rem] text-white/50">{legalNotice}</p>
|
||||||
<span>{t(lang, 'footer.rbq')}</span>
|
<p class="flex items-center gap-2.5 text-xs tracking-[0.02em] text-white/40">
|
||||||
|
<span>{footer.rbq}</span>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<span>{t(lang, 'footer.neq')}</span>
|
<span>{footer.neq}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
---
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
const currentPath = Astro.url.pathname;
|
|
||||||
|
|
||||||
const navLinks = [
|
|
||||||
{ href: '/landing-1', label: 'Landing 1' },
|
|
||||||
{ href: '/landing-2', label: 'Landing 2' },
|
|
||||||
{ href: '/landing-3', label: 'Landing 3' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function isActive(href: string): boolean {
|
|
||||||
if (href === '/') return currentPath === '/';
|
|
||||||
return currentPath.startsWith(href);
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<header class="site-header">
|
|
||||||
<div class="container-site header-inner">
|
|
||||||
<!-- Logo -->
|
|
||||||
<a href="/" class="header-logo" aria-label="Cachet Peintres Décorateurs — Accueil">
|
|
||||||
<img
|
|
||||||
src="/images/logo.png"
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
width="160"
|
|
||||||
height="60"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Desktop nav -->
|
|
||||||
<nav class="header-nav" aria-label="Navigation principale">
|
|
||||||
<ul>
|
|
||||||
{navLinks.map(link => (
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
class:list={['nav-link', { active: isActive(link.href) }]}
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<div class="header-cta">
|
|
||||||
<a href="/soumission" class="btn-primary">
|
|
||||||
{t(lang, 'cta.soumission.short')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
|
||||||
<button
|
|
||||||
class="mobile-menu-btn"
|
|
||||||
aria-label="Menu"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="mobile-menu"
|
|
||||||
id="mobile-menu-btn"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Ouvrir le menu</span>
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6" />
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12" />
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile menu -->
|
|
||||||
<div class="mobile-menu" id="mobile-menu" aria-hidden="true">
|
|
||||||
<nav aria-label="Navigation mobile">
|
|
||||||
<ul>
|
|
||||||
{navLinks.map(link => (
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
class:list={['mobile-nav-link', { active: isActive(link.href) }]}
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li>
|
|
||||||
<a href="/soumission" class="mobile-cta-btn">
|
|
||||||
{t(lang, 'cta.soumission.short')}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.site-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 50;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-bottom: 2px solid var(--color-brand-100);
|
|
||||||
box-shadow: 0 1px 8px rgba(49, 71, 50, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 72px;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo img {
|
|
||||||
height: 48px;
|
|
||||||
width: auto;
|
|
||||||
display: block;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-nav ul {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2rem;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-gray-dark);
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: color 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover,
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
border-bottom-color: var(--color-brand-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn:hover {
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu {
|
|
||||||
display: none;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-top: 1px solid var(--color-brand-100);
|
|
||||||
padding: 1rem 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav-link {
|
|
||||||
display: block;
|
|
||||||
padding: 0.875rem 1.5rem;
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--color-gray-dark);
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
transition: color 0.2s ease, background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav-link:hover,
|
|
||||||
.mobile-nav-link.active {
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
border-left-color: var(--color-brand-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-cta-btn {
|
|
||||||
display: block;
|
|
||||||
margin: 1rem 1.5rem 0;
|
|
||||||
padding: 0.875rem 1.5rem;
|
|
||||||
background-color: var(--color-brand-600);
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-cta-btn:hover {
|
|
||||||
background-color: var(--color-brand-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header-nav,
|
|
||||||
.header-cta {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu.open {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const btn = document.getElementById('mobile-menu-btn');
|
|
||||||
const menu = document.getElementById('mobile-menu');
|
|
||||||
|
|
||||||
btn?.addEventListener('click', () => {
|
|
||||||
const isOpen = menu?.classList.toggle('open');
|
|
||||||
btn.setAttribute('aria-expanded', String(isOpen));
|
|
||||||
menu?.setAttribute('aria-hidden', String(!isOpen));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
---
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import general from '../content/settings/general.json';
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="hero-section">
|
|
||||||
<div class="hero-bg" aria-hidden="true"></div>
|
|
||||||
<div class="container-site hero-content min-h-[60vh]">
|
|
||||||
<div class="hero-text">
|
|
||||||
<p class="hero-eyebrow">{general.tagline}</p>
|
|
||||||
<h1 class="hero-title">{t(lang, 'hero.title')}</h1>
|
|
||||||
<p class="hero-subtitle">{t(lang, 'hero.subtitle')}</p>
|
|
||||||
<div class="hero-actions">
|
|
||||||
<a href="/soumission" class="btn-primary hero-cta-primary">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
||||||
<polyline points="14,2 14,8 20,8"/>
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
|
||||||
<polyline points="10,9 9,9 8,9"/>
|
|
||||||
</svg>
|
|
||||||
{t(lang, 'hero.cta')}
|
|
||||||
</a>
|
|
||||||
<a href="/services" class="btn-outline-white">
|
|
||||||
{t(lang, 'cta.learn_more')}
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
<polyline points="12,5 19,12 12,19"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="hero-trust">
|
|
||||||
<div class="trust-item">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
||||||
</svg>
|
|
||||||
<span>RBQ 5839 8736 01</span>
|
|
||||||
</div>
|
|
||||||
<div class="trust-separator" aria-hidden="true">·</div>
|
|
||||||
<div class="trust-item">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
|
||||||
<circle cx="12" cy="10" r="3"/>
|
|
||||||
</svg>
|
|
||||||
<span>{general.serviceArea}</span>
|
|
||||||
</div>
|
|
||||||
<div class="trust-separator" aria-hidden="true">·</div>
|
|
||||||
<div class="trust-item">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<polyline points="20 6 9 17 4 12"/>
|
|
||||||
</svg>
|
|
||||||
<span>Soumission gratuite</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hero-logo-wrap" aria-hidden="true">
|
|
||||||
<img
|
|
||||||
src="/images/logo.png"
|
|
||||||
alt=""
|
|
||||||
width="320"
|
|
||||||
height="320"
|
|
||||||
loading="eager"
|
|
||||||
class="hero-logo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.hero-section {
|
|
||||||
position: relative;
|
|
||||||
background-color: var(--color-brand-600);
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 5rem 0 4rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-bg {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(ellipse at 70% 50%, rgba(122, 136, 118, 0.25) 0%, transparent 60%),
|
|
||||||
linear-gradient(135deg, var(--color-brand-700) 0%, var(--color-brand-600) 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-eyebrow {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(2rem, 5vw, 3.25rem);
|
|
||||||
font-weight: 500;
|
|
||||||
color: #ffffff;
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
line-height: 1.65;
|
|
||||||
max-width: 540px;
|
|
||||||
margin-bottom: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-primary {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.875rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-primary:hover {
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-trust {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-separator {
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo {
|
|
||||||
width: 280px;
|
|
||||||
height: 280px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.18;
|
|
||||||
filter: brightness(0) invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero-section {
|
|
||||||
padding: 3.5rem 0 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo-wrap {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(1.75rem, 6vw, 2.5rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
// Language picker — currently hidden (French only)
|
|
||||||
// Uncomment and use when English is added:
|
|
||||||
// import { locales, defaultLocale } from '../i18n/config';
|
|
||||||
// import { getLangFromUrl } from '../i18n/utils';
|
|
||||||
// const lang = getLangFromUrl(Astro.url);
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- LanguagePicker hidden until English is enabled -->
|
|
||||||
<!-- When adding English:
|
|
||||||
1. Add 'en' to locales in src/i18n/config.ts and astro.config.mjs
|
|
||||||
2. Create src/pages/en/ directory with translated pages
|
|
||||||
3. Remove the 'hidden' attribute below and uncomment the picker -->
|
|
||||||
<div hidden aria-hidden="true">
|
|
||||||
<!-- Language picker placeholder -->
|
|
||||||
</div>
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon?: string;
|
|
||||||
slug: string;
|
|
||||||
featured?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, description, icon, slug, featured = false } = Astro.props;
|
|
||||||
|
|
||||||
const icons: Record<string, string> = {
|
|
||||||
interior: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>`,
|
|
||||||
exterior: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 22V12l10-10 10 10v10"/><path d="M15 22v-4a3 3 0 0 0-6 0v4"/><path d="M22 22H2"/></svg>`,
|
|
||||||
commercial: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>`,
|
|
||||||
decoration: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
|
|
||||||
renovation: `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconSvg = icon ? (icons[icon] ?? icons['interior']) : icons['interior'];
|
|
||||||
const serviceId = slug.replace('fr/', '');
|
|
||||||
---
|
|
||||||
|
|
||||||
<article class:list={['service-card', { featured }]} id={serviceId}>
|
|
||||||
<div class="service-card-icon" aria-hidden="true" set:html={iconSvg} />
|
|
||||||
<div class="service-card-body">
|
|
||||||
<h3 class="service-card-title">{title}</h3>
|
|
||||||
<p class="service-card-desc">{description}</p>
|
|
||||||
<a href={`/services#${serviceId}`} class="service-card-link">
|
|
||||||
En savoir plus
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
<polyline points="12,5 19,12 12,19"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.service-card {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1.5px solid var(--color-brand-100);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card:hover {
|
|
||||||
box-shadow: 0 8px 24px rgba(49, 71, 50, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
border-color: var(--color-brand-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card.featured {
|
|
||||||
border-color: var(--color-brand-300);
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-icon {
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--color-brand-600);
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.625rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-desc {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-gray-dark);
|
|
||||||
line-height: 1.6;
|
|
||||||
opacity: 0.85;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
transition: gap 0.2s ease;
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card-link:hover {
|
|
||||||
gap: 0.625rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import ServiceCard from './ServiceCard.astro';
|
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
|
|
||||||
const allServices = await getCollection('services');
|
|
||||||
const frServices = allServices
|
|
||||||
.filter(s => s.id.startsWith('fr/'))
|
|
||||||
.sort((a, b) => a.data.order - b.data.order)
|
|
||||||
.slice(0, 3);
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="services-preview-section">
|
|
||||||
<div class="container-site">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">{t(lang, 'services.title')}</h2>
|
|
||||||
<p class="section-subtitle">{t(lang, 'services.subtitle')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{frServices.length > 0 ? (
|
|
||||||
<div class="services-grid">
|
|
||||||
{frServices.map(service => (
|
|
||||||
<ServiceCard
|
|
||||||
title={service.data.title}
|
|
||||||
description={service.data.description}
|
|
||||||
icon={service.data.icon}
|
|
||||||
slug={service.id}
|
|
||||||
featured={service.data.featured}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p class="services-empty">{t(lang, 'services.empty')}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="services-cta">
|
|
||||||
<a href="/services" class="btn-outline">
|
|
||||||
{t(lang, 'services.view_all')}
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
<polyline points="12,5 19,12 12,19"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.services-preview-section {
|
|
||||||
padding: 5rem 0;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-empty {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-gray-dark);
|
|
||||||
opacity: 0.6;
|
|
||||||
padding: 3rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-cta {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.services-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 540px) {
|
|
||||||
.services-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
---
|
|
||||||
import pourquoi from '../content/settings/pourquoi.json';
|
|
||||||
|
|
||||||
const iconPaths: Record<string, string> = {
|
|
||||||
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
|
|
||||||
clock: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z M12 6v6l4 2',
|
|
||||||
check: 'M20 6L9 17l-5-5',
|
|
||||||
team: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
|
|
||||||
star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
|
|
||||||
location: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z M15 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0z',
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIconPath(icon: string): string {
|
|
||||||
return iconPaths[icon] ?? iconPaths.check;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="why-section">
|
|
||||||
<div class="container-site">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">{pourquoi.title}</h2>
|
|
||||||
<p class="section-subtitle">{pourquoi.subtitle}</p>
|
|
||||||
</div>
|
|
||||||
<div class="why-grid">
|
|
||||||
{pourquoi.cards.map((card) => (
|
|
||||||
<div class="why-card">
|
|
||||||
<div class="why-icon">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
{card.icon === 'clock' ? (
|
|
||||||
<>
|
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
<polyline points="12,6 12,12 16,14"/>
|
|
||||||
</>
|
|
||||||
) : card.icon === 'team' ? (
|
|
||||||
<>
|
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
||||||
<circle cx="9" cy="7" r="4"/>
|
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
||||||
</>
|
|
||||||
) : card.icon === 'location' ? (
|
|
||||||
<>
|
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
|
||||||
<circle cx="12" cy="10" r="3"/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<path d={getIconPath(card.icon)}/>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3>{card.title}</h3>
|
|
||||||
<p>{card.description}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.why-section {
|
|
||||||
padding: 5rem 0;
|
|
||||||
background-color: var(--color-brand-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.why-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.why-card {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1.5px solid var(--color-brand-100);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.75rem 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.why-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background-color: var(--color-brand-600);
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.why-card h3 {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-brand-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.why-card p {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-gray-dark);
|
|
||||||
opacity: 0.8;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.why-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 540px) {
|
|
||||||
.why-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
141
src/components/header.tsx
Normal file
141
src/components/header.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Dialog, DialogPanel } from "@headlessui/react";
|
||||||
|
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import general from "../content/settings/general.json";
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
navigation,
|
||||||
|
ctaLabel,
|
||||||
|
ctaHref,
|
||||||
|
}: {
|
||||||
|
navigation: { name: string; href: string }[];
|
||||||
|
ctaLabel: string;
|
||||||
|
ctaHref: string;
|
||||||
|
}) {
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="absolute inset-x-0 top-0 z-50">
|
||||||
|
<nav
|
||||||
|
aria-label="Global"
|
||||||
|
className="flex items-center justify-between p-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div className="flex lg:flex-1">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="flex items-center bg-[white] rounded-3xl shadow-md
|
||||||
|
ps-0.5 pe-2 pt-1 pb-0.5
|
||||||
|
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
||||||
|
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="p-1 bg-[white] rounded-full ">
|
||||||
|
<span className="sr-only">{general.siteName}</span>
|
||||||
|
<img
|
||||||
|
alt={general.siteName}
|
||||||
|
src="/images/logo.png"
|
||||||
|
className="h-10 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">
|
||||||
|
{general.brandName}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
|
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-[var(--color-brand-100)] hover:text-[var(--color-brand-300)]"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
<Bars3Icon aria-hidden="true" className="size-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:flex lg:gap-x-12">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||||
|
<a
|
||||||
|
href={ctaHref}
|
||||||
|
className="rounded-md bg-[var(--color-brand-600)] px-4 py-2 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] transition-colors"
|
||||||
|
>
|
||||||
|
{ctaLabel}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<Dialog
|
||||||
|
open={mobileMenuOpen}
|
||||||
|
onClose={setMobileMenuOpen}
|
||||||
|
className="lg:hidden"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 z-50" />
|
||||||
|
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
||||||
|
<div className="flex items-center justify-between sm:justify-end lg:justify-between">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="flex sm:hidden lg:flex items-center bg-[white] rounded-3xl
|
||||||
|
ps-0.5 pe-2 pt-1 pb-0.5
|
||||||
|
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
||||||
|
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="p-1 bg-[white] rounded-full ">
|
||||||
|
<span className="sr-only">{general.siteName}</span>
|
||||||
|
<img
|
||||||
|
alt={general.siteName}
|
||||||
|
src="/images/logo.png"
|
||||||
|
className="h-10 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">
|
||||||
|
{general.brandName}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close menu</span>
|
||||||
|
<XMarkIcon aria-hidden="true" className="size-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flow-root">
|
||||||
|
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
||||||
|
<div className="space-y-2 py-6">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="py-6">
|
||||||
|
<a
|
||||||
|
href={ctaHref}
|
||||||
|
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-[var(--color-brand-600)] text-white ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)]"
|
||||||
|
>
|
||||||
|
{ctaLabel}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</Dialog>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import seoData from '../content/settings/seo.json';
|
import seoData from '../content/settings/seo.json';
|
||||||
import general from '../content/settings/general.json';
|
import general from '../content/settings/general.json';
|
||||||
import { locales, defaultLocale } from '../i18n/config';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
---
|
---
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
|
const [entry] = await getCollection('soumission', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!entry) throw new Error(`Missing soumission content for locale: ${locale}`);
|
||||||
|
|
||||||
|
const s = entry.data;
|
||||||
|
const f = s.form;
|
||||||
|
const v = f.validation;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="form-wrapper">
|
<div class="form-wrapper">
|
||||||
@@ -9,12 +16,12 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
id="soumission-form"
|
id="soumission-form"
|
||||||
class="soumission-form"
|
class="soumission-form"
|
||||||
novalidate
|
novalidate
|
||||||
aria-label={t(lang, 'soumission.title')}
|
aria-label={f.ariaLabel}
|
||||||
>
|
>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="nom" class="form-label">
|
<label for="nom" class="form-label">
|
||||||
{t(lang, 'form.nom')}
|
{f.nom}
|
||||||
<span class="form-required" aria-hidden="true">*</span>
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -22,7 +29,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
id="nom"
|
id="nom"
|
||||||
name="nom"
|
name="nom"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder={t(lang, 'form.nom.placeholder')}
|
placeholder={f.nomPlaceholder}
|
||||||
required
|
required
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
/>
|
/>
|
||||||
@@ -31,7 +38,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="courriel" class="form-label">
|
<label for="courriel" class="form-label">
|
||||||
{t(lang, 'form.courriel')}
|
{f.courriel}
|
||||||
<span class="form-required" aria-hidden="true">*</span>
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -39,7 +46,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
id="courriel"
|
id="courriel"
|
||||||
name="courriel"
|
name="courriel"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder={t(lang, 'form.courriel.placeholder')}
|
placeholder={f.courrielPlaceholder}
|
||||||
required
|
required
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
/>
|
/>
|
||||||
@@ -50,7 +57,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="telephone" class="form-label">
|
<label for="telephone" class="form-label">
|
||||||
{t(lang, 'form.telephone')}
|
{f.telephone}
|
||||||
<span class="form-required" aria-hidden="true">*</span>
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -58,7 +65,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
id="telephone"
|
id="telephone"
|
||||||
name="telephone"
|
name="telephone"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder={t(lang, 'form.telephone.placeholder')}
|
placeholder={f.telephonePlaceholder}
|
||||||
required
|
required
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
/>
|
/>
|
||||||
@@ -67,7 +74,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="type_service" class="form-label">
|
<label for="type_service" class="form-label">
|
||||||
{t(lang, 'form.type_service')}
|
{f.typeService}
|
||||||
<span class="form-required" aria-hidden="true">*</span>
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -76,12 +83,12 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
class="form-input form-select"
|
class="form-input form-select"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">{t(lang, 'form.type_service.placeholder')}</option>
|
<option value="">{f.typeServicePlaceholder}</option>
|
||||||
<option value="interieur">{t(lang, 'form.type_service.interieur')}</option>
|
<option value="interieur">{f.typeServiceOptions.interieur}</option>
|
||||||
<option value="exterieur">{t(lang, 'form.type_service.exterieur')}</option>
|
<option value="exterieur">{f.typeServiceOptions.exterieur}</option>
|
||||||
<option value="commercial">{t(lang, 'form.type_service.commercial')}</option>
|
<option value="commercial">{f.typeServiceOptions.commercial}</option>
|
||||||
<option value="decoration">{t(lang, 'form.type_service.decoration')}</option>
|
<option value="decoration">{f.typeServiceOptions.decoration}</option>
|
||||||
<option value="autre">{t(lang, 'form.type_service.autre')}</option>
|
<option value="autre">{f.typeServiceOptions.autre}</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="form-error" id="type_service-error" role="alert" aria-live="polite"></span>
|
<span class="form-error" id="type_service-error" role="alert" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,28 +96,28 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="adresse" class="form-label">
|
<label for="adresse" class="form-label">
|
||||||
{t(lang, 'form.adresse')}
|
{f.adresse}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="adresse"
|
id="adresse"
|
||||||
name="adresse"
|
name="adresse"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder={t(lang, 'form.adresse.placeholder')}
|
placeholder={f.adressePlaceholder}
|
||||||
autocomplete="street-address"
|
autocomplete="street-address"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="message" class="form-label">
|
<label for="message" class="form-label">
|
||||||
{t(lang, 'form.message')}
|
{f.message}
|
||||||
<span class="form-required" aria-hidden="true">*</span>
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="message"
|
id="message"
|
||||||
name="message"
|
name="message"
|
||||||
class="form-input form-textarea"
|
class="form-input form-textarea"
|
||||||
placeholder={t(lang, 'form.message.placeholder')}
|
placeholder={f.messagePlaceholder}
|
||||||
required
|
required
|
||||||
rows="5"
|
rows="5"
|
||||||
></textarea>
|
></textarea>
|
||||||
@@ -122,11 +129,10 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||||
<polygon points="22,2 15,22 11,13 2,9"/>
|
<polygon points="22,2 15,22 11,13 2,9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="submit-text">{t(lang, 'form.envoyer')}</span>
|
<span id="submit-text">{f.envoyer}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Success message -->
|
|
||||||
<div class="form-success" id="form-success" aria-live="polite" aria-hidden="true">
|
<div class="form-success" id="form-success" aria-live="polite" aria-hidden="true">
|
||||||
<div class="form-success-icon">
|
<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">
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||||
@@ -134,11 +140,10 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
<polyline points="22,4 12,14.01 9,11.01"/>
|
<polyline points="22,4 12,14.01 9,11.01"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>{t(lang, 'form.success.title')}</h3>
|
<h3>{f.successTitle}</h3>
|
||||||
<p>{t(lang, 'form.success.message')}</p>
|
<p>{f.successMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error message -->
|
|
||||||
<div class="form-error-msg" id="form-error-msg" aria-live="assertive" aria-hidden="true">
|
<div class="form-error-msg" id="form-error-msg" aria-live="assertive" aria-hidden="true">
|
||||||
<div class="form-error-icon">
|
<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">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||||
@@ -148,141 +153,13 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>{t(lang, 'form.error.title')}</strong>
|
<strong>{f.errorTitle}</strong>
|
||||||
<p>{t(lang, 'form.error.message')}</p>
|
<p>{f.errorMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<script define:vars={{ v, sendingLabel: f.sending, submitLabel: f.envoyer }}>
|
||||||
.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 form = document.getElementById('soumission-form') as HTMLFormElement | null;
|
||||||
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement | null;
|
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement | null;
|
||||||
const submitText = document.getElementById('submit-text');
|
const submitText = document.getElementById('submit-text');
|
||||||
@@ -297,7 +174,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearErrors() {
|
function clearErrors() {
|
||||||
['nom', 'courriel', 'telephone', 'type_service', 'message'].forEach(id => {
|
['nom', 'courriel', 'telephone', 'type_service', 'message'].forEach((id) => {
|
||||||
const el = document.getElementById(`${id}-error`);
|
const el = document.getElementById(`${id}-error`);
|
||||||
const input = document.getElementById(id);
|
const input = document.getElementById(id);
|
||||||
if (el) el.textContent = '';
|
if (el) el.textContent = '';
|
||||||
@@ -316,27 +193,27 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
const message = data.get('message') as string;
|
const message = data.get('message') as string;
|
||||||
|
|
||||||
if (!nom?.trim()) {
|
if (!nom?.trim()) {
|
||||||
showError('nom', 'Le nom est requis.');
|
showError('nom', v.nom);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!courriel?.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(courriel)) {
|
if (!courriel?.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(courriel)) {
|
||||||
showError('courriel', 'Une adresse courriel valide est requise.');
|
showError('courriel', v.courriel);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!telephone?.trim()) {
|
if (!telephone?.trim()) {
|
||||||
showError('telephone', 'Le numéro de téléphone est requis.');
|
showError('telephone', v.telephone);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!type_service) {
|
if (!type_service) {
|
||||||
showError('type_service', 'Veuillez sélectionner un type de service.');
|
showError('type_service', v.typeService);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message?.trim()) {
|
if (!message?.trim()) {
|
||||||
showError('message', 'Un message est requis.');
|
showError('message', v.message);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,9 +229,8 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
|
|
||||||
if (!validateForm(formData)) return;
|
if (!validateForm(formData)) return;
|
||||||
|
|
||||||
// Disable form during submission
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitText.textContent = 'Envoi en cours...';
|
submitText.textContent = sendingLabel;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
@@ -379,10 +255,9 @@ const lang = getLangFromUrl(Astro.url);
|
|||||||
errorDiv.style.display = 'none';
|
errorDiv.style.display = 'none';
|
||||||
successDiv.setAttribute('aria-hidden', 'false');
|
successDiv.setAttribute('aria-hidden', 'false');
|
||||||
successDiv.style.display = 'flex';
|
successDiv.style.display = 'flex';
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitText.textContent = 'Envoyer ma demande';
|
submitText.textContent = submitLabel;
|
||||||
errorDiv.setAttribute('aria-hidden', 'false');
|
errorDiv.setAttribute('aria-hidden', 'false');
|
||||||
errorDiv.style.display = 'flex';
|
errorDiv.style.display = 'flex';
|
||||||
}
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import imgInterieure from '@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp'
|
|
||||||
import imgExterieure from '@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp'
|
|
||||||
import imgDecoration from '@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg'
|
|
||||||
import imgCommerciale from '@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp'
|
|
||||||
import imgRenovation from '@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp'
|
|
||||||
|
|
||||||
export default function TwoRowBentoGridWithThreeColumnSecondRow() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[var(--color-brand-600)] py-24 sm:py-32">
|
|
||||||
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
|
|
||||||
<h2
|
|
||||||
className="text-base/7 font-semibold text-white"
|
|
||||||
style={{ fontFamily: 'var(--font-body)' }}
|
|
||||||
>
|
|
||||||
Nos Services
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
className="mt-2 max-w-lg text-4xl font-semibold tracking-tight text-pretty text-white sm:text-5xl"
|
|
||||||
style={{ fontFamily: 'var(--font-display)' }}
|
|
||||||
>
|
|
||||||
Des solutions complètes en peinture et décoration
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 grid grid-cols-1 gap-4 sm:mt-16 lg:grid-cols-6 lg:grid-rows-2">
|
|
||||||
|
|
||||||
{/* Peinture intérieure — top left */}
|
|
||||||
<div className="relative lg:col-span-3">
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-white max-lg:rounded-t-xl lg:rounded-tl-xl" />
|
|
||||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl max-lg:rounded-t-xl lg:rounded-tl-xl">
|
|
||||||
<img
|
|
||||||
alt="Peinture intérieure résidentielle"
|
|
||||||
src={imgInterieure.src}
|
|
||||||
className="h-80 object-cover object-left"
|
|
||||||
/>
|
|
||||||
<div className="p-10 pt-4">
|
|
||||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
|
||||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
|
||||||
Peinture intérieure
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
|
||||||
Transformation complète de vos espaces intérieurs. Préparation des surfaces, application soignée et finitions impeccables pour tous types de pièces.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 max-lg:rounded-t-xl lg:rounded-tl-xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Peinture extérieure — top right */}
|
|
||||||
<div className="relative lg:col-span-3">
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-white lg:rounded-tr-xl" />
|
|
||||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl lg:rounded-tr-xl">
|
|
||||||
<img
|
|
||||||
alt="Peinture extérieure résidentielle"
|
|
||||||
src={imgExterieure.src}
|
|
||||||
className="h-80 object-cover object-left lg:object-right"
|
|
||||||
/>
|
|
||||||
<div className="p-10 pt-4">
|
|
||||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
|
||||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
|
||||||
Peinture extérieure
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
|
||||||
Protection et embellissement de la façade de votre maison. Travaux résistants aux conditions climatiques du Québec pour une durabilité maximale.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 lg:rounded-tr-xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Décoration intérieure — bottom left */}
|
|
||||||
<div className="relative lg:col-span-2">
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-white lg:rounded-bl-xl" />
|
|
||||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl lg:rounded-bl-xl">
|
|
||||||
<img
|
|
||||||
alt="Décoration intérieure"
|
|
||||||
src={imgDecoration.src}
|
|
||||||
className="h-80 object-cover object-left"
|
|
||||||
/>
|
|
||||||
<div className="p-10 pt-4">
|
|
||||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
|
||||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
|
||||||
Décoration intérieure
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
|
||||||
Conseils personnalisés en couleur et décoration. Effets décoratifs, murales, textures et finitions spécialisées.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 lg:rounded-bl-xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Peinture commerciale — bottom center */}
|
|
||||||
<div className="relative lg:col-span-2">
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-white" />
|
|
||||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl">
|
|
||||||
<img
|
|
||||||
alt="Peinture commerciale"
|
|
||||||
src={imgCommerciale.src}
|
|
||||||
className="h-80 object-cover"
|
|
||||||
/>
|
|
||||||
<div className="p-10 pt-4">
|
|
||||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Commercial</h3>
|
|
||||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
|
||||||
Peinture commerciale
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
|
||||||
Bureaux, commerces et espaces institutionnels. Travaux planifiés selon votre calendrier pour minimiser les perturbations.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rénovation complète — bottom right */}
|
|
||||||
<div className="relative lg:col-span-2">
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-white max-lg:rounded-b-xl lg:rounded-br-xl" />
|
|
||||||
<div className="relative flex h-full flex-col overflow-hidden rounded-xl max-lg:rounded-b-xl lg:rounded-br-xl">
|
|
||||||
<img
|
|
||||||
alt="Rénovation complète"
|
|
||||||
src={imgRenovation.src}
|
|
||||||
className="h-80 object-cover"
|
|
||||||
/>
|
|
||||||
<div className="p-10 pt-4">
|
|
||||||
<h3 className="text-sm/4 font-semibold text-[var(--color-brand-500)]">Résidentiel</h3>
|
|
||||||
<p className="mt-2 text-lg font-medium tracking-tight text-gray-950">
|
|
||||||
Rénovation complète
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
|
|
||||||
Coordination complète de vos travaux de finition. De la peinture aux moulures, un seul entrepreneur pour tous vos projets.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-xl shadow-sm outline outline-black/5 max-lg:rounded-b-xl lg:rounded-br-xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
export default function WithImageTiles() {
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden py-32" style={{ backgroundColor: 'var(--color-brand-600)' }}>
|
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:flex lg:px-8">
|
|
||||||
<div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-12 gap-y-16 lg:mx-0 lg:max-w-none lg:min-w-full lg:flex-none lg:gap-y-8">
|
|
||||||
<div className="lg:col-end-1 lg:w-full lg:max-w-lg lg:pb-8">
|
|
||||||
<h2 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
|
|
||||||
Prêt à transformer votre espace?
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 text-xl/8 text-green-100">
|
|
||||||
Obtenez une soumission gratuite et sans engagement dès aujourd'hui.
|
|
||||||
</p>
|
|
||||||
<p className="mt-6 text-base/7 text-green-200">
|
|
||||||
Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale,
|
|
||||||
décoration intérieure et extérieure.
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 flex">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
||||||
style={{ color: 'var(--color-brand-600)' }}
|
|
||||||
>
|
|
||||||
Demandez votre soumission
|
|
||||||
<span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-start justify-end gap-6 sm:gap-8 lg:contents">
|
|
||||||
<div className="w-0 flex-auto lg:ml-auto lg:w-auto lg:flex-none lg:self-end">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1670272502246-768d249768ca?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1152&q=80"
|
|
||||||
className="aspect-7/5 w-148 max-w-none rounded-2xl object-cover max-sm:w-120" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="contents lg:col-span-2 lg:col-end-2 lg:ml-auto lg:flex lg:w-148 lg:items-start lg:justify-end lg:gap-x-8">
|
|
||||||
<div className="order-first flex w-64 flex-none justify-end self-end max-sm:w-40 lg:w-auto">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1605656816944-971cd5c1407f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=768&h=604&q=80"
|
|
||||||
className="aspect-4/3 w-[24rem] max-w-none flex-none rounded-2xl object-cover" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-96 flex-auto justify-end lg:w-auto lg:flex-none">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1568992687947-868a62a9f521?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1152&h=842&q=80"
|
|
||||||
className="aspect-7/5 w-148 max-w-none flex-none rounded-2xl object-cover max-sm:w-120" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block sm:w-0 sm:flex-auto lg:w-auto lg:flex-none">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1612872087720-bb876e2e67d1?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=768&h=604&q=80"
|
|
||||||
className="aspect-4/3 w-[24rem] max-w-none rounded-2xl object-cover" style={{ backgroundColor: 'var(--color-brand-700)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
98
src/components/ui/hero.tsx
Normal file
98
src/components/ui/hero.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
} from "@/components/ui/carousel";
|
||||||
|
import Autoplay from "embla-carousel-autoplay";
|
||||||
|
import general from "../../content/settings/general.json";
|
||||||
|
import photo1 from "@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp";
|
||||||
|
import photo2 from "@/assets/photos/the-attic-of-a-home-with-white-ceilings-and-white-2023-11-27-05-27-44-utc.webp";
|
||||||
|
import photo3 from "@/assets/photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp";
|
||||||
|
import photo4 from "@/assets/photos/a-bedroom-with-white-walls-and-a-bed-and-a-2023-11-27-05-25-30-utc.webp";
|
||||||
|
import photo5 from "@/assets/photos/empty-room-with-window-2023-11-27-05-08-12-utc.webp";
|
||||||
|
|
||||||
|
export type HeroContent = {
|
||||||
|
pillCta: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
cta: string;
|
||||||
|
ctaHref: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const slides = [
|
||||||
|
{ src: photo1.src, alt: "Peinture intérieure" },
|
||||||
|
{ src: photo2.src, alt: "Décoration murale" },
|
||||||
|
{ src: photo3.src, alt: "Finition de qualité" },
|
||||||
|
{ src: photo4.src, alt: "Chambre" },
|
||||||
|
{ src: photo5.src, alt: "Pièce lumineuse" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const autoplayPlugin = Autoplay({
|
||||||
|
delay: 5000,
|
||||||
|
stopOnInteraction: false,
|
||||||
|
stopOnMouseEnter: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Hero({ content }: { content: HeroContent }) {
|
||||||
|
const { pillCta, title, subtitle, cta, ctaHref } = content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-900">
|
||||||
|
<div className="relative isolate overflow-hidden h-screen min-h-[600px]">
|
||||||
|
<Carousel
|
||||||
|
className="absolute inset-0 -z-10 size-full [&_[data-slot='carousel-content']]:h-full"
|
||||||
|
opts={{ loop: true, dragFree: false }}
|
||||||
|
plugins={[autoplayPlugin]}
|
||||||
|
>
|
||||||
|
<CarouselContent className="-ml-0 h-full">
|
||||||
|
{slides.map((slide, index) => (
|
||||||
|
<CarouselItem key={index} className="pl-0 h-full relative">
|
||||||
|
<img
|
||||||
|
src={slide.src}
|
||||||
|
alt={slide.alt}
|
||||||
|
className="size-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/50" />
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<div className="relative z-10 mx-auto max-w-7xl px-6 lg:px-8 h-full flex items-center justify-center pt-14">
|
||||||
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
|
<div className="hidden sm:mb-8 sm:flex sm:justify-center">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-2 gap-x-3.5 rounded-full px-4 py-2 text-sm/6 text-gray-300 ring-1 ring-white/20">
|
||||||
|
<span>RBQ {general.rbq}</span>
|
||||||
|
<span className="text-white/30" aria-hidden>
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<span>{general.serviceArea}</span>
|
||||||
|
<span className="text-white/30" aria-hidden>
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<span>{pillCta}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-7xl"
|
||||||
|
style={{ fontFamily: "var(--font-display)" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-8 text-lg font-medium text-pretty text-gray-300 sm:text-xl/8">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 flex items-center justify-center">
|
||||||
|
<a
|
||||||
|
href={ctaHref}
|
||||||
|
className="rounded-md bg-[var(--color-brand-600)] px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-[var(--color-brand-700)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-brand-600)]"
|
||||||
|
>
|
||||||
|
{cta}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Dialog, DialogPanel } from '@headlessui/react'
|
|
||||||
import heroVideoMp4 from '@/assets/videos/cachet-deco.mp4'
|
|
||||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { defaultLocale } from '../../../i18n/config'
|
|
||||||
import { t } from '../../../i18n/utils'
|
|
||||||
import general from '../../../content/settings/general.json'
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{ name: 'Landing 1', href: '/landing-1' },
|
|
||||||
{ name: 'Landing 2', href: '/landing-2' },
|
|
||||||
{ name: 'Landing 3', href: '/landing-3' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function SimpleCenteredWithBackgroundImage() {
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white dark:bg-gray-900">
|
|
||||||
<header className="absolute inset-x-0 top-0 z-50 bg-[var(--color-brand-600)]">
|
|
||||||
<nav aria-label="Global" className="flex items-center justify-between p-2 lg:px-8">
|
|
||||||
<div className="flex lg:flex-1">
|
|
||||||
<a href="/" className="-m-1.5 p-1 m-1 bg-[white] rounded-full">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex lg:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(true)}
|
|
||||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-white"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
<Bars3Icon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:gap-x-12">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a key={item.name} href={item.href} className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2">
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="rounded-md bg-white px-4 py-2 text-sm font-semibold text-[var(--color-brand-600)] shadow-sm ring-1 ring-inset ring-white/30 hover:bg-white/90 transition-colors"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen} className="lg:hidden">
|
|
||||||
<div className="fixed inset-0 z-50" />
|
|
||||||
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<a href="/" className="-m-1.5 p-1.5 block sm:hidden">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
<XMarkIcon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flow-root">
|
|
||||||
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
|
||||||
<div className="space-y-2 py-6">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="py-6">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-[var(--color-brand-600)] text-white ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)]"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</Dialog>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="relative isolate min-h-screen overflow-hidden pt-14">
|
|
||||||
<video
|
|
||||||
autoPlay
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
playsInline
|
|
||||||
className="absolute inset-0 -z-10 size-full object-cover"
|
|
||||||
>
|
|
||||||
<source src={heroVideoMp4} type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
<div className="absolute inset-0 -z-10 bg-black/50" />
|
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="grid min-h-[calc(100vh-3.5rem)] grid-cols-1 items-center gap-12 py-20 lg:grid-cols-[1fr_auto] lg:gap-12 lg:py-24">
|
|
||||||
<div>
|
|
||||||
<p className="mb-4 inline-block text-sm font-bold uppercase tracking-wider text-white/65">
|
|
||||||
{general.tagline}
|
|
||||||
</p>
|
|
||||||
<h1 className="mb-5 text-4xl font-medium leading-tight text-white sm:text-5xl lg:text-[2.5rem]" style={{ fontFamily: 'var(--font-display)' }}>
|
|
||||||
{t(defaultLocale, 'hero.title')}
|
|
||||||
</h1>
|
|
||||||
<p className="mb-9 max-w-[540px] text-xl leading-relaxed text-white/82">
|
|
||||||
{t(defaultLocale, 'hero.subtitle')}
|
|
||||||
</p>
|
|
||||||
<div className="mb-8 flex flex-wrap gap-4">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="inline-flex items-center gap-2 rounded-md bg-white px-8 py-3.5 text-lg font-semibold text-[var(--color-brand-600)] transition-colors hover:bg-[var(--color-brand-50)]"
|
|
||||||
>
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14,2 14,8 20,8" />
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
|
||||||
<polyline points="10,9 9,9 8,9" />
|
|
||||||
</svg>
|
|
||||||
{t(defaultLocale, 'hero.cta')}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/services"
|
|
||||||
className="inline-flex items-center gap-2 rounded-md border-2 border-white px-6 py-3 text-base font-semibold text-white transition-colors hover:bg-white/10"
|
|
||||||
>
|
|
||||||
{t(defaultLocale, 'cta.learn_more')}
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
|
||||||
<polyline points="12,5 19,12 12,19" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 gap-x-3.5 text-sm text-white/65">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
||||||
</svg>
|
|
||||||
<span>RBQ {general.rbq}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-white/30" aria-hidden>·</span>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
|
||||||
<circle cx="12" cy="10" r="3" />
|
|
||||||
</svg>
|
|
||||||
<span>{general.serviceArea}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-white/30" aria-hidden>·</span>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
||||||
<polyline points="20 6 9 17 4 12" />
|
|
||||||
</svg>
|
|
||||||
<span>Soumission gratuite</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden items-center justify-center lg:flex" aria-hidden>
|
|
||||||
<img
|
|
||||||
src="/images/logo.png"
|
|
||||||
alt=""
|
|
||||||
width={320}
|
|
||||||
height={320}
|
|
||||||
loading="eager"
|
|
||||||
className="h-[280px] w-[280px] object-contain opacity-[0.18] brightness-0 invert"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Dialog, DialogPanel } from '@headlessui/react'
|
|
||||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{ name: 'Accueil', href: '/' },
|
|
||||||
{ name: 'Services', href: '/services' },
|
|
||||||
{ name: 'Contact', href: '/contact' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function SimpleCenteredWithBackgroundImage() {
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white dark:bg-gray-900">
|
|
||||||
<header className="absolute inset-x-0 top-0 z-50">
|
|
||||||
<nav aria-label="Global" className="flex items-center justify-between p-6 lg:px-8">
|
|
||||||
<div className="flex lg:flex-1">
|
|
||||||
<a href="/" className="-m-1.5 p-1.5">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex lg:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(true)}
|
|
||||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-500 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
<Bars3Icon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:gap-x-12">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a key={item.name} href={item.href} className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:underline underline-offset-4 decoration-2">
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen} className="lg:hidden">
|
|
||||||
<div className="fixed inset-0 z-50" />
|
|
||||||
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<a href="/" className="-m-1.5 p-1.5">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
<XMarkIcon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flow-root">
|
|
||||||
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
|
||||||
<div className="space-y-2 py-6">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="py-6">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</Dialog>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="relative isolate overflow-hidden pt-14">
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2830&q=80&blend=111827&sat=-100&exp=15&blend-mode=multiply"
|
|
||||||
className="absolute inset-0 -z-10 size-full object-cover not-dark:hidden"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2830&q=80&blend=fff&sat=-100&exp=15&blend-mode=overlay"
|
|
||||||
className="absolute inset-0 -z-10 size-full object-cover opacity-10 dark:hidden"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
clipPath:
|
|
||||||
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
|
||||||
}}
|
|
||||||
className="relative left-[calc(50%-11rem)] aspect-1155/678 w-144.5 -translate-x-1/2 rotate-30 bg-linear-to-tr from-[#ff80b5] to-[#9089fc] opacity-20 sm:left-[calc(50%-30rem)] sm:w-288.75"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
|
|
||||||
<div className="hidden sm:mb-8 sm:flex sm:justify-center">
|
|
||||||
<div className="relative rounded-full px-3 py-1 text-sm/6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20 dark:text-gray-400 dark:ring-white/10 dark:hover:ring-white/20">
|
|
||||||
Announcing our next round of funding.{' '}
|
|
||||||
<a href="#" className="font-semibold text-indigo-600 dark:text-indigo-400">
|
|
||||||
<span aria-hidden="true" className="absolute inset-0" />
|
|
||||||
Read more <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-5xl font-semibold tracking-tight text-balance text-gray-900 sm:text-7xl dark:text-white">
|
|
||||||
Data to enrich your online business
|
|
||||||
</h1>
|
|
||||||
<p className="mt-8 text-lg font-medium text-pretty text-gray-600 sm:text-xl/8 dark:text-gray-400">
|
|
||||||
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet
|
|
||||||
fugiat veniam occaecat.
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
|
|
||||||
>
|
|
||||||
Get started
|
|
||||||
</a>
|
|
||||||
<a href="#" className="text-sm/6 font-semibold text-gray-900 dark:text-white">
|
|
||||||
Learn more <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
clipPath:
|
|
||||||
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
|
||||||
}}
|
|
||||||
className="relative left-[calc(50%+3rem)] aspect-1155/678 w-144.5 -translate-x-1/2 bg-linear-to-tr from-[#ff80b5] to-[#9089fc] opacity-20 sm:left-[calc(50%+36rem)] sm:w-288.75"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Dialog, DialogPanel } from "@headlessui/react";
|
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
||||||
import Autoplay from "embla-carousel-autoplay";
|
|
||||||
import {
|
|
||||||
Carousel,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
import { defaultLocale } from "../../../i18n/config";
|
|
||||||
import { t } from "../../../i18n/utils";
|
|
||||||
import general from "../../../content/settings/general.json";
|
|
||||||
import photo1 from "@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp";
|
|
||||||
import photo2 from "@/assets/photos/the-attic-of-a-home-with-white-ceilings-and-white-2023-11-27-05-27-44-utc.webp";
|
|
||||||
import photo3 from "@/assets/photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp";
|
|
||||||
import photo4 from "@/assets/photos/a-bedroom-with-white-walls-and-a-bed-and-a-2023-11-27-05-25-30-utc.webp";
|
|
||||||
import photo5 from "@/assets/photos/empty-room-with-window-2023-11-27-05-08-12-utc.webp";
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{ name: "Landing 1", href: "/landing-1" },
|
|
||||||
{ name: "Landing 2", href: "/landing-2" },
|
|
||||||
{ name: "Landing 3", href: "/landing-3" },
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
const slides = [
|
|
||||||
{ src: photo1.src, alt: "Peinture intérieure" },
|
|
||||||
{ src: photo2.src, alt: "Décoration murale" },
|
|
||||||
{ src: photo3.src, alt: "Finition de qualité" },
|
|
||||||
{ src: photo4.src, alt: "Chambre" },
|
|
||||||
{ src: photo5.src, alt: "Pièce lumineuse" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const autoplayPlugin = Autoplay({
|
|
||||||
delay: 5000,
|
|
||||||
stopOnInteraction: false,
|
|
||||||
stopOnMouseEnter: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function SimpleCenteredWithImageSlider() {
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white dark:bg-gray-900">
|
|
||||||
<header className="absolute inset-x-0 top-0 z-50">
|
|
||||||
<nav
|
|
||||||
aria-label="Global"
|
|
||||||
className="flex items-center justify-between p-6 lg:px-8"
|
|
||||||
>
|
|
||||||
<div className="flex lg:flex-1">
|
|
||||||
<a href="/" className="flex items-center bg-[white] rounded-3xl shadow-md
|
|
||||||
ps-0.5 pe-2 pt-1 pb-0.5
|
|
||||||
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
|
||||||
|
|
||||||
">
|
|
||||||
<div className="p-1 bg-[white] rounded-full ">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">{general.brandName}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex lg:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(true)}
|
|
||||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-[var(--color-brand-100)] hover:text-[var(--color-brand-300)]"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
<Bars3Icon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:gap-x-12">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="rounded-md bg-[var(--color-brand-600)] px-4 py-2 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] transition-colors"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<Dialog
|
|
||||||
open={mobileMenuOpen}
|
|
||||||
onClose={setMobileMenuOpen}
|
|
||||||
className="lg:hidden"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-50" />
|
|
||||||
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
|
|
||||||
<div className="flex items-center justify-between sm:justify-end lg:justify-between">
|
|
||||||
<a href="/" className="flex sm:hidden lg:flex items-center bg-[white] rounded-3xl
|
|
||||||
ps-0.5 pe-2 pt-1 pb-0.5
|
|
||||||
md:ps-1 md:pe-3 md:pt-2 md:pb-1
|
|
||||||
|
|
||||||
">
|
|
||||||
<div className="p-1 bg-[white] rounded-full ">
|
|
||||||
<span className="sr-only">Cachet Peintres Décorateurs</span>
|
|
||||||
<img
|
|
||||||
alt="Cachet Peintres Décorateurs"
|
|
||||||
src="/images/logo.png"
|
|
||||||
className="h-10 w-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-xl sm:text-2xl md:text-3xl font-semibold text-brand-600 uppercase">{general.brandName}</p>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
<XMarkIcon aria-hidden="true" className="size-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flow-root">
|
|
||||||
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-gray-500/25">
|
|
||||||
<div className="space-y-2 py-6">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="py-6">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-center bg-[var(--color-brand-600)] text-white ring-1 ring-inset ring-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)]"
|
|
||||||
>
|
|
||||||
Soumission gratuite
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</Dialog>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Full-screen hero with background carousel */}
|
|
||||||
<div className="relative isolate overflow-hidden h-screen min-h-[600px]">
|
|
||||||
{/* Background carousel */}
|
|
||||||
<Carousel
|
|
||||||
className="absolute inset-0 -z-10 size-full [&_[data-slot='carousel-content']]:h-full"
|
|
||||||
opts={{ loop: true, dragFree: false }}
|
|
||||||
plugins={[autoplayPlugin]}
|
|
||||||
>
|
|
||||||
<CarouselContent className="-ml-0 h-full">
|
|
||||||
{slides.map((slide, index) => (
|
|
||||||
<CarouselItem key={index} className="pl-0 h-full relative">
|
|
||||||
<img
|
|
||||||
src={slide.src}
|
|
||||||
alt={slide.alt}
|
|
||||||
className="size-full object-cover"
|
|
||||||
/>
|
|
||||||
{/* Dark overlay for text legibility */}
|
|
||||||
<div className="absolute inset-0 bg-black/50" />
|
|
||||||
</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>
|
|
||||||
</Carousel>
|
|
||||||
|
|
||||||
{/* Hero text content */}
|
|
||||||
<div className="relative z-10 mx-auto max-w-7xl px-6 lg:px-8 h-full flex items-center justify-center pt-14">
|
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
|
||||||
<div className="hidden sm:mb-8 sm:flex sm:justify-center">
|
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2 gap-x-3.5 rounded-full px-4 py-2 text-sm/6 text-gray-300 ring-1 ring-white/20">
|
|
||||||
<span>RBQ {general.rbq}</span>
|
|
||||||
<span className="text-white/30" aria-hidden>·</span>
|
|
||||||
<span>{general.serviceArea}</span>
|
|
||||||
<span className="text-white/30" aria-hidden>·</span>
|
|
||||||
<span>Soumission gratuite</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1
|
|
||||||
className="text-5xl font-semibold tracking-tight text-balance text-white sm:text-7xl"
|
|
||||||
style={{ fontFamily: "var(--font-display)" }}
|
|
||||||
>
|
|
||||||
{t(defaultLocale, "hero.title")}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-8 text-lg font-medium text-pretty text-gray-300 sm:text-xl/8">
|
|
||||||
{t(defaultLocale, "hero.subtitle")}
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 flex items-center justify-center">
|
|
||||||
<a
|
|
||||||
href="/soumission"
|
|
||||||
className="rounded-md bg-[var(--color-brand-600)] px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-[var(--color-brand-700)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-brand-600)]"
|
|
||||||
>
|
|
||||||
{t(defaultLocale, "hero.cta")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import imgBg from '@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp'
|
|
||||||
|
|
||||||
const features = [
|
|
||||||
{
|
|
||||||
name: 'Certifié RBQ',
|
|
||||||
description: 'Entrepreneur certifié par la Régie du Bâtiment du Québec (RBQ : 5839 8736 01). Travaux assurés et conformes aux normes.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Ponctualité et fiabilité',
|
|
||||||
description: 'Nous respectons nos engagements. Les délais sont tenus et votre projet est livré à temps, selon les standards convenus.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Matériaux de qualité',
|
|
||||||
description: 'Nous utilisons exclusivement des peintures et produits de première qualité pour un résultat durable et esthétique.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Équipe professionnelle',
|
|
||||||
description: 'Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition.',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function WithFadingImage() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[linear-gradient(to_bottom,white_0%,white_75%,var(--color-brand-50)_100%)]">
|
|
||||||
<div aria-hidden="true" className="relative">
|
|
||||||
<img
|
|
||||||
alt="Peintres décorateurs au travail"
|
|
||||||
src={imgBg.src}
|
|
||||||
className="h-96 w-full object-cover object-center "
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-t from-white" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative mx-auto -mt-12 max-w-7xl px-4 pb-16 sm:px-6 sm:pb-24 lg:px-8">
|
|
||||||
<div className="mx-auto max-w-2xl text-center lg:max-w-4xl">
|
|
||||||
<h2
|
|
||||||
className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"
|
|
||||||
style={{ fontFamily: 'var(--font-display)' }}
|
|
||||||
>
|
|
||||||
Pourquoi choisir Cachet?
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 text-gray-500" style={{ fontFamily: 'var(--font-body)' }}>
|
|
||||||
Une expertise reconnue au service de votre satisfaction
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 sm:gap-y-16 lg:max-w-none lg:grid-cols-4 lg:gap-x-8">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div key={feature.name} className="border-t-2 border-[var(--color-brand-500)] pt-4">
|
|
||||||
<dt
|
|
||||||
className="font-semibold text-[var(--color-gray-600)]"
|
|
||||||
style={{ fontFamily: 'var(--font-body)' }}
|
|
||||||
>
|
|
||||||
{feature.name}
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-2 text-sm text-gray-500 leading-relaxed">{feature.description}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,7 @@ function classNames(...classes: string[]) {
|
|||||||
|
|
||||||
export default function Grid() {
|
export default function Grid() {
|
||||||
return (
|
return (
|
||||||
<div className="relative isolate bg-brand-50 pt-24 pb-32 sm:pt-32 dark:bg-gray-900">
|
<div className="relative isolate bg-brand-50 pt-24 pb-32 sm:pt-32 dark:bg-brand-50">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute inset-x-0 top-1/2 -z-10 -translate-y-1/2 transform-gpu overflow-hidden opacity-30 blur-3xl"
|
className="absolute inset-x-0 top-1/2 -z-10 -translate-y-1/2 transform-gpu overflow-hidden opacity-30 blur-3xl"
|
||||||
@@ -2,6 +2,11 @@ import { defineCollection } from 'astro:content';
|
|||||||
import { glob } from 'astro/loaders';
|
import { glob } from 'astro/loaders';
|
||||||
import { z } from 'astro/zod';
|
import { z } from 'astro/zod';
|
||||||
|
|
||||||
|
const linkItem = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const services = defineCollection({
|
const services = defineCollection({
|
||||||
loader: glob({ pattern: '**/*.md', base: './src/content/services' }),
|
loader: glob({ pattern: '**/*.md', base: './src/content/services' }),
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
@@ -14,4 +19,185 @@ const services = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { services };
|
const navigation = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/navigation' }),
|
||||||
|
schema: z.object({
|
||||||
|
links: z.array(linkItem),
|
||||||
|
cta: z.object({ label: z.string(), href: z.string() }),
|
||||||
|
servicesSection: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
viewAll: z.string(),
|
||||||
|
empty: z.string(),
|
||||||
|
}),
|
||||||
|
pageTitles: z.object({
|
||||||
|
services: z.string(),
|
||||||
|
soumission: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const hero = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/hero' }),
|
||||||
|
schema: z.object({
|
||||||
|
pillCta: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
cta: z.string(),
|
||||||
|
ctaHref: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cta = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/cta' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
button: z.string(),
|
||||||
|
buttonHref: z.string(),
|
||||||
|
imageAlt: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contact = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/contact' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
address: z.string(),
|
||||||
|
serviceAreaLabel: z.string(),
|
||||||
|
hours: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
weekdays: z.object({ day: z.string(), time: z.string() }),
|
||||||
|
saturday: z.object({ day: z.string(), time: z.string() }),
|
||||||
|
sunday: z.object({ day: z.string(), time: z.string() }),
|
||||||
|
}),
|
||||||
|
follow: z.string(),
|
||||||
|
facebookCta: z.string(),
|
||||||
|
map: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
region: z.string(),
|
||||||
|
mapsButton: z.string(),
|
||||||
|
}),
|
||||||
|
ctaCard: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
button: z.string(),
|
||||||
|
}),
|
||||||
|
breadcrumbHome: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const soumission = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/soumission' }),
|
||||||
|
schema: z.object({
|
||||||
|
pageTitle: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
form: z.object({
|
||||||
|
ariaLabel: z.string(),
|
||||||
|
nom: z.string(),
|
||||||
|
nomPlaceholder: z.string(),
|
||||||
|
courriel: z.string(),
|
||||||
|
courrielPlaceholder: z.string(),
|
||||||
|
telephone: z.string(),
|
||||||
|
telephonePlaceholder: z.string(),
|
||||||
|
adresse: z.string(),
|
||||||
|
adressePlaceholder: z.string(),
|
||||||
|
typeService: z.string(),
|
||||||
|
typeServicePlaceholder: z.string(),
|
||||||
|
typeServiceOptions: z.object({
|
||||||
|
interieur: z.string(),
|
||||||
|
exterieur: z.string(),
|
||||||
|
commercial: z.string(),
|
||||||
|
decoration: z.string(),
|
||||||
|
autre: z.string(),
|
||||||
|
}),
|
||||||
|
message: z.string(),
|
||||||
|
messagePlaceholder: z.string(),
|
||||||
|
envoyer: z.string(),
|
||||||
|
sending: z.string(),
|
||||||
|
successTitle: z.string(),
|
||||||
|
successMessage: z.string(),
|
||||||
|
errorTitle: z.string(),
|
||||||
|
errorMessage: z.string(),
|
||||||
|
validation: z.object({
|
||||||
|
nom: z.string(),
|
||||||
|
courriel: z.string(),
|
||||||
|
telephone: z.string(),
|
||||||
|
typeService: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
sidebar: z.object({
|
||||||
|
processTitle: z.string(),
|
||||||
|
steps: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
directContactTitle: z.string(),
|
||||||
|
rbqLabel: z.string(),
|
||||||
|
}),
|
||||||
|
breadcrumbHome: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const footer = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/footer' }),
|
||||||
|
schema: z.object({
|
||||||
|
tagline: z.string(),
|
||||||
|
navTitle: z.string(),
|
||||||
|
servicesTitle: z.string(),
|
||||||
|
contactTitle: z.string(),
|
||||||
|
legal: z.string(),
|
||||||
|
rbq: z.string(),
|
||||||
|
neq: z.string(),
|
||||||
|
facebookLabel: z.string(),
|
||||||
|
serviceLinks: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const notFound = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/not-found' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
home: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pourquoiCard = z.object({
|
||||||
|
icon: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pourquoi = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.json', base: './src/content/pourquoi' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
cards: z.array(pourquoiCard),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
services,
|
||||||
|
navigation,
|
||||||
|
hero,
|
||||||
|
cta,
|
||||||
|
contact,
|
||||||
|
soumission,
|
||||||
|
footer,
|
||||||
|
notFound,
|
||||||
|
pourquoi,
|
||||||
|
};
|
||||||
|
|||||||
27
src/content/contact/fr/main.json
Normal file
27
src/content/contact/fr/main.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"title": "Contactez-nous",
|
||||||
|
"subtitle": "Nous sommes à votre disposition pour répondre à vos questions.",
|
||||||
|
"phone": "Téléphone",
|
||||||
|
"email": "Courriel",
|
||||||
|
"address": "Adresse",
|
||||||
|
"serviceAreaLabel": "Zone desservie :",
|
||||||
|
"hours": {
|
||||||
|
"title": "Heures d'ouverture",
|
||||||
|
"weekdays": { "day": "Lundi – Vendredi", "time": "7h00 – 18h00" },
|
||||||
|
"saturday": { "day": "Samedi", "time": "8h00 – 16h00" },
|
||||||
|
"sunday": { "day": "Dimanche", "time": "Fermé" }
|
||||||
|
},
|
||||||
|
"follow": "Suivez-nous",
|
||||||
|
"facebookCta": "Suivez-nous sur Facebook",
|
||||||
|
"map": {
|
||||||
|
"title": "Notre territoire de service",
|
||||||
|
"region": "Laval & Rive-Nord, Québec",
|
||||||
|
"mapsButton": "Voir sur Google Maps"
|
||||||
|
},
|
||||||
|
"ctaCard": {
|
||||||
|
"title": "Demandez votre soumission",
|
||||||
|
"description": "Obtenez une estimation personnalisée et gratuite pour votre projet de peinture ou décoration.",
|
||||||
|
"button": "Soumission gratuite"
|
||||||
|
},
|
||||||
|
"breadcrumbHome": "Accueil"
|
||||||
|
}
|
||||||
7
src/content/cta/fr/main.json
Normal file
7
src/content/cta/fr/main.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Prêt à transformer votre espace?",
|
||||||
|
"subtitle": "Obtenez une soumission gratuite et sans engagement dès aujourd'hui.",
|
||||||
|
"button": "Soumission gratuite",
|
||||||
|
"buttonHref": "/soumission",
|
||||||
|
"imageAlt": "Intérieur peint avec soin par Cachet Peintres Décorateurs"
|
||||||
|
}
|
||||||
16
src/content/footer/fr/main.json
Normal file
16
src/content/footer/fr/main.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tagline": "Peintres Décorateurs professionnels",
|
||||||
|
"navTitle": "Navigation",
|
||||||
|
"servicesTitle": "Nos services",
|
||||||
|
"contactTitle": "Contact",
|
||||||
|
"legal": "© {year} 9500-5609 Québec Inc. Tous droits réservés.",
|
||||||
|
"rbq": "RBQ : 5839 8736 01",
|
||||||
|
"neq": "NEQ : 9500-5609",
|
||||||
|
"facebookLabel": "Facebook",
|
||||||
|
"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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
7
src/content/hero/fr/main.json
Normal file
7
src/content/hero/fr/main.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"pillCta": "Soumission gratuite",
|
||||||
|
"title": "Peintres Décorateurs à votre service",
|
||||||
|
"subtitle": "Travaux de peinture résidentielle et commerciale, décoration intérieure et extérieure. Qualité professionnelle sur la Rive-Nord et Laval.",
|
||||||
|
"cta": "Soumission gratuite",
|
||||||
|
"ctaHref": "/soumission"
|
||||||
|
}
|
||||||
22
src/content/navigation/fr/main.json
Normal file
22
src/content/navigation/fr/main.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{ "name": "Accueil", "href": "/" },
|
||||||
|
{ "name": "Services", "href": "/services" },
|
||||||
|
{ "name": "Contact", "href": "/contact" },
|
||||||
|
{ "name": "Soumission", "href": "/soumission" }
|
||||||
|
],
|
||||||
|
"cta": {
|
||||||
|
"label": "Soumission gratuite",
|
||||||
|
"href": "/soumission"
|
||||||
|
},
|
||||||
|
"servicesSection": {
|
||||||
|
"title": "Nos Services",
|
||||||
|
"subtitle": "Des solutions complètes en peinture et décoration",
|
||||||
|
"viewAll": "Voir tous nos services",
|
||||||
|
"empty": "Aucun service disponible pour le moment."
|
||||||
|
},
|
||||||
|
"pageTitles": {
|
||||||
|
"services": "Services",
|
||||||
|
"soumission": "Soumission"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/content/not-found/fr/main.json
Normal file
5
src/content/not-found/fr/main.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "Page introuvable",
|
||||||
|
"message": "La page que vous cherchez n'existe pas ou a été déplacée.",
|
||||||
|
"home": "Retour à l'accueil"
|
||||||
|
}
|
||||||
@@ -21,6 +21,16 @@
|
|||||||
"icon": "team",
|
"icon": "team",
|
||||||
"title": "Équipe professionnelle",
|
"title": "Équipe professionnelle",
|
||||||
"description": "Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition."
|
"description": "Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "location",
|
||||||
|
"title": "Localisation",
|
||||||
|
"description": "Nous sommes situés à Laval et sur la Rive-Nord. Nous pouvons vous accompagner dans vos projets de peinture et décoration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "star",
|
||||||
|
"title": "Excellence",
|
||||||
|
"description": "Nous sommes une entreprise de peinture et décoration de qualité. Nous nous engageons à vous fournir un service de qualité."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
60
src/content/soumission/fr/main.json
Normal file
60
src/content/soumission/fr/main.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Soumission",
|
||||||
|
"title": "Demande de soumission",
|
||||||
|
"subtitle": "Remplissez le formulaire ci-dessous et nous vous contacterons dans les plus brefs délais.",
|
||||||
|
"form": {
|
||||||
|
"ariaLabel": "Demande de soumission",
|
||||||
|
"nom": "Nom complet",
|
||||||
|
"nomPlaceholder": "Jean Tremblay",
|
||||||
|
"courriel": "Courriel",
|
||||||
|
"courrielPlaceholder": "jean@exemple.com",
|
||||||
|
"telephone": "Téléphone",
|
||||||
|
"telephonePlaceholder": "(450) 555-1234",
|
||||||
|
"adresse": "Adresse des travaux",
|
||||||
|
"adressePlaceholder": "123 rue Principale, Laval, QC",
|
||||||
|
"typeService": "Type de service",
|
||||||
|
"typeServicePlaceholder": "Sélectionnez un service",
|
||||||
|
"typeServiceOptions": {
|
||||||
|
"interieur": "Peinture intérieure",
|
||||||
|
"exterieur": "Peinture extérieure",
|
||||||
|
"commercial": "Peinture commerciale",
|
||||||
|
"decoration": "Décoration intérieure",
|
||||||
|
"autre": "Autre"
|
||||||
|
},
|
||||||
|
"message": "Détails du projet",
|
||||||
|
"messagePlaceholder": "Décrivez votre projet (pièces à peindre, superficie approximative, délai souhaité...)",
|
||||||
|
"envoyer": "Envoyer ma demande",
|
||||||
|
"sending": "Envoi en cours...",
|
||||||
|
"successTitle": "Demande envoyée avec succès!",
|
||||||
|
"successMessage": "Merci pour votre demande. Nous vous contacterons dans les 24 à 48 heures.",
|
||||||
|
"errorTitle": "Erreur lors de l'envoi",
|
||||||
|
"errorMessage": "Une erreur s'est produite. Veuillez réessayer ou nous contacter directement.",
|
||||||
|
"validation": {
|
||||||
|
"nom": "Le nom est requis.",
|
||||||
|
"courriel": "Une adresse courriel valide est requise.",
|
||||||
|
"telephone": "Le numéro de téléphone est requis.",
|
||||||
|
"typeService": "Veuillez sélectionner un type de service.",
|
||||||
|
"message": "Un message est requis."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"processTitle": "Comment ça fonctionne",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"title": "Remplissez le formulaire",
|
||||||
|
"description": "Décrivez votre projet en quelques mots — type de travaux, superficie et délai souhaité."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Nous vous contactons",
|
||||||
|
"description": "Un de nos experts vous rappelle dans les 24–48 heures pour planifier une visite gratuite."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Soumission détaillée",
|
||||||
|
"description": "Vous recevez une soumission claire et sans engagement, avec tous les détails du projet."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"directContactTitle": "Ou contactez-nous directement",
|
||||||
|
"rbqLabel": "Numéro RBQ :"
|
||||||
|
},
|
||||||
|
"breadcrumbHome": "Accueil"
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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' },
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
export const ui = {
|
|
||||||
fr: {
|
|
||||||
// Navigation
|
|
||||||
'nav.accueil': 'Accueil',
|
|
||||||
'nav.services': 'Services',
|
|
||||||
'nav.contact': 'Contact',
|
|
||||||
'nav.soumission': 'Soumission',
|
|
||||||
|
|
||||||
// CTA
|
|
||||||
'cta.soumission': 'Soumission gratuite',
|
|
||||||
'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': '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': 'Soumission gratuite',
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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}`;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
||||||
<main id="main-content">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</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>
|
|
||||||
44
src/layouts/base.astro
Normal file
44
src/layouts/base.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
|
import SEOHead from '@/components/seo-head.astro';
|
||||||
|
import Header from '@/components/header';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
keywords?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
canonical?: string;
|
||||||
|
noindex?: boolean;
|
||||||
|
pageType?: 'website' | 'article';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = Astro.props;
|
||||||
|
const locale = getLocale(Astro);
|
||||||
|
|
||||||
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!navEntry) throw new Error(`Missing navigation content for locale: ${locale}`);
|
||||||
|
|
||||||
|
const nav = navEntry.data;
|
||||||
|
const navigationItems = nav.links.map((link) => ({ name: link.name, href: link.href }));
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={locale}>
|
||||||
|
<head>
|
||||||
|
<SEOHead {...props} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header
|
||||||
|
navigation={navigationItems}
|
||||||
|
ctaLabel={nav.cta.label}
|
||||||
|
ctaHref={nav.cta.href}
|
||||||
|
client:load
|
||||||
|
/>
|
||||||
|
<main id="main-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
src/lib/locale.ts
Normal file
12
src/lib/locale.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** Matches Astro `i18n.defaultLocale` — single source for fallback when `Astro.currentLocale` is unset. */
|
||||||
|
export const defaultLocale = 'fr' as const;
|
||||||
|
|
||||||
|
export type SiteLocale = typeof defaultLocale | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active locale for this request (from Astro i18n routing when enabled).
|
||||||
|
* Falls back to `defaultLocale` for static builds without prefixed locale in URL.
|
||||||
|
*/
|
||||||
|
export function getLocale(astro: { currentLocale?: string | undefined }): SiteLocale {
|
||||||
|
return astro.currentLocale ?? defaultLocale;
|
||||||
|
}
|
||||||
@@ -1,64 +1,28 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
import Layout from '@/layouts/base.astro';
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
|
const [entry] = await getCollection('notFound', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!entry) throw new Error(`Missing not-found content for locale: ${locale}`);
|
||||||
|
|
||||||
|
const notFound = entry.data;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<Layout title={notFound.title} noindex={true}>
|
||||||
title={t(lang, '404.title')}
|
|
||||||
noindex={true}
|
|
||||||
>
|
|
||||||
<section class="not-found-section">
|
<section class="not-found-section">
|
||||||
<div class="container-site not-found-inner">
|
<div class="container-site not-found-inner">
|
||||||
<div class="not-found-number" aria-hidden="true">404</div>
|
<div class="not-found-number" aria-hidden="true">404</div>
|
||||||
<h1 class="not-found-title">{t(lang, '404.title')}</h1>
|
<h1 class="not-found-title">{notFound.title}</h1>
|
||||||
<p class="not-found-message">{t(lang, '404.message')}</p>
|
<p class="not-found-message">{notFound.message}</p>
|
||||||
<a href="/" class="btn-primary">
|
<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">
|
<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"/>
|
<line x1="19" y1="12" x2="5" y2="12"/>
|
||||||
<polyline points="12,19 5,12 12,5"/>
|
<polyline points="12,19 5,12 12,5"/>
|
||||||
</svg>
|
</svg>
|
||||||
{t(lang, '404.home')}
|
{notFound.home}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</Layout>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -1,35 +1,43 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import Layout from '@/layouts/base.astro';
|
||||||
import CTABanner from '../components/CTABanner.astro';
|
import CTABanner from '@/components/ui/cta-banner';
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
import general from '../content/settings/general.json';
|
import general from '../content/settings/general.json';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
return Astro.redirect('/404');
|
|
||||||
|
|
||||||
|
const [contactEntry] = await getCollection('contact', (e) => e.id === `${locale}/main`);
|
||||||
|
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||||
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!contactEntry || !ctaEntry || !navEntry) throw new Error(`Missing contact content for locale: ${locale}`);
|
||||||
|
|
||||||
|
const contact = contactEntry.data;
|
||||||
|
const ctaData = ctaEntry.data;
|
||||||
|
const homeLink = navEntry.data.links.find((l) => l.href === '/')?.name ?? contact.breadcrumbHome;
|
||||||
|
|
||||||
|
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<Layout
|
||||||
title={t(lang, 'contact.title')}
|
title={contact.title}
|
||||||
description={`Contactez Cachet Peintres Décorateurs — ${general.phone} — ${general.email}. Nous servons Laval, la Rive-Nord et les environs.`}
|
description={`Contactez Cachet Peintres Décorateurs — ${general.phone} — ${general.email}. Nous servons Laval, la Rive-Nord et les environs.`}
|
||||||
canonical="/contact"
|
canonical="/contact"
|
||||||
>
|
>
|
||||||
<!-- Page header -->
|
|
||||||
<section class="page-header">
|
<section class="page-header">
|
||||||
<div class="container-site">
|
<div class="container-site">
|
||||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
<a href="/">{homeLink}</a>
|
||||||
<span aria-hidden="true">›</span>
|
<span aria-hidden="true">›</span>
|
||||||
<span aria-current="page">{t(lang, 'contact.title')}</span>
|
<span aria-current="page">{contact.title}</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="page-title">{t(lang, 'contact.title')}</h1>
|
<h1 class="page-title">{contact.title}</h1>
|
||||||
<p class="page-subtitle">{t(lang, 'contact.subtitle')}</p>
|
<p class="page-subtitle">{contact.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="contact-section">
|
<section class="contact-section">
|
||||||
<div class="container-site contact-grid">
|
<div class="container-site contact-grid">
|
||||||
<!-- Contact info -->
|
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="contact-card">
|
<div class="contact-card">
|
||||||
<div class="contact-card-icon">
|
<div class="contact-card-icon">
|
||||||
@@ -38,7 +46,7 @@ return Astro.redirect('/404');
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{t(lang, 'contact.phone')}</h3>
|
<h3>{contact.phone}</h3>
|
||||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="contact-value contact-link">
|
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="contact-value contact-link">
|
||||||
{general.phone}
|
{general.phone}
|
||||||
</a>
|
</a>
|
||||||
@@ -53,7 +61,7 @@ return Astro.redirect('/404');
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{t(lang, 'contact.email')}</h3>
|
<h3>{contact.email}</h3>
|
||||||
<a href={`mailto:${general.email}`} class="contact-value contact-link">
|
<a href={`mailto:${general.email}`} class="contact-value contact-link">
|
||||||
{general.email}
|
{general.email}
|
||||||
</a>
|
</a>
|
||||||
@@ -68,35 +76,33 @@ return Astro.redirect('/404');
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{t(lang, 'contact.address')}</h3>
|
<h3>{contact.address}</h3>
|
||||||
<p class="contact-value">{general.address}</p>
|
<p class="contact-value">{general.address}</p>
|
||||||
<p class="contact-service-area">Zone desservie : {general.serviceArea}</p>
|
<p class="contact-service-area">{contact.serviceAreaLabel} {general.serviceArea}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hours -->
|
|
||||||
<div class="contact-hours">
|
<div class="contact-hours">
|
||||||
<h3 class="contact-hours-title">{t(lang, 'contact.hours')}</h3>
|
<h3 class="contact-hours-title">{contact.hours.title}</h3>
|
||||||
<ul class="hours-list">
|
<ul class="hours-list">
|
||||||
<li>
|
<li>
|
||||||
<span class="hours-day">Lundi – Vendredi</span>
|
<span class="hours-day">{contact.hours.weekdays.day}</span>
|
||||||
<span class="hours-time">7h00 – 18h00</span>
|
<span class="hours-time">{contact.hours.weekdays.time}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="hours-day">Samedi</span>
|
<span class="hours-day">{contact.hours.saturday.day}</span>
|
||||||
<span class="hours-time">8h00 – 16h00</span>
|
<span class="hours-time">{contact.hours.saturday.time}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="hours-day">Dimanche</span>
|
<span class="hours-day">{contact.hours.sunday.day}</span>
|
||||||
<span class="hours-time closed">Fermé</span>
|
<span class="hours-time closed">{contact.hours.sunday.time}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Social -->
|
|
||||||
{general.facebook && (
|
{general.facebook && (
|
||||||
<div class="contact-social">
|
<div class="contact-social">
|
||||||
<h3 class="contact-social-title">{t(lang, 'contact.follow')}</h3>
|
<h3 class="contact-social-title">{contact.follow}</h3>
|
||||||
<a
|
<a
|
||||||
href={general.facebook}
|
href={general.facebook}
|
||||||
class="social-link"
|
class="social-link"
|
||||||
@@ -106,19 +112,17 @@ return Astro.redirect('/404');
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
<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"/>
|
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Suivez-nous sur Facebook
|
{contact.facebookCta}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<!-- Legal -->
|
|
||||||
<div class="contact-legal">
|
<div class="contact-legal">
|
||||||
<p>RBQ : {general.rbq}</p>
|
<p>RBQ : {general.rbq}</p>
|
||||||
<p>NEQ : {general.neq} ({general.legalName})</p>
|
<p>NEQ : {general.neq} ({general.legalName})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<div class="contact-action-panel">
|
<div class="contact-action-panel">
|
||||||
<div class="contact-cta-card">
|
<div class="contact-cta-card">
|
||||||
<div class="contact-cta-icon">
|
<div class="contact-cta-icon">
|
||||||
@@ -129,23 +133,22 @@ return Astro.redirect('/404');
|
|||||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2>Demandez votre soumission</h2>
|
<h2>{contact.ctaCard.title}</h2>
|
||||||
<p>Obtenez une estimation personnalisée et gratuite pour votre projet de peinture ou décoration.</p>
|
<p>{contact.ctaCard.description}</p>
|
||||||
<a href="/soumission" class="btn-primary contact-soumission-btn">
|
<a href="/soumission" class="btn-primary contact-soumission-btn">
|
||||||
{t(lang, 'cta.soumission')}
|
{contact.ctaCard.button}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Map placeholder -->
|
|
||||||
<div class="contact-map">
|
<div class="contact-map">
|
||||||
<h3 class="contact-map-title">{t(lang, 'contact.map.title')}</h3>
|
<h3 class="contact-map-title">{contact.map.title}</h3>
|
||||||
<div class="map-placeholder">
|
<div class="map-placeholder">
|
||||||
<div class="map-placeholder-inner">
|
<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">
|
<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"/>
|
<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"/>
|
<circle cx="12" cy="10" r="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
<p>Laval & Rive-Nord, Québec</p>
|
<p>{contact.map.region}</p>
|
||||||
<span class="map-service-area">{general.serviceArea}</span>
|
<span class="map-service-area">{general.serviceArea}</span>
|
||||||
<a
|
<a
|
||||||
href={`https://maps.google.com/?q=Laval+Quebec`}
|
href={`https://maps.google.com/?q=Laval+Quebec`}
|
||||||
@@ -153,7 +156,7 @@ return Astro.redirect('/404');
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="btn-outline map-btn"
|
class="btn-outline map-btn"
|
||||||
>
|
>
|
||||||
Voir sur Google Maps
|
{contact.map.mapsButton}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,295 +165,13 @@ return Astro.redirect('/404');
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<CTABanner />
|
<CTABanner
|
||||||
</BaseLayout>
|
client:load
|
||||||
|
title={ctaData.title}
|
||||||
<style>
|
subtitle={ctaData.subtitle}
|
||||||
.page-header {
|
buttonText={ctaData.button}
|
||||||
background-color: var(--color-brand-50);
|
buttonHref={ctaData.buttonHref}
|
||||||
border-bottom: 1.5px solid var(--color-brand-100);
|
imageSrc={ctaImage.src}
|
||||||
padding: 3rem 0 2.5rem;
|
imageAlt={ctaData.imageAlt}
|
||||||
}
|
/>
|
||||||
|
</Layout>
|
||||||
.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>
|
|
||||||
|
|||||||
@@ -1,30 +1,118 @@
|
|||||||
---
|
---
|
||||||
import Hero from '../components/Hero.astro';
|
import { getCollection } from 'astro:content';
|
||||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
import { getLocale } from '@/lib/locale';
|
||||||
import WhyUs from '../components/WhyUs.astro';
|
|
||||||
import CTABanner from '../components/CTABanner.astro';
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import general from '../content/settings/general.json';
|
import general from '../content/settings/general.json';
|
||||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
import seo from '../content/settings/seo.json';
|
||||||
import Header from '../components/Header.astro';
|
import Layout from '@/layouts/base.astro';
|
||||||
import Testimonials from '../components/ui/testimonials/Grid';
|
import Hero from '@/components/ui/hero';
|
||||||
import Footer from '../components/Footer.astro';
|
import Services from '@/components/ui/services';
|
||||||
const lang = getLangFromUrl(Astro.url);
|
import WhyChooseUs from '@/components/ui/why-choose-us';
|
||||||
|
import CTABanner from '@/components/ui/cta-banner';
|
||||||
|
import Footer from '@/components/footer.astro';
|
||||||
|
import Testimonials from '@/components/ui/testimonials';
|
||||||
|
|
||||||
|
const locale = getLocale(Astro);
|
||||||
|
|
||||||
|
const [heroEntry] = await getCollection('hero', (e) => e.id === `${locale}/main`);
|
||||||
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
|
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||||
|
const [pourquoiEntry] = await getCollection('pourquoi', (e) => e.id === `${locale}/main`);
|
||||||
|
|
||||||
|
if (!heroEntry || !navEntry || !ctaEntry || !pourquoiEntry) {
|
||||||
|
throw new Error(`Missing content collections for locale: ${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroData = heroEntry.data;
|
||||||
|
const nav = navEntry.data;
|
||||||
|
const ctaData = ctaEntry.data;
|
||||||
|
const pourquoiData = pourquoiEntry.data;
|
||||||
|
|
||||||
|
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||||
|
|
||||||
|
const allServices = await getCollection('services', (s) => s.id.startsWith(`${locale}/`));
|
||||||
|
const services = allServices.sort((a, b) => a.data.order - b.data.order);
|
||||||
|
|
||||||
|
const serviceImages: Record<string, { src: import('astro').ImageMetadata; alt: string }> = {
|
||||||
|
interior: {
|
||||||
|
src: (await import('@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp')).default,
|
||||||
|
alt: 'Salon avec peinture intérieure soignée',
|
||||||
|
},
|
||||||
|
exterior: {
|
||||||
|
src: (await import('@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp')).default,
|
||||||
|
alt: 'Maison avec peinture extérieure',
|
||||||
|
},
|
||||||
|
commercial: {
|
||||||
|
src: (await import('@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp')).default,
|
||||||
|
alt: 'Espace commercial peint',
|
||||||
|
},
|
||||||
|
decoration: {
|
||||||
|
src: (await import('@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg')).default,
|
||||||
|
alt: 'Décoration intérieure et murale',
|
||||||
|
},
|
||||||
|
renovation: {
|
||||||
|
src: (await import('@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp')).default,
|
||||||
|
alt: 'Rénovation et finitions',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const features = services.map((service) => {
|
||||||
|
const icon = service.data.icon ?? 'interior';
|
||||||
|
const img = serviceImages[icon] ?? serviceImages.interior;
|
||||||
|
const imageSrc = (img.src as import('astro').ImageMetadata).src;
|
||||||
|
return {
|
||||||
|
name: service.data.title,
|
||||||
|
description: service.data.description,
|
||||||
|
imageSrc,
|
||||||
|
imageAlt: img.alt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const heroContent = {
|
||||||
|
pillCta: heroData.pillCta,
|
||||||
|
title: heroData.title,
|
||||||
|
subtitle: heroData.subtitle,
|
||||||
|
cta: heroData.cta,
|
||||||
|
ctaHref: heroData.ctaHref,
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<Layout
|
||||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
title={seo.defaultTitle}
|
||||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
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="/"
|
canonical="/"
|
||||||
>
|
>
|
||||||
<Header />
|
<Hero client:load content={heroContent} />
|
||||||
<Hero />
|
<Services
|
||||||
<ServicesPreview />
|
client:load
|
||||||
<WhyUs />
|
features={features}
|
||||||
|
title={nav.servicesSection.title}
|
||||||
|
subtitle={nav.servicesSection.subtitle}
|
||||||
|
/>
|
||||||
<Testimonials client:load />
|
<Testimonials client:load />
|
||||||
<CTABanner />
|
|
||||||
|
<WhyChooseUs
|
||||||
|
client:load
|
||||||
|
title={pourquoiData.title}
|
||||||
|
subtitle={pourquoiData.subtitle}
|
||||||
|
cards={pourquoiData.cards}
|
||||||
|
images={{
|
||||||
|
main: (await import('@/assets/Website Content/Photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp')).default.src,
|
||||||
|
mainAlt: 'Peintres décorateurs au travail',
|
||||||
|
detail2: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp')).default.src,
|
||||||
|
detail2Alt: 'Peintres décorateurs au travail',
|
||||||
|
detail1: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-04-59-54-utc.webp')).default.src,
|
||||||
|
detail1Alt: 'Peintres décorateurs au travail',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CTABanner
|
||||||
|
client:load
|
||||||
|
title={ctaData.title}
|
||||||
|
subtitle={ctaData.subtitle}
|
||||||
|
buttonText={ctaData.button}
|
||||||
|
buttonHref={ctaData.buttonHref}
|
||||||
|
imageSrc={ctaImage.src}
|
||||||
|
imageAlt={ctaData.imageAlt}
|
||||||
|
/>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
</Layout>
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
import Hero from '../components/Hero.astro';
|
|
||||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
|
||||||
import WhyUs from '../components/WhyUs.astro';
|
|
||||||
import CTABanner from '../components/CTABanner.astro';
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import general from '../content/settings/general.json';
|
|
||||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
||||||
import Header from '../components/Header.astro';
|
|
||||||
import Testimonials from '../components/ui/testimonials/Grid';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
|
||||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
|
||||||
canonical="/"
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
<Hero />
|
|
||||||
<ServicesPreview />
|
|
||||||
<WhyUs />
|
|
||||||
<Testimonials client:load />
|
|
||||||
<CTABanner />
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
import Hero from '../components/Hero.astro';
|
|
||||||
import ServicesPreview from '../components/ServicesPreview.astro';
|
|
||||||
import CTABanner from '../components/CTABanner.astro';
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import general from '../content/settings/general.json';
|
|
||||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
||||||
import SimpleBackgroundVideo from '../components/ui/heroes/SimpleBackgroundVideo';
|
|
||||||
import TwoRowBentoGridWithThreeColumnSecondRow from '@/components/ui/bento-grids/TwoRowBentoGridWithThreeColumnSecondRow';
|
|
||||||
import WithFadingImage from '@/components/ui/product-features/WithFadingImage';
|
|
||||||
import Testimonials from '../components/ui/testimonials/Grid';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
|
||||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
|
||||||
canonical="/"
|
|
||||||
>
|
|
||||||
<SimpleBackgroundVideo client:load />
|
|
||||||
<TwoRowBentoGridWithThreeColumnSecondRow client:load />
|
|
||||||
<WithFadingImage client:load />
|
|
||||||
<Testimonials client:load />
|
|
||||||
<CTABanner />
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
---
|
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
|
||||||
import general from '../content/settings/general.json';
|
|
||||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
||||||
import SimpleCenteredWithImageSlider from '@/components/ui/heroes/SimpleCenteredWithImageSlider';
|
|
||||||
import WithAlternatingSections from '@/components/ui/product-features/WithAlternatingSections';
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import WithTieredImages from '@/components/ui/product-features/WithTieredImages';
|
|
||||||
import FullWidthWithBackgroundImageAndLargeContent from '@/components/ui/promo-sections/FullWidthWithBackgroundImageAndLargeContent';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
|
||||||
|
|
||||||
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
|
||||||
|
|
||||||
const allServices = await getCollection('services');
|
|
||||||
const services = allServices
|
|
||||||
.filter(s => s.id.startsWith('fr/'))
|
|
||||||
.sort((a, b) => a.data.order - b.data.order);
|
|
||||||
|
|
||||||
const serviceImages: Record<string, { src: import('astro').ImageMetadata; alt: string }> = {
|
|
||||||
interior: {
|
|
||||||
src: (await import('@/assets/photos/empty-room-blue-wall-with-moulding-and-parquet-fl-2023-11-27-04-52-31-utc.webp')).default,
|
|
||||||
alt: 'Salon avec peinture intérieure soignée',
|
|
||||||
},
|
|
||||||
exterior: {
|
|
||||||
src: (await import('@/assets/Website Content/Photos/wooden-deck-with-sky-2023-11-27-05-29-44-utc.webp')).default,
|
|
||||||
alt: 'Maison avec peinture extérieure',
|
|
||||||
},
|
|
||||||
commercial: {
|
|
||||||
src: (await import('@/assets/Website Content/Photos/minimalist-bright-office-room-interior-design-mee-2023-11-27-05-17-04-utc.webp')).default,
|
|
||||||
alt: 'Espace commercial peint',
|
|
||||||
},
|
|
||||||
decoration: {
|
|
||||||
src: (await import('@/assets/Website Content/Photos/Mural_Jerkspot_02.jpg')).default,
|
|
||||||
alt: 'Décoration intérieure et murale',
|
|
||||||
},
|
|
||||||
renovation: {
|
|
||||||
src: (await import('@/assets/Website Content/Photos/amazing-design-of-a-room-with-doors-and-parquet-fl-2023-11-27-05-33-04-utc.webp')).default,
|
|
||||||
alt: 'Rénovation et finitions',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const features = services.map(service => {
|
|
||||||
const icon = service.data.icon ?? 'interior';
|
|
||||||
const img = serviceImages[icon] ?? serviceImages.interior;
|
|
||||||
const imageSrc = (img.src as import('astro').ImageMetadata).src;
|
|
||||||
return {
|
|
||||||
name: service.data.title,
|
|
||||||
description: service.data.description,
|
|
||||||
imageSrc,
|
|
||||||
imageAlt: img.alt,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title="Peintres Décorateurs | Laval & Rive-Nord"
|
|
||||||
description={`${general.siteName} — Peintres décorateurs professionnels à Laval et sur la Rive-Nord. Peinture résidentielle et commerciale, décoration intérieure et extérieure. Soumission gratuite.`}
|
|
||||||
canonical="/"
|
|
||||||
>
|
|
||||||
<SimpleCenteredWithImageSlider client:load />
|
|
||||||
<WithAlternatingSections
|
|
||||||
client:load
|
|
||||||
features={features}
|
|
||||||
title={t(lang, 'services.title')}
|
|
||||||
subtitle={t(lang, 'services.subtitle')}
|
|
||||||
/>
|
|
||||||
<WithTieredImages client:load
|
|
||||||
title="Pourquoi choisir Cachet?"
|
|
||||||
subtitle="Une expertise reconnue au service de votre satisfaction"
|
|
||||||
cards={[
|
|
||||||
{
|
|
||||||
icon: 'shield',
|
|
||||||
title: 'Certifié RBQ',
|
|
||||||
description: 'Entrepreneur certifié par la Régie du Bâtiment du Québec (RBQ : 5839 8736 01). Travaux assurés et conformes aux normes.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'clock',
|
|
||||||
title: 'Ponctualité et fiabilité',
|
|
||||||
description: 'Nous respectons nos engagements. Les délais sont tenus et votre projet est livré à temps, selon les standards convenus.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'team',
|
|
||||||
title: 'Équipe professionnelle',
|
|
||||||
description: 'Nos peintres décorateurs sont formés et expérimentés. Chaque détail compte pour nous, de la préparation à la finition.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'location',
|
|
||||||
title: 'Localisation',
|
|
||||||
description: 'Nous sommes situés à Laval et sur la Rive-Nord. Nous pouvons vous accompagner dans vos projets de peinture et décoration.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'star',
|
|
||||||
title: 'Excellence',
|
|
||||||
description: 'Nous sommes une entreprise de peinture et décoration de qualité. Nous nous engageons à vous fournir un service de qualité.',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
images={{
|
|
||||||
main: (await import('@/assets/Website Content/Photos/silver-and-white-apartment-interior-2023-11-27-05-32-03-utc.webp')).default.src,
|
|
||||||
mainAlt: 'Peintres décorateurs au travail',
|
|
||||||
detail2: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-05-09-01-utc.webp')).default.src,
|
|
||||||
detail2Alt: 'Peintres décorateurs au travail',
|
|
||||||
detail1: (await import('@/assets/Website Content/Photos/choosing-wall-paints-2023-11-27-04-59-54-utc.webp')).default.src,
|
|
||||||
detail1Alt: 'Peintres décorateurs au travail',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FullWidthWithBackgroundImageAndLargeContent
|
|
||||||
client:load
|
|
||||||
title={t(lang, 'cta_banner.title')}
|
|
||||||
subtitle={t(lang, 'cta_banner.subtitle')}
|
|
||||||
buttonText={t(lang, 'cta_banner.button')}
|
|
||||||
buttonHref="/soumission"
|
|
||||||
imageSrc={ctaImage.src}
|
|
||||||
imageAlt="Intérieur peint avec soin par Cachet Peintres Décorateurs"
|
|
||||||
/>
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import Layout from '@/layouts/base.astro';
|
||||||
import CTABanner from '../components/CTABanner.astro';
|
import CTABanner from '@/components/ui/cta-banner';
|
||||||
import { getCollection, render } from 'astro:content';
|
import { getCollection, render } from 'astro:content';
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getLocale } from '@/lib/locale';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
|
|
||||||
const allServices = await getCollection('services');
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
const services = allServices
|
const [ctaEntry] = await getCollection('cta', (e) => e.id === `${locale}/main`);
|
||||||
.filter(s => s.id.startsWith('fr/'))
|
if (!navEntry || !ctaEntry) throw new Error(`Missing navigation or cta for locale: ${locale}`);
|
||||||
.sort((a, b) => a.data.order - b.data.order);
|
|
||||||
|
const nav = navEntry.data;
|
||||||
|
const ctaData = ctaEntry.data;
|
||||||
|
|
||||||
|
const ctaImage = (await import('@/assets/Website Content/Photos/cozy-modern-kitchen-room-interior-design-with-dark-2024-04-01-23-13-59-utc.webp')).default;
|
||||||
|
|
||||||
|
const allServices = await getCollection('services', (s) => s.id.startsWith(`${locale}/`));
|
||||||
|
const services = allServices.sort((a, b) => a.data.order - b.data.order);
|
||||||
|
|
||||||
const icons: Record<string, string> = {
|
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>`,
|
interior: `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>`,
|
||||||
@@ -19,218 +26,70 @@ const icons: Record<string, string> = {
|
|||||||
renovation: `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
renovation: `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Astro.redirect('/404');
|
const homeLink = nav.links.find((l) => l.href === '/')?.name ?? 'Accueil';
|
||||||
|
|
||||||
|
const serviceArticles = await Promise.all(
|
||||||
|
services.map(async (service) => {
|
||||||
|
const { Content } = await render(service);
|
||||||
|
const iconSvg = service.data.icon ? (icons[service.data.icon] ?? icons.interior) : icons.interior;
|
||||||
|
const serviceId = service.id.replace(`${locale}/`, '');
|
||||||
|
return { Content, iconSvg, serviceId, service };
|
||||||
|
}),
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<Layout
|
||||||
title={t(lang, 'nav.services')}
|
title={nav.pageTitles.services}
|
||||||
description="Découvrez tous nos services de peinture et décoration à Laval et sur la Rive-Nord : peinture intérieure et extérieure, commerciale, décoration d'intérieur et rénovation."
|
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"
|
canonical="/services"
|
||||||
>
|
>
|
||||||
<!-- Page header -->
|
|
||||||
<section class="page-header">
|
<section class="page-header">
|
||||||
<div class="container-site">
|
<div class="container-site">
|
||||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
<a href="/">{homeLink}</a>
|
||||||
<span aria-hidden="true">›</span>
|
<span aria-hidden="true">›</span>
|
||||||
<span aria-current="page">{t(lang, 'nav.services')}</span>
|
<span aria-current="page">{nav.pageTitles.services}</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="page-title">{t(lang, 'services.title')}</h1>
|
<h1 class="page-title">{nav.servicesSection.title}</h1>
|
||||||
<p class="page-subtitle">{t(lang, 'services.subtitle')}</p>
|
<p class="page-subtitle">{nav.servicesSection.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Services listing -->
|
|
||||||
<section class="services-listing">
|
<section class="services-listing">
|
||||||
<div class="container-site">
|
<div class="container-site">
|
||||||
{services.length > 0 ? (
|
{serviceArticles.length > 0 ? (
|
||||||
services.map(async (service) => {
|
serviceArticles.map(({ Content, iconSvg, serviceId, service }) => (
|
||||||
const { Content } = await render(service);
|
<article class="service-detail" id={serviceId}>
|
||||||
const iconSvg = service.data.icon ? (icons[service.data.icon] ?? icons['interior']) : icons['interior'];
|
<div class="service-detail-header">
|
||||||
const serviceId = service.id.replace('fr/', '');
|
<div class="service-detail-icon" set:html={iconSvg} />
|
||||||
return (
|
<div>
|
||||||
<article class="service-detail" id={serviceId}>
|
<h2 class="service-detail-title">{service.data.title}</h2>
|
||||||
<div class="service-detail-header">
|
<p class="service-detail-lead">{service.data.description}</p>
|
||||||
<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>
|
||||||
<div class="service-detail-content prose">
|
</div>
|
||||||
<Content />
|
<div class="service-detail-content prose">
|
||||||
</div>
|
<Content />
|
||||||
<div class="service-detail-cta">
|
</div>
|
||||||
<a href="/soumission" class="btn-primary">
|
<div class="service-detail-cta">
|
||||||
{t(lang, 'cta.soumission.short')}
|
<a href="/soumission" class="btn-primary">
|
||||||
</a>
|
{nav.cta.label}
|
||||||
</div>
|
</a>
|
||||||
</article>
|
</div>
|
||||||
);
|
</article>
|
||||||
})
|
))
|
||||||
) : (
|
) : (
|
||||||
<p class="services-empty">{t(lang, 'services.empty')}</p>
|
<p class="services-empty">{nav.servicesSection.empty}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<CTABanner />
|
<CTABanner
|
||||||
</BaseLayout>
|
client:load
|
||||||
|
title={ctaData.title}
|
||||||
<style>
|
subtitle={ctaData.subtitle}
|
||||||
.page-header {
|
buttonText={ctaData.button}
|
||||||
background-color: var(--color-brand-50);
|
buttonHref={ctaData.buttonHref}
|
||||||
border-bottom: 1.5px solid var(--color-brand-100);
|
imageSrc={ctaImage.src}
|
||||||
padding: 3rem 0 2.5rem;
|
imageAlt={ctaData.imageAlt}
|
||||||
}
|
/>
|
||||||
|
</Layout>
|
||||||
.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>
|
|
||||||
|
|||||||
@@ -1,69 +1,63 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import Layout from '@/layouts/base.astro';
|
||||||
import SoumissionForm from '../components/SoumissionForm.astro';
|
import SoumissionForm from '@/components/submission-form.astro';
|
||||||
import { getLangFromUrl, t } from '../i18n/utils';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getLocale } from '@/lib/locale';
|
||||||
import general from '../content/settings/general.json';
|
import general from '../content/settings/general.json';
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const locale = getLocale(Astro);
|
||||||
return Astro.redirect('/404');
|
|
||||||
|
|
||||||
|
const [soumissionEntry] = await getCollection('soumission', (e) => e.id === `${locale}/main`);
|
||||||
|
const [navEntry] = await getCollection('navigation', (e) => e.id === `${locale}/main`);
|
||||||
|
if (!soumissionEntry || !navEntry) throw new Error(`Missing soumission content for locale: ${locale}`);
|
||||||
|
|
||||||
|
const soumission = soumissionEntry.data;
|
||||||
|
const nav = navEntry.data;
|
||||||
|
const homeLink = nav.links.find((l) => l.href === '/')?.name ?? soumission.breadcrumbHome;
|
||||||
|
const soumissionNav = nav.links.find((l) => l.href === '/soumission');
|
||||||
---
|
---
|
||||||
<BaseLayout
|
|
||||||
title={t(lang, 'nav.soumission')}
|
<Layout
|
||||||
|
title={nav.pageTitles.soumission}
|
||||||
description="Demandez votre soumission gratuite pour vos travaux de peinture ou décoration à Laval et sur la Rive-Nord. Cachet Peintres Décorateurs vous répond rapidement."
|
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"
|
canonical="/soumission"
|
||||||
>
|
>
|
||||||
<!-- Page header -->
|
|
||||||
<section class="page-header">
|
<section class="page-header">
|
||||||
<div class="container-site">
|
<div class="container-site">
|
||||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||||
<a href="/">{t(lang, 'nav.accueil')}</a>
|
<a href="/">{homeLink}</a>
|
||||||
<span aria-hidden="true">›</span>
|
<span aria-hidden="true">›</span>
|
||||||
<span aria-current="page">{t(lang, 'nav.soumission')}</span>
|
<span aria-current="page">{soumissionNav?.name ?? soumission.pageTitle}</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="page-title">{t(lang, 'soumission.title')}</h1>
|
<h1 class="page-title">{soumission.title}</h1>
|
||||||
<p class="page-subtitle">{t(lang, 'soumission.subtitle')}</p>
|
<p class="page-subtitle">{soumission.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="soumission-section">
|
<section class="soumission-section">
|
||||||
<div class="container-site soumission-grid">
|
<div class="container-site soumission-grid">
|
||||||
<!-- Form -->
|
|
||||||
<div class="soumission-form-col">
|
<div class="soumission-form-col">
|
||||||
<SoumissionForm />
|
<SoumissionForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Side info -->
|
|
||||||
<aside class="soumission-sidebar">
|
<aside class="soumission-sidebar">
|
||||||
<div class="sidebar-card">
|
<div class="sidebar-card">
|
||||||
<h2 class="sidebar-title">Comment ça fonctionne</h2>
|
<h2 class="sidebar-title">{soumission.sidebar.processTitle}</h2>
|
||||||
<ol class="process-list">
|
<ol class="process-list">
|
||||||
<li class="process-step">
|
{soumission.sidebar.steps.map((step, i) => (
|
||||||
<div class="process-num">1</div>
|
<li class="process-step">
|
||||||
<div>
|
<div class="process-num">{i + 1}</div>
|
||||||
<h3>Remplissez le formulaire</h3>
|
<div>
|
||||||
<p>Décrivez votre projet en quelques mots — type de travaux, superficie et délai souhaité.</p>
|
<h3>{step.title}</h3>
|
||||||
</div>
|
<p>{step.description}</p>
|
||||||
</li>
|
</div>
|
||||||
<li class="process-step">
|
</li>
|
||||||
<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>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-contact">
|
<div class="sidebar-contact">
|
||||||
<h3>Ou contactez-nous directement</h3>
|
<h3>{soumission.sidebar.directContactTitle}</h3>
|
||||||
<a href={`tel:${general.phone.replace(/\D/g, '')}`} class="sidebar-phone">
|
<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">
|
<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"/>
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2.18h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.18 6.18l.94-.94a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||||
@@ -78,183 +72,11 @@ return Astro.redirect('/404');
|
|||||||
{general.email}
|
{general.email}
|
||||||
</a>
|
</a>
|
||||||
<p class="sidebar-legal">
|
<p class="sidebar-legal">
|
||||||
Numéro RBQ : {general.rbq}<br />
|
{soumission.sidebar.rbqLabel} {general.rbq}<br />
|
||||||
{general.legalName}
|
{general.legalName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</Layout>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,139 +99,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user