Ajouter les réseaux sociaux au menu public

This commit is contained in:
Alexandre NOEL 2026-10-11 23:33:50 +02:00
commit 9e05196440
8 changed files with 67 additions and 13 deletions

View file

@ -2,6 +2,15 @@
Toutes les modifications importantes du projet sont consignées ici. Le format s’inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/). Toutes les modifications importantes du projet sont consignées ici. Le format s’inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).
## [1.0.0-rc.33] — 2026-10-11
### Réseaux sociaux dans le menu public
- ajout des icônes des réseaux configurés immédiatement après le lien Contact ;
- masquage automatique des réseaux sans adresse renseignée ;
- ouverture sécurisée dans un nouvel onglet et libellés accessibles aux lecteurs d’écran ;
- réutilisation d’un catalogue unique entre le menu et la page Contact.
## [1.0.0-rc.32] — 2026-10-02 ## [1.0.0-rc.32] — 2026-10-02
### Statistiques par mois et par période ### Statistiques par mois et par période

View file

@ -10,6 +10,10 @@ L’import depuis Animal Shelter Manager a été conçu comme un parcours pruden
Avant la version 1.0, le projet a fait l’objet d’un nettoyage structurel et d’un découpage de ses fichiers les plus volumineux. La recherche universelle, les salles configurables, les traductions française et anglaise et une suite de contrôles isolés ont complété cette phase de stabilisation. Avant la version 1.0, le projet a fait l’objet d’un nettoyage structurel et d’un découpage de ses fichiers les plus volumineux. La recherche universelle, les salles configurables, les traductions française et anglaise et une suite de contrôles isolés ont complété cette phase de stabilisation.
## `1.0.0-rc.33` — Réseaux visibles dès la navigation
Le site public reprend désormais dans son menu les réseaux sociaux renseignés par l’association. Les icônes restent discrètes, accessibles et cohérentes avec les liens déjà présentés sur la page Contact.
## `1.0.0-rc.32` — Statistiques à la période souhaitée ## `1.0.0-rc.32` — Statistiques à la période souhaitée
Les bilans ne sont plus limités à une année complète. Une association peut désormais analyser un mois précis ou choisir librement deux dates ; les volumes d’activité, profils, coûts vétérinaires et comparaisons utilisent tous le même intervalle. Les bilans ne sont plus limités à une année complète. Une association peut désormais analyser un mois précis ou choisir librement deux dates ; les volumes d’activité, profils, coûts vétérinaires et comparaisons utilisent tous le même intervalle.

View file

@ -200,7 +200,7 @@ L’option `--apply` ne doit être utilisée qu’après lecture des rapports. L
## État du projet ## État du projet
La candidate actuelle est `1.0.0-rc.32`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`. La candidate actuelle est `1.0.0-rc.33`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`.
Le contrôle de stabilisation est disponible dans [docs/AUDIT-1.0.md](docs/AUDIT-1.0.md). Le contrôle de stabilisation est disponible dans [docs/AUDIT-1.0.md](docs/AUDIT-1.0.md).

View file

@ -1 +1 @@
1.0.0-rc.32 1.0.0-rc.33

View file

@ -31,10 +31,10 @@
<div class="col-lg-6"> <div class="col-lg-6">
<section class="card h-100"> <section class="card h-100">
<div class="card-body"> <div class="card-body">
<span class="badge bg-blue-lt mb-2">1.0.0-rc.32</span> <span class="badge bg-blue-lt mb-2">1.0.0-rc.33</span>
<h3>Statistiques sur mesure</h3> <h3>Un site public mieux connecté</h3>
<p class="text-muted">Bilans par année, par mois ou entre deux dates, avec coûts vétérinaires et <p class="text-muted">Les réseaux sociaux renseignés par l’association rejoignent la navigation,
comparaison de la période précédente.</p> avec des liens discrets, sécurisés et accessibles.</p>
</div> </div>
</section> </section>
</div> </div>

View file

@ -14,12 +14,7 @@ $obfuscate = static fn (string$value): string => implode('', array_reverse(mb_st
class="contact-obfuscated"><?= h($obfuscate($settings['association_email'])) ?></span></div> class="contact-obfuscated"><?= h($obfuscate($settings['association_email'])) ?></span></div>
<?php endif; ?></div> <?php endif; ?></div>
<div class="social-links"> <div class="social-links">
<?php $socialNetworks = [ <?php foreach ($socialNetworks as $network => [$label,$icon]):
'facebook' => ['Facebook','f'], 'instagram' => ['Instagram','◎'], 'youtube' => ['YouTube','▶'],
'tiktok' => ['TikTok','♪'], 'whatsapp' => ['WhatsApp','☎'], 'linkedin' => ['LinkedIn','in'],
'x' => ['X','𝕏'], 'snapchat' => ['Snapchat','👻'],
];
foreach ($socialNetworks as $network => [$label,$icon]):
$url = trim((string)($settings['public_site_' . $network . '_url'] ?? '')); $url = trim((string)($settings['public_site_' . $network . '_url'] ?? ''));
if ($url === '') continue; ?> if ($url === '') continue; ?>
<a class="social-link social-<?= h($network) ?>" href="<?= h($url) ?>" target="_blank" rel="noopener noreferrer"><span class="social-icon" aria-hidden="true"><?= h($icon) ?></span><span><?= h($label) ?></span></a> <a class="social-link social-<?= h($network) ?>" href="<?= h($url) ?>" target="_blank" rel="noopener noreferrer"><span class="social-icon" aria-hidden="true"><?= h($icon) ?></span><span><?= h($label) ?></span></a>

View file

@ -13,6 +13,11 @@ $activeSpecies = (string)($selectedSpecies ?? ($animal['species'] ?? ''));
$navClass = static fn (bool$active): string => $active ? ' class="active" aria-current="page"' : ''; $navClass = static fn (bool$active): string => $active ? ' class="active" aria-current="page"' : '';
$fullAddress = implode(', ', array_filter([$settings['association_address'] ?? '',trim(($settings['association_postal_code'] ?? '') . ' ' . ($settings['association_city'] ?? ''))])); $fullAddress = implode(', ', array_filter([$settings['association_address'] ?? '',trim(($settings['association_postal_code'] ?? '') . ' ' . ($settings['association_city'] ?? ''))]));
$hasContactMap = $activePage === 'contact' && is_numeric($settings['association_latitude'] ?? null) && is_numeric($settings['association_longitude'] ?? null); $hasContactMap = $activePage === 'contact' && is_numeric($settings['association_latitude'] ?? null) && is_numeric($settings['association_longitude'] ?? null);
$socialNetworks = [
'facebook' => ['Facebook','f'], 'instagram' => ['Instagram','◎'], 'youtube' => ['YouTube','▶'],
'tiktok' => ['TikTok','♪'], 'whatsapp' => ['WhatsApp','☎'], 'linkedin' => ['LinkedIn','in'],
'x' => ['X','𝕏'], 'snapchat' => ['Snapchat','👻'],
];
if (!function_exists('public_age')) { if (!function_exists('public_age')) {
function public_age(array$a): string function public_age(array$a): string
{ {
@ -170,6 +175,32 @@ if (!function_exists('public_age')) {
color: var(--accent-dark) color: var(--accent-dark)
} }
.nav .nav-social-link {
width: 36px;
height: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 36px;
color: #fff;
font-size: .9rem;
font-weight: 800;
line-height: 1;
box-shadow: 0 3px 9px rgba(0, 0, 0, .12)
}
.nav .nav-social-link:hover,
.nav .nav-social-link:focus-visible {
color: #fff;
filter: brightness(.9);
transform: translateY(-1px)
}
.nav .nav-social-snapchat,
.nav .nav-social-snapchat:hover,
.nav .nav-social-snapchat:focus-visible { color: #111 }
.nav a.active { .nav a.active {
font-weight: 800 font-weight: 800
} }
@ -493,7 +524,12 @@ if (!function_exists('public_age')) {
<a<?= $navClass($activePage === 'donation') ?> href="<?= h($siteUrl('donation')) ?>"> <a<?= $navClass($activePage === 'donation') ?> href="<?= h($siteUrl('donation')) ?>">
<?= h(t('public_site.donation')) ?></a> <?= h(t('public_site.donation')) ?></a>
<a<?= $navClass($activePage === 'contact') ?> href="<?= h($siteUrl('contact')) ?>"> <a<?= $navClass($activePage === 'contact') ?> href="<?= h($siteUrl('contact')) ?>">
<?= h(t('public_site.contact')) ?></a> <?= h(t('public_site.contact')) ?></a><?php foreach ($socialNetworks as $network => [$label,$icon]):
$socialUrl = trim((string)($settings['public_site_' . $network . '_url'] ?? ''));
if ($socialUrl === '') continue; ?>
<a class="nav-social-link nav-social-<?= h($network) ?> social-<?= h($network) ?>"
href="<?= h($socialUrl) ?>" target="_blank" rel="noopener noreferrer"
aria-label="<?= h($label) ?>" title="<?= h($label) ?>"><span aria-hidden="true"><?= h($icon) ?></span></a><?php endforeach; ?>
</nav> </nav>
</div> </div>
</header> </header>

View file

@ -296,6 +296,16 @@ try {
$externalImage = PublicContentFormatter::render('![Piège](https://example.org/image.webp)'); $externalImage = PublicContentFormatter::render('![Piège](https://example.org/image.webp)');
$assert(!str_contains($externalImage, '<img'), 'Une image externe a été acceptée'); $assert(!str_contains($externalImage, '<img'), 'Une image externe a été acceptée');
}); });
$test('Le menu public affiche uniquement les réseaux sociaux configurés', function () use ($assert): void {
$layout = file_get_contents(__DIR__ . '/../app/Views/public_site_layout.php');
$contact = file_get_contents(__DIR__ . '/../app/Views/public_site_contact.php');
$assert(str_contains($layout, '$socialNetworks = ['), 'Le catalogue des réseaux sociaux est absent');
$assert(str_contains($layout, "public_site_' . \$network . '_url"), 'Les liens sociaux ne viennent pas des paramètres');
$assert(str_contains($layout, 'class="nav-social-link'), 'Les icônes sociales sont absentes du menu');
$assert(str_contains($layout, 'aria-label="<?= h($label) ?>"'), 'Les icônes sociales ne sont pas accessibles');
$assert(str_contains($layout, 'rel="noopener noreferrer"'), 'Les liens externes ne sont pas protégés');
$assert(!str_contains($contact, '$socialNetworks = ['), 'Le catalogue social est encore dupliqué');
});
$test('Authentification et mot de passe haché', function () use ($db, $assert): void { $test('Authentification et mot de passe haché', function () use ($db, $assert): void {
$assert(ini_get('session.use_strict_mode') === '1', 'Le mode strict des sessions est désactivé'); $assert(ini_get('session.use_strict_mode') === '1', 'Le mode strict des sessions est désactivé');
$hash = password_hash('MotDePasseSolide!', PASSWORD_DEFAULT); $hash = password_hash('MotDePasseSolide!', PASSWORD_DEFAULT);