ajout de la page formulaire
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 22s

This commit is contained in:
sarahgarcin1 2026-07-14 19:32:39 +02:00
parent 4926fa8ead
commit ebe6bb6401
10 changed files with 476 additions and 8 deletions

View file

@ -64,6 +64,9 @@ Popups expose imperatively (defineExpose: open/close/visible/handleClick); App w
- `FigureSettingsPopup.vue` (thin view) + `useFigureSettings.js` (composable) — per-figure width/height/margin/position editor for block-image & block-carte. prop `kind`('image'|'carte') selects baseClass + selector derivation + defaults store. Composable logic in named helpers: read{Width,Height,Margins,Position}FromCss (store→data), apply*, loadFromCss/restoreFromStored, parse*FromText/persistStateAsCss (manual edits). exposes handleClick(figure,event). (Merged from former ImagePopup/CartePopup.)
- `editor/FigureDefaultsSettings.vue` — default width/height panel for images & cartes. prop `kind`; image targets [.block-image,.geoformat-cover-image], carte targets [.block-carte]; writes ⊙image/carteDefaults. (Merged from former ImageSettings/CarteSettings.)
## PHP templates (public/site/templates/) — notable additions
- `form.php` — Page avec deux formulaires de contact (autonome / partenaire). POST handling natif Kirby : CSRF (`csrf()`), honeypot, `kirby()->email()`, Post/Redirect/Get (`go()`). Blueprint : `blueprints/pages/form.yml` (writer `text` + `email_to` text). CSS : `assets/css/form.css` (chargé conditionnellement dans `header-web.php`). Pas de plugin tiers.
## PHP snippets (public/site/snippets/)
- `cover-img.php` — snippet réutilisable pour les images de couverture responsives.
- `blocks/gallery.php` — rendu du bloc galerie : grille CSS avec `--gallery-cols` (14 colonnes), srcset 2 tailles, liens `class="glightbox"`. Blueprint : `blueprints/blocks/gallery.yml`. Preview panel : composant natif Kirby (`preview: gallery` dans le blueprint — ne pas créer de plugin `k-gallery-block-preview` car Kirby en fournit un). Génère un `<img>` avec 3 breakpoints `w` srcset, crop Kirby (ratio configurable via `$imgW`/`$imgH`, défaut 900×600 = 3:2), point focus panel → position nommée (crop) + `object-position` CSS. Variables : `$file`, `$alt`, `$lazy` (bool, défaut true), `$sizes` (string), `$imgW`, `$imgH`. Utilisé par home.php, projects.php, actus.php, actu.php.

159
public/assets/css/form.css Normal file
View file

@ -0,0 +1,159 @@
/* ── FORMULAIRES DE DEMANDE D'ACCOMPAGNEMENT ──────────── */
.form-intro {
max-width: 680px;
margin-bottom: 2.5rem;
}
/* Tabs */
.form-tabs {
display: flex;
flex-wrap: wrap;
gap: 0;
border-bottom: 1px solid currentColor;
margin-bottom: 2rem;
}
.form-tab {
background: none;
border: 1px solid transparent;
border-bottom: none;
cursor: pointer;
padding: 0.6rem 1.25rem;
font: inherit;
font-size: 1em;
opacity: 0.5;
transition: opacity 0.15s ease;
margin-bottom: -1px;
color: var(--nw-accent);
}
.form-tab:hover { opacity: 1; }
.form-tab.is-active {
opacity: 1;
border-color: #FFF;
background: var(--nw-accent);
color: #FFF;
}
/* Panels */
.form-panel[hidden] { display: none; }
/* Notices */
.form-notice {
padding: 0.9rem 1.1rem;
margin-bottom: 1.5rem;
border-left: 3px solid;
font-size: 0.95em;
}
.form-notice--success { border-color: #2a9d5c; background: #f0fdf4; }
.form-notice--error { border-color: #c0392b; background: #fef2f2; }
/* Fields */
.form-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 1.25rem;
}
.form-field label {
font-size: 1em;
}
.form-field label abbr,
.form-required-note abbr {
text-decoration: none;
color: #c0392b;
margin-left: 0.1em;
}
.form-field small {
font-size: 0.85em;
opacity: 0.7;
}
.form-field input,
.form-field select,
.form-field textarea {
font: inherit;
font-size: 1rem;
padding: 1rem 0.75rem;
border: 1px solid currentColor;
background: #FFF;
color: inherit;
border-radius: 0;
width: 100%;
box-sizing: border-box;
appearance: none;
-webkit-appearance: none;
}
.form-field select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='currentColor' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.25rem;
padding-top: 0.8rem;
height: 40px;
}
.form-field textarea {
resize: vertical;
min-height: 8rem;
line-height: 1.5;
}
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
outline: 2px solid currentColor;
outline-offset: 1px;
}
/* Honeypot — caché visuellement et pour l'accessibilité */
.form-hp {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
tab-index: -1;
}
/* Actions */
.form-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: 1.75rem;
flex-wrap: wrap;
}
.form-required-note {
font-size: 0.85em;
opacity: 0.65;
margin: 0;
}
.form-submit {
font: inherit;
cursor: pointer;
padding: 0.6rem 2.25rem;
background: transparent;
border: 1px solid currentColor;
color: inherit;
letter-spacing: 0.02em;
transition: background 0.15s ease, color 0.15s ease;
}
.form-submit:hover,
.form-submit:focus-visible {
background: var(--nw-accent);
color: #FFF;
}

View file

@ -192,13 +192,15 @@ body[data-template="actus"] .hp-section {
}
/* Page about */
body[data-template="about"] .hp-section {
body[data-template="about"] .hp-section,
body[data-template="form"] .hp-section {
margin-top: 100px;
border: none;
min-height: calc(100vh - 250px);
}
body[data-template="about"] .hp-section .hp-container {
body[data-template="about"] .hp-section .hp-container,
body[data-template="form"] .hp-section .hp-container {
max-width: 800px;
margin: 0 auto;
}

View file

@ -65,7 +65,8 @@
body[data-template='tutos'],
body[data-template='actus'],
body[data-template='actu'],
body[data-template='tuto'] {
body[data-template='tuto'],
body[data-template='form'] {
background: var(--w-bg);
}
@ -740,7 +741,8 @@
body[data-template="tutos"] .btn-connect,
body[data-template="tuto"] .btn-connect,
body[data-template="actus"] .btn-connect,
body[data-template="actu"] .btn-connect{
body[data-template="actu"] .btn-connect,
body[data-template="form"] .btn-connect{
position: relative;
top: -3px;
right: 0;
@ -1723,7 +1725,6 @@
.nw-geoformat-toggle:hover {
background: var(--nw-accent);
/*border-color: #fff;*/
color: #FFF;
}

View file

@ -0,0 +1,12 @@
title: Formulaire
columns:
main:
width: 2/3
sections:
fields:
type: fields
fields:
text:
label: Texte
type: writer

View file

@ -26,6 +26,7 @@ tabs:
- projects
- actus
- tutos
- form
- error
settings:
label: Paramètres

View file

@ -29,5 +29,18 @@ return [
],
'panel' => [
'css' => 'assets/css/panel.css'
],
'email' => [
'transport' => [
'type' => 'smtp',
'host' => 'ssl0.ovh.net',
'port' => 465,
'security' => true,
'auth' => true,
// 'username' => 'newsletter@ballet-de-lorraine.eu',
// 'password' => 'Tusia7p!?2026',
'username' => 'contact@sarahgarcin.com',
'password' => 'saracroche2804',
]
]
];

View file

@ -43,11 +43,16 @@
<link rel="stylesheet" href="<?= url('assets/css/projet.css') ?>">
<?php endif ?>
<!-- Styles pour site vitrine (home, projects, about, tutos, tuto, actus, actu) -->
<?php if(in_array($page->template()->name(), ['home', 'projects', 'about', 'tutos', 'tuto', 'actus', 'actu'])):?>
<!-- Styles pour site vitrine (home, projects, about, tutos, tuto, actus, actu, form) -->
<?php if(in_array($page->template()->name(), ['home', 'projects', 'about', 'tutos', 'tuto', 'actus', 'actu', 'form'])):?>
<link rel="stylesheet" href="<?= url('assets/css/home.css') ?>">
<?php endif ?>
<!-- Styles formulaires -->
<?php if($page->template() == "form"):?>
<link rel="stylesheet" href="<?= url('assets/css/form.css') ?>">
<?php endif ?>
<!-- MapLibre GL (récit web + vue carte projet) -->
<?php if($page->template() == "narrative-web" || ($page->template() == "project" && kirby()->request()->get('view') === 'map')): ?>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">

View file

@ -6,7 +6,7 @@
<nav class="hp-header__nav" id="hp-nav" aria-label="Navigation principale">
<?php
$navPages = $site->children()->listed()
->filterBy('intendedTemplate', 'in', ['actus', 'tutos', 'projects', 'about']);
->filterBy('intendedTemplate', 'in', ['actus', 'tutos', 'projects', 'about', 'form']);
?>
<?php foreach ($navPages as $navPage): ?>
<a

View file

@ -0,0 +1,272 @@
<?php
/**
* Template: form.php Formulaires de demande d'accompagnement
* Protection : CSRF (Kirby natif) + honeypot
* Envoi : kirby()->email() (PHP mail par défaut, configurable SMTP dans config.php)
*/
$success = false;
$error = null;
$activeTab = 'autonome';
$old = [];
$emailTo = 'garcinsarah@gmail.com';
// Post/Redirect/Get — succès après redirection pour éviter la re-soumission
$sent = get('sent');
if (in_array($sent, ['autonome', 'partenaire'], true)) {
$success = true;
$activeTab = $sent;
}
if (!$success && kirby()->request()->is('POST')) {
$data = kirby()->request()->data();
$formType = in_array($data['form_type'] ?? '', ['autonome', 'partenaire'], true)
? $data['form_type'] : 'autonome';
$activeTab = $formType;
if (!csrf($data['csrf_token'] ?? '')) {
$error = 'Formulaire expiré. Rafraîchissez la page et réessayez.';
} elseif (!empty($data['website'])) {
// Honeypot rempli — bot silencieusement ignoré
go($page->url() . '?sent=' . $formType);
} else {
$required = ['structure', 'nom', 'localisation', 'email', 'nature', 'titre', 'presentation'];
if ($formType === 'partenaire') {
$required[] = 'partenariat';
}
$missing = array_filter($required, fn($f) => trim($data[$f] ?? '') === '');
if (!empty($missing)) {
$error = 'Veuillez remplir tous les champs obligatoires.';
$old = $data;
} elseif (!filter_var(trim($data['email']), FILTER_VALIDATE_EMAIL)) {
$error = 'L\'adresse email n\'est pas valide.';
$old = $data;
} else {
$label = $formType === 'partenaire' ? 'Partenaire' : 'Autonome';
$lines = [
'Demande d\'accompagnement (' . $label . ')',
str_repeat('─', 40),
'Nom de la structure : ' . $data['structure'],
'Nom et prénom : ' . $data['nom'],
'Lieu : ' . $data['localisation'],
'Email : ' . $data['email'],
'Nature du projet : ' . $data['nature'],
'Titre provisoire : ' . $data['titre'],
'',
'Présentation :',
$data['presentation'],
];
if ($formType === 'partenaire') {
$lines[] = '';
$lines[] = 'Demande de partenariat :';
$lines[] = $data['partenariat'];
}
try {
kirby()->email([
'from' => 'noreply@' . parse_url(site()->url(), PHP_URL_HOST),
'replyTo' => trim($data['email']),
'to' => $emailTo,
'subject' => 'Demande d\'accompagnement (' . $label . ') — ' . $data['titre'],
'body' => implode("\n", $lines),
]);
go($page->url() . '?sent=' . $formType);
} catch (Exception $e) {
$error = 'Une erreur est survenue lors de l\'envoi. Veuillez réessayer ou nous contacter directement.';
$old = $data;
}
}
}
}
$natures = [
'médiation',
'création fictionnelle',
'création documentaire',
'création poétique',
'recherche',
'projet éducatif',
'projet d\'inclusion',
'projet économie social et solidaire',
'projet journalistique',
'projet de voyage',
'projet de valorisation patrimoniale',
];
?>
<?php snippet('header-web') ?>
<div class="hp-wrapper">
<?php snippet('nav-web') ?>
<section class="hp-section">
<div class="hp-container">
<?php if ($page->text()->isNotEmpty()): ?>
<div class="form-intro">
<?= $page->text()->kt() ?>
</div>
<?php endif ?>
<?php if ($success): ?>
<div class="form-notice form-notice--success" role="alert">
<p>Votre demande a bien été envoyée. Nous vous contacterons dans les meilleurs délais.</p>
</div>
<?php else: ?>
<div class="form-tabs" role="tablist" aria-label="Type de demande">
<?php foreach (['autonome' => 'Demande d\'accompagnement autonome', 'partenaire' => 'Demande d\'accompagnement partenaire'] as $type => $label): ?>
<button
class="form-tab<?= $activeTab === $type ? ' is-active' : '' ?>"
type="button"
role="tab"
aria-selected="<?= $activeTab === $type ? 'true' : 'false' ?>"
aria-controls="form-panel-<?= $type ?>"
data-tab="<?= $type ?>"
><?= $label ?></button>
<?php endforeach ?>
</div>
<?php foreach (['autonome', 'partenaire'] as $type): ?>
<div
id="form-panel-<?= $type ?>"
class="form-panel"
role="tabpanel"
<?= $activeTab !== $type ? 'hidden' : '' ?>
>
<?php if ($error && $activeTab === $type): ?>
<div class="form-notice form-notice--error" role="alert"><?= html($error) ?></div>
<?php endif ?>
<form method="post" action="<?= $page->url() ?>#form-panel-<?= $type ?>" novalidate>
<input type="hidden" name="csrf_token" value="<?= csrf() ?>">
<input type="hidden" name="form_type" value="<?= $type ?>">
<!-- Honeypot : invisible pour les humains, les bots le remplissent -->
<div class="form-hp" aria-hidden="true">
<label for="hp-website-<?= $type ?>">Site web</label>
<input type="text" id="hp-website-<?= $type ?>" name="website" tabindex="-1" autocomplete="off">
</div>
<?php $v = ($activeTab === $type && !empty($old)) ? $old : []; ?>
<div class="form-field">
<label for="structure-<?= $type ?>">
Nom de la structure <abbr title="Obligatoire">*</abbr>
</label>
<input type="text" id="structure-<?= $type ?>" name="structure"
value="<?= html($v['structure'] ?? '') ?>"
required maxlength="400" autocomplete="organization">
</div>
<div class="form-field">
<label for="nom-<?= $type ?>">
Nom et prénom de l'acteur du projet <abbr title="Obligatoire">*</abbr>
</label>
<input type="text" id="nom-<?= $type ?>" name="nom"
value="<?= html($v['nom'] ?? '') ?>"
required maxlength="400" autocomplete="name">
</div>
<div class="form-field">
<label for="localisation-<?= $type ?>">
Lieu du demandeur <abbr title="Obligatoire">*</abbr>
</label>
<input type="text" id="localisation-<?= $type ?>" name="localisation"
value="<?= html($v['localisation'] ?? '') ?>"
required maxlength="400">
</div>
<div class="form-field">
<label for="email-<?= $type ?>">
Adresse de messagerie <abbr title="Obligatoire">*</abbr>
</label>
<input type="email" id="email-<?= $type ?>" name="email"
value="<?= html($v['email'] ?? '') ?>"
required maxlength="400" autocomplete="email">
</div>
<div class="form-field">
<label for="nature-<?= $type ?>">
Nature du projet cartographique <abbr title="Obligatoire">*</abbr>
</label>
<select id="nature-<?= $type ?>" name="nature" required>
<option value="" disabled <?= empty($v['nature']) ? 'selected' : '' ?>>— Choisir —</option>
<?php foreach ($natures as $n): ?>
<option value="<?= html($n) ?>"<?= ($v['nature'] ?? '') === $n ? ' selected' : '' ?>><?= html($n) ?></option>
<?php endforeach ?>
</select>
</div>
<div class="form-field">
<label for="titre-<?= $type ?>">
Titre provisoire du projet <abbr title="Obligatoire">*</abbr>
</label>
<input type="text" id="titre-<?= $type ?>" name="titre"
value="<?= html($v['titre'] ?? '') ?>"
required maxlength="400">
</div>
<div class="form-field">
<label for="presentation-<?= $type ?>">
Présentation et objectifs du projet <small>(100 mots)</small>
<abbr title="Obligatoire">*</abbr>
</label>
<textarea id="presentation-<?= $type ?>" name="presentation"
rows="8" maxlength="2000" required><?= html($v['presentation'] ?? '') ?></textarea>
</div>
<?php if ($type === 'partenaire'): ?>
<div class="form-field">
<label for="partenariat-<?= $type ?>">
Pistes et/ou demandes de partenariats envisagés avec la bibliothèque
<small>(100 mots minimum)</small> <abbr title="Obligatoire">*</abbr>
</label>
<textarea id="partenariat-<?= $type ?>" name="partenariat"
rows="8" maxlength="2000" required><?= html($v['partenariat'] ?? '') ?></textarea>
</div>
<?php endif ?>
<div class="form-actions">
<p class="form-required-note"><abbr title="Obligatoire">*</abbr> Champs obligatoires</p>
<button type="submit" class="form-submit">Envoyer</button>
</div>
</form>
</div>
<?php endforeach ?>
<?php endif ?>
</div>
</section>
<?php snippet('footer-web') ?>
</div>
<script>
(function () {
var tabs = document.querySelectorAll('.form-tab');
var panels = document.querySelectorAll('.form-panel');
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
var target = tab.dataset.tab;
tabs.forEach(function (t) {
var on = t.dataset.tab === target;
t.classList.toggle('is-active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
panels.forEach(function (p) {
var on = p.id === 'form-panel-' + target;
p.classList.toggle('is-active', on);
if (on) p.removeAttribute('hidden');
else p.setAttribute('hidden', '');
});
});
});
})();
</script>
</body>
</html>