feat: enrichir l'édition du site public
This commit is contained in:
parent
6e3beea211
commit
1df26401a3
17 changed files with 115 additions and 15 deletions
|
|
@ -47,7 +47,10 @@ trait AnimalBrowseActions
|
|||
];
|
||||
$orderBy = $allowed[$sort] ?? 'a.updated_at';
|
||||
|
||||
$where = ['a.deleted_at IS NULL', 'a.archived_at IS NULL'];
|
||||
$where = ['a.deleted_at IS NULL'];
|
||||
if ($status !== '') {
|
||||
$where[] = 'a.archived_at IS NULL';
|
||||
}
|
||||
$params = [];
|
||||
if (in_array($availability, ['available', 'not_available', 'unknown'], true)) {
|
||||
$where[] = 'a.adoption_availability=:availability';
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ final class DashboardController
|
|||
$agendaDue = PermissionService::can('agenda') ? AgendaService::due() : [];
|
||||
|
||||
render('dashboard.php', [
|
||||
'title' => 'Dashboard',
|
||||
'title' => t('nav.dashboard'),
|
||||
'stats' => $stats,
|
||||
'shelterRoomCounts' => $shelterRoomCounts,
|
||||
'quality' => $quality,
|
||||
|
|
|
|||
|
|
@ -666,13 +666,41 @@ final class SettingsController
|
|||
public static function publicSite(): void
|
||||
{
|
||||
self::admin();
|
||||
$images = [];
|
||||
$mediaDirectory = dirname(__DIR__, 2) . '/public/media/site';
|
||||
foreach (glob($mediaDirectory . '/*.{jpg,jpeg,png,webp,gif}', GLOB_BRACE) ?: [] as $path) {
|
||||
$filename = basename($path);
|
||||
$images[] = ['filename' => $filename, 'url' => '/media/site/' . rawurlencode($filename)];
|
||||
}
|
||||
usort($images, static fn (array $left, array $right): int => strcmp($right['filename'], $left['filename']));
|
||||
render('settings_public_site.php', [
|
||||
'title' => t('public_site.settings_title'),
|
||||
'settings' => AppSettings::all(),
|
||||
'saved' => isset($_GET['saved']),
|
||||
'error' => (string) ($_GET['error'] ?? ''),
|
||||
'images' => $images,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function uploadPublicSiteImage(): void
|
||||
{
|
||||
self::admin();
|
||||
self::postOnly();
|
||||
try {
|
||||
$upload = $_FILES['public_site_image'] ?? null;
|
||||
if (!is_array($upload) || (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException(t('public_site.image_invalid'));
|
||||
}
|
||||
$directory = dirname(__DIR__, 2) . '/public/media/site';
|
||||
$filename = ImageService::storeUploaded((string) $upload['tmp_name'], $directory, 'site', 'photo');
|
||||
@chmod($directory, 0755);
|
||||
AuditService::log('public_site_image_uploaded', '/settings/public-site/image', 'Image du site public ajoutée');
|
||||
header('Location: /settings/public-site?image=' . rawurlencode($filename));
|
||||
} catch (Throwable $e) {
|
||||
header('Location: /settings/public-site?error=' . rawurlencode(SecurityService::publicError($e)));
|
||||
}
|
||||
exit();
|
||||
}
|
||||
public static function health(): void
|
||||
{
|
||||
self::admin();
|
||||
|
|
@ -770,7 +798,7 @@ final class SettingsController
|
|||
exit();
|
||||
}
|
||||
$values['public_site_custom_css'] = $customCss;
|
||||
foreach (['public_site_donation_url', 'public_site_facebook_url', 'public_site_instagram_url', 'public_site_mastodon_url', 'public_site_youtube_url', 'public_site_tiktok_url', 'public_site_bluesky_url'] as $key) {
|
||||
foreach (['public_site_donation_url', 'public_site_facebook_url', 'public_site_instagram_url', 'public_site_youtube_url', 'public_site_tiktok_url'] as $key) {
|
||||
$url = trim((string) ($_POST[$key] ?? ''));
|
||||
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
|
||||
if ($url !== '' && (!filter_var($url, FILTER_VALIDATE_URL) || $scheme !== 'https')) {
|
||||
|
|
|
|||
|
|
@ -350,6 +350,9 @@ switch ($path) {
|
|||
case '/settings/public-site/save':
|
||||
SettingsController::savePublicSite();
|
||||
break;
|
||||
case '/settings/public-site/image':
|
||||
SettingsController::uploadPublicSiteImage();
|
||||
break;
|
||||
case '/settings/privacy':
|
||||
PrivacyController::index();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -37,10 +37,8 @@ final class AppSettings
|
|||
'public_site_contact_text' => 'Une question sur une adoption ou sur l’association ? Contactez-nous.',
|
||||
'public_site_facebook_url' => '',
|
||||
'public_site_instagram_url' => '',
|
||||
'public_site_mastodon_url' => '',
|
||||
'public_site_youtube_url' => '',
|
||||
'public_site_tiktok_url' => '',
|
||||
'public_site_bluesky_url' => '',
|
||||
'notification_email_enabled' => '0',
|
||||
'notification_email_frequency' => 'weekly',
|
||||
'notification_email_recipient' => '',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ final class PermissionService
|
|||
'lecture' => 'Lecture seule',
|
||||
];
|
||||
public const MODULES = [
|
||||
'dashboard' => ['Dashboard', 'Priorités et situation quotidienne'],
|
||||
'dashboard' => ['Tableau de bord', 'Priorités et situation quotidienne'],
|
||||
'animals' => ['Animaux', 'Fiches, portées, liens, médias et adoptions'],
|
||||
'medical' => ['Médical', 'Notes, traitements, vaccinations et décès'],
|
||||
'care' => ['Tournées', 'Passages tablette et configuration des box'],
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ final class PublicContentFormatter
|
|||
|
||||
private static function inline(string $text): string
|
||||
{
|
||||
$images = [];
|
||||
$text = preg_replace_callback('/!\[([^\]]*)\]\((\/media\/site\/[A-Za-z0-9._%-]+)\)/u', static function (array $match) use (&$images): string {
|
||||
$token = "\x1B" . count($images) . "\x1B";
|
||||
$images[] = '<img class="public-content-image" src="' .
|
||||
htmlspecialchars($match[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" alt="' .
|
||||
htmlspecialchars($match[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" loading="lazy">';
|
||||
return $token;
|
||||
}, $text) ?? $text;
|
||||
$links = [];
|
||||
$text = preg_replace_callback('/\[([^\]]+)\]\((https:\/\/[^\s)]+)\)/u', static function (array $match) use (&$links): string {
|
||||
if (!filter_var($match[2], FILTER_VALIDATE_URL)) {
|
||||
|
|
@ -62,6 +70,9 @@ final class PublicContentFormatter
|
|||
foreach ($links as $index => $link) {
|
||||
$text = str_replace("\x1A" . $index . "\x1A", $link, $text);
|
||||
}
|
||||
foreach ($images as $index => $image) {
|
||||
$text = str_replace("\x1B" . $index . "\x1B", $image, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@
|
|||
<div class="col-lg-6">
|
||||
<section class="card h-100">
|
||||
<div class="card-body">
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.4</span>
|
||||
<h3>Retours de terrain avant la 1.0</h3>
|
||||
<p class="text-muted">Photos publiques et déplacements fiabilisés, statistiques navigables,
|
||||
références biologiques extensibles et site public personnalisable.</p>
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.5</span>
|
||||
<h3>Édition du site public</h3>
|
||||
<p class="text-muted">Bibliothèque d’images, réseaux sociaux simplifiés et navigation complète
|
||||
depuis les statistiques, y compris vers les fiches archivées.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ $obfuscate = static fn (string$value): string => implode('', array_reverse(mb_st
|
|||
class="contact-obfuscated"><?= h($obfuscate($settings['association_email'])) ?></span></div>
|
||||
<?php endif; ?></div>
|
||||
<div class="chips" style="margin-top:25px">
|
||||
<?php foreach (['facebook' => 'Facebook','instagram' => 'Instagram','mastodon' => 'Mastodon','youtube' => 'YouTube','tiktok' => 'TikTok','bluesky' => 'Bluesky'] as $network => $label):
|
||||
<?php foreach (['facebook' => 'Facebook','instagram' => 'Instagram','youtube' => 'YouTube','tiktok' => 'TikTok'] as $network => $label):
|
||||
$url = trim((string)($settings['public_site_' . $network . '_url'] ?? ''));
|
||||
if ($url === '') continue; ?>
|
||||
<a class="button secondary" href="<?= h($url) ?>" rel="noopener noreferrer"><?= h($label) ?></a>
|
||||
|
|
|
|||
|
|
@ -216,6 +216,14 @@ if (!function_exists('public_age')) {
|
|||
min-height: 65vh
|
||||
}
|
||||
|
||||
.public-content-image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1rem 0;
|
||||
border-radius: var(--radius)
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 52px 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,25 @@
|
|||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('public_site.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title">🖼️ <?= h(t('public_site.image_library')) ?></h3></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/settings/public-site/image" enctype="multipart/form-data" class="row g-2 align-items-end mb-3">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="col-md"><label class="form-label"><?= h(t('public_site.image_file')) ?></label><input class="form-control" type="file" name="public_site_image" accept="image/jpeg,image/png,image/webp" required></div>
|
||||
<div class="col-md-auto"><button class="btn btn-outline-primary"><?= h(t('public_site.image_upload')) ?></button></div>
|
||||
</form>
|
||||
<div class="form-hint mb-3"><?= h(t('public_site.image_help')) ?></div>
|
||||
<?php if (!$images): ?><div class="text-muted"><?= h(t('public_site.image_none')) ?></div><?php else: ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($images as $image): ?><div class="col-6 col-md-3 col-xl-2">
|
||||
<div class="card h-100"><img src="<?= h($image['url']) ?>" alt="" class="card-img-top" style="height:110px;object-fit:cover">
|
||||
<div class="card-body p-2"><button type="button" class="btn btn-sm btn-outline-secondary w-100 public-image-insert" data-markup="<?= h('') ?>"><?= h(t('public_site.image_insert')) ?></button></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/settings/public-site/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card mb-3">
|
||||
<div class="card-status-top bg-green"></div>
|
||||
|
|
@ -118,7 +137,7 @@
|
|||
name="public_site_facebook_url" value="<?= h($settings['public_site_facebook_url']) ?>"></div>
|
||||
<div class="col-md-6"><label class="form-label">Instagram</label><input class="form-control" type="url"
|
||||
name="public_site_instagram_url" value="<?= h($settings['public_site_instagram_url']) ?>"></div>
|
||||
<?php foreach (['mastodon' => 'Mastodon','youtube' => 'YouTube','tiktok' => 'TikTok','bluesky' => 'Bluesky'] as $network => $label): ?>
|
||||
<?php foreach (['youtube' => 'YouTube','tiktok' => 'TikTok'] as $network => $label): ?>
|
||||
<div class="col-md-6"><label class="form-label"><?= h($label) ?></label><input class="form-control"
|
||||
type="url" name="public_site_<?= h($network) ?>_url"
|
||||
value="<?= h($settings['public_site_' . $network . '_url']) ?>" placeholder="https://..."></div>
|
||||
|
|
@ -142,7 +161,9 @@
|
|||
<div class="text-end mb-4"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
<script>
|
||||
let activePublicEditor = null;
|
||||
document.querySelectorAll('.public-rich-text').forEach((textarea) => {
|
||||
textarea.addEventListener('focus', () => { activePublicEditor = textarea; });
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'btn-list mb-2';
|
||||
const actions = [['B', '**', '**'], ['I', '*', '*'], ['Titre', '## ', ''], ['Liste', '- ', ''], ['Lien', '[', '](https://)']];
|
||||
|
|
@ -162,4 +183,13 @@ document.querySelectorAll('.public-rich-text').forEach((textarea) => {
|
|||
});
|
||||
textarea.before(toolbar);
|
||||
});
|
||||
document.querySelectorAll('.public-image-insert').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const textarea = activePublicEditor || document.querySelector('.public-rich-text');
|
||||
if (!textarea) return;
|
||||
const start = textarea.selectionStart;
|
||||
textarea.setRangeText(button.dataset.markup + '\n', start, textarea.selectionEnd, 'end');
|
||||
textarea.focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue