feat: add next live announcement with multi-timezone display

This commit is contained in:
2025-10-08 17:00:40 +04:00
parent e6b2b60edc
commit b7ccfce43e
7 changed files with 715 additions and 29 deletions
+4
View File
@@ -24,6 +24,10 @@ temp/
# Fichiers de cache
cache/
# Dossier pour les images d'annonces (tout ignorer sauf .gitkeep)
uploads/*
!uploads/.gitkeep
# Fichiers de dépendances (si nécessaire)
# vendor/
# node_modules/
+66
View File
@@ -26,6 +26,7 @@ kaubuntu.re est une interface web responsive qui permet de consulter et recherch
- 📰 *Intégration WordPress* : Affichage des articles depuis un site WordPress via REST API
- 💝 *Système de dons* : Interface PayPal Me configurable pour collecter des dons
- ⏰ *Système de countdown* : Page de lancement configurable avec compte à rebours multi-fuseaux
- 📺 *Annonce du prochain live* : Affichage dynamique avec multi-fuseaux horaires et image personnalisable
== 🛠️ Technologies utilisées
@@ -157,6 +158,71 @@ cp mentions-legales.php.sample mentions-legales.php
Ces fichiers sont listés dans le `.gitignore` afin que vos modifications ne soient pas suivies par Git, ce qui vous permet de personnaliser votre instance sans affecter le code source principal.
== 📺 Annonce du prochain live
kaubuntu.re intègre un système d'annonce configurable qui s'affiche automatiquement lorsqu'il n'y a pas de diffusion en direct en cours.
=== ✨ Fonctionnalités
- 📅 *Date et heure dynamiques* : Génération automatique à partir de `NEXT_LIVE_DATE`
- 🌍 *Multi-fuseaux horaires* : Affichage automatique pour 5 territoires (Ma'ohi Nui, Martinique/Guadeloupe, Guyane, France, Kanaky)
- ⏰ *Décalage UTC* : Affichage du fuseau horaire de référence (UTC+04:00 pour La Réunion)
- 📅 *Indicateurs de jour* : Affichage des décalages de jour (+1j/-1j) si nécessaire
- 🖼️ *Image personnalisable* : Support des formats Instagram (portrait 4:5, carré 1:1) et paysage (16:9)
- 📱 *Responsive complet* : Layouts adaptés pour desktop (50/50), tablette (vertical), et mobile
- 🔄 *Flexbox intelligent* : Réorganisation automatique des fuseaux horaires selon la largeur d'écran
=== ⚙️ Configuration
Pour configurer l'annonce du prochain live, ajoutez dans votre `config.local.php` :
[source,php]
----
// Activer l'annonce du prochain live
define('NEXT_LIVE_ENABLED', true);
// Titre (la date sera ajoutée automatiquement)
define('NEXT_LIVE_TITLE', 'Prochain live');
// Description (l'heure sera ajoutée automatiquement)
define('NEXT_LIVE_DESCRIPTION', 'Constitution du futur état réunionnais & Hommage à Thomas Sankara.');
// Date du prochain live au format Y-m-d H:i:s
define('NEXT_LIVE_DATE', '2025-10-11 10:00:00');
// Chemin vers l'image d'annonce (optionnel)
define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg');
----
=== 📁 Gestion des images
. Placez vos images dans le dossier `uploads/` (non tracké par Git)
. Formats recommandés :
* Portrait 4:5 : 1080×1350px ou 1280×1600px - *Idéal*
* Carré 1:1 : 1080×1080px - *Parfait*
* Paysage 16:9 : 1920×1080px
. Optimisez vos images (< 500 Ko recommandé)
=== 🎯 Affichage
*Desktop (≥1700px)* : Layout 50/50 (image à gauche, informations à droite)
*Tablette (1025-1699px)* : Hero seul sur une ligne, Mastodon et WordPress côte à côte en dessous
*Mobile (<769px)* : Stack vertical avec scroll si nécessaire
=== 🌍 Fuseaux horaires affichés
L'annonce calcule automatiquement les heures locales pour :
- *Ma'ohi Nui* (Polynésie française) - UTC-10:00
- *Martinique / Guadeloupe* - UTC-04:00
- *Guyane* - UTC-03:00
- *France* - UTC+02:00
- *Kanaky* (Nouvelle-Calédonie) - UTC+11:00
L'heure de référence (La Réunion, UTC+04:00) est affichée dans le badge principal.
== 💝 Système de dons
kaubuntu.re intègre un système de dons configurable qui permet de collecter des contributions via PayPal Me.
+407 -8
View File
@@ -584,21 +584,32 @@ img {
align-items: start;
}
/* Responsive breakpoint intermédiaire pour les 3 éléments */
@media (max-width: 1200px) and (min-width: 1025px) {
/* Responsive breakpoint pour écrans entre 1025px et 1699px */
@media (max-width: 1699px) and (min-width: 1025px) {
.hero-mastodon-wrapper {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.hero {
grid-column: 1 / -1;
width: 100%;
}
.timeline-wordpress-container {
grid-column: 1 / -1;
grid-template-columns: 1fr;
gap: 15px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.timeline-wordpress-container .wordpress-section {
width: 100%;
#mt-container {
grid-column: 1 / 2;
}
.wordpress-section {
grid-column: 2 / 3;
}
}
@@ -729,6 +740,116 @@ img {
opacity: 0.9;
}
/* Annonce du prochain live dans le hero (page d'accueil) - Split gauche/droite */
.hero-next-live {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
}
.hero-next-live-image-container {
flex: 0 0 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-dark);
padding: 15px;
}
.hero-next-live-image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
.hero-next-live-content {
flex: 0 0 50%;
padding: 20px 15px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.95) 100%);
color: white;
}
.hero-next-live-content i.fa-calendar-alt {
font-size: 35px;
margin-bottom: 8px;
color: var(--primary-red);
}
.hero-next-live-content h2 {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
color: white;
line-height: 1.2;
}
.hero-next-live-content p {
font-size: 14px;
margin: 0 auto 12px;
color: rgba(255, 255, 255, 0.95);
line-height: 1.3;
}
.hero-next-live-date {
font-size: 15px !important;
font-weight: 600;
color: white !important;
background: var(--primary-red);
padding: 7px 14px;
border-radius: 8px;
display: inline-block;
margin-bottom: 12px !important;
box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3);
}
.hero-next-live-date i {
margin-right: 8px;
}
.hero-next-live-datetime {
width: 100%;
}
.hero-next-live-timezones {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 12px;
padding: 12px 16px;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 8px;
}
.hero-timezone-item {
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
white-space: nowrap;
line-height: 1.5;
background-color: rgba(0, 0, 0, 0.3);
padding: 5px 10px;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.15);
flex: 0 1 auto;
min-width: fit-content;
}
.hero-timezone-item strong {
color: white;
font-weight: 700;
}
#mt-container {
width: 100%;
height: 400px;
@@ -1983,6 +2104,16 @@ img {
height: 300px;
}
/* Augmenter la hauteur du hero quand il y a une annonce */
.hero:has(.hero-next-live) {
height: 600px;
}
/* Augmenter aussi la hauteur du hero pour "no-live" pour cohérence */
.hero:has(.hero-no-live) {
height: 300px;
}
.hero-mastodon-wrapper #mt-container {
height: 600px;
}
@@ -2342,6 +2473,129 @@ i.icon-mastodon,
margin: 0 auto 30px;
}
/* Annonce du prochain live - Split gauche/droite */
.next-live-announcement {
display: flex;
flex-direction: row;
border-radius: 8px;
overflow: hidden;
background-color: var(--card-bg);
min-height: 500px;
}
.next-live-image-container {
flex: 0 0 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-dark);
padding: 20px;
}
.next-live-image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
max-height: 600px;
}
.next-live-content {
flex: 0 0 50%;
padding: 50px 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-dark) 100%);
}
.next-live-content i.fa-calendar-alt {
font-size: 60px;
margin-bottom: 20px;
color: var(--primary-red);
}
.next-live-content h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 20px;
color: var(--text-color);
line-height: 1.3;
}
.next-live-content p {
font-size: 20px;
margin: 0 auto 25px;
color: var(--text-muted);
line-height: 1.5;
max-width: 100%;
}
.next-live-date {
font-size: 22px !important;
font-weight: 600;
color: white !important;
background: var(--primary-red);
padding: 12px 24px;
border-radius: 8px;
display: inline-block;
margin-bottom: 30px !important;
box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3);
}
.next-live-date i {
margin-right: 8px;
}
.utc-offset {
font-size: 16px;
opacity: 0.85;
font-weight: 500;
}
.next-live-datetime {
width: 100%;
}
.next-live-timezones {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 15px;
margin-bottom: 20px;
padding: 16px 20px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 8px;
}
.timezone-item {
font-size: 16px;
color: rgba(255, 255, 255, 0.95);
white-space: nowrap;
line-height: 1.5;
background-color: rgba(0, 0, 0, 0.3);
padding: 6px 12px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
flex: 0 1 auto;
min-width: fit-content;
}
.timezone-item strong {
color: white;
font-weight: 700;
}
.day-shift {
font-size: 12px;
color: var(--primary-red);
font-weight: 700;
margin-left: 4px;
}
.btn-primary {
display: inline-block;
padding: 12px 25px;
@@ -2361,6 +2615,32 @@ i.icon-mastodon,
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
/* Responsive tablette/desktop étroit - 769px à 1024px */
@media (max-width: 1024px) and (min-width: 769px) {
.hero-mastodon-wrapper {
display: block;
}
.hero {
width: 100%;
margin-bottom: 20px;
}
.timeline-wordpress-container {
display: flex;
flex-direction: row;
gap: 20px;
}
#mt-container {
flex: 1;
}
.wordpress-section {
flex: 1;
}
}
/* Responsive pour la page de direct */
@media (max-width: 768px) {
.live-title {
@@ -2399,6 +2679,125 @@ i.icon-mastodon,
.no-live-message p {
font-size: 16px;
}
/* Responsive mobile - Stack vertical pour l'annonce */
.next-live-announcement {
flex-direction: column;
min-height: auto;
}
.next-live-image-container {
flex: 0 0 auto;
width: 100%;
max-height: 400px;
padding: 15px;
}
.next-live-image {
max-height: 350px;
}
.next-live-content {
flex: 0 0 auto;
width: 100%;
padding: 30px 20px;
overflow-y: auto;
max-height: 600px;
}
.next-live-content i.fa-calendar-alt {
font-size: 45px;
}
.next-live-content h2 {
font-size: 24px;
}
.next-live-content p {
font-size: 16px;
}
.next-live-date {
font-size: 17px !important;
padding: 10px 18px;
}
.next-live-timezones {
gap: 8px;
padding: 14px 16px;
margin-top: 12px;
margin-bottom: 16px;
}
.timezone-item {
font-size: 14px;
padding: 5px 10px;
}
/* Hero next live responsive */
.hero-next-live {
flex-direction: column;
}
.hero-next-live-image-container {
flex: 0 0 300px;
width: 100%;
padding: 15px;
max-height: 300px;
}
.hero-next-live-image {
max-height: 270px;
}
.hero-next-live-content {
flex: 1;
width: 100%;
padding: 5px 15px 25px 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.hero-next-live-content i.fa-calendar-alt {
font-size: 35px;
margin-bottom: 10px;
margin-top: 0;
}
.hero-next-live-content h2 {
font-size: 20px;
margin-bottom: 8px;
line-height: 1.2;
}
.hero-next-live-content p {
font-size: 13px;
margin-bottom: 10px;
line-height: 1.4;
}
.hero-next-live-date {
font-size: 13px !important;
padding: 6px 12px;
margin-bottom: 10px !important;
}
.hero-next-live-timezones {
gap: 5px;
padding: 8px 10px;
margin-top: 8px;
}
.hero-timezone-item {
font-size: 10px;
padding: 3px 6px;
}
.utc-offset {
font-size: 11px;
}
}
/* Catégories */
+109 -9
View File
@@ -152,15 +152,115 @@ $liveStream = getLiveStream();
</div>
<?php
} else {
// Aucun direct en cours
?>
<div class="no-live-message">
<i class="fas fa-tv"></i>
<h2>Aucun direct en cours</h2>
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
<a href="index.php" class="btn-primary">Retour à l'accueil</a>
</div>
<?php
// Aucun direct en cours - vérifier s'il y a une annonce configurée
$showNextLiveAnnouncement = defined('NEXT_LIVE_ENABLED') && NEXT_LIVE_ENABLED === true;
if ($showNextLiveAnnouncement) {
// Afficher l'annonce du prochain live
?>
<div class="next-live-announcement">
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
<div class="next-live-image-container">
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
alt="<?php echo htmlspecialchars(NEXT_LIVE_TITLE); ?>"
class="next-live-image">
</div>
<?php endif; ?>
<div class="next-live-content">
<i class="fas fa-calendar-alt"></i>
<?php
if (!empty(NEXT_LIVE_DATE)) {
$liveDate = new DateTime(NEXT_LIVE_DATE, new DateTimeZone('Indian/Reunion'));
$dayFormatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Indian/Reunion',
IntlDateFormatter::GREGORIAN,
'EEEE d MMMM'
);
$formattedDay = $dayFormatter->format($liveDate);
$formattedDay = ucfirst($formattedDay);
$dynamicTitle = NEXT_LIVE_TITLE . ' - ' . $formattedDay;
} else {
$dynamicTitle = NEXT_LIVE_TITLE;
}
?>
<h2><?php echo htmlspecialchars($dynamicTitle); ?></h2>
<?php
if (!empty(NEXT_LIVE_DATE)) {
$liveHour = $liveDate->format('H\hi');
$dynamicDescription = 'Rejoignez-nous à ' . $liveHour . '. ' . NEXT_LIVE_DESCRIPTION;
} else {
$dynamicDescription = NEXT_LIVE_DESCRIPTION;
}
?>
<p><?php echo nl2br(htmlspecialchars($dynamicDescription)); ?></p>
<?php if (!empty(NEXT_LIVE_DATE)): ?>
<div class="next-live-datetime">
<p class="next-live-date">
<i class="fas fa-clock"></i>
<?php
$formatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::SHORT,
'Indian/Reunion'
);
echo $formatter->format($liveDate);
$offset = $liveDate->format('P');
echo ' <span class="utc-offset">(UTC' . $offset . ')</span>';
?>
</p>
<!-- Autres fuseaux horaires -->
<div class="next-live-timezones">
<?php
// Ordre croissant : du plus en retard au plus en avance
$timezones = [
'Ma\'ohi Nui' => 'Pacific/Tahiti',
'Martinique / Guadeloupe' => 'America/Martinique',
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
'Kanaky' => 'Pacific/Noumea'
];
foreach($timezones as $name => $timezone):
$liveDateLocal = clone $liveDate;
$liveDateLocal->setTimezone(new DateTimeZone($timezone));
// Vérifier si c'est un jour différent
$dayDiff = $liveDateLocal->format('j') - $liveDate->format('j');
$dayIndicator = '';
if ($dayDiff > 0) {
$dayIndicator = ' <span class="day-shift">+1j</span>';
} elseif ($dayDiff < 0) {
$dayIndicator = ' <span class="day-shift">-1j</span>';
}
?>
<span class="timezone-item">
<strong><?php echo $name; ?> :</strong> <?php echo $liveDateLocal->format('H\hi'); ?><?php echo $dayIndicator; ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<a href="index.php" class="btn-primary">Retour à l'accueil</a>
</div>
</div>
<?php
} else {
?>
<div class="no-live-message">
<i class="fas fa-tv"></i>
<h2>Aucun direct en cours</h2>
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
<a href="index.php" class="btn-primary">Retour à l'accueil</a>
</div>
<?php
}
}
?>
</div>
+20
View File
@@ -166,4 +166,24 @@ if (!defined('DONATION_AMOUNTS')) define('DONATION_AMOUNTS', [5, 10, 20, 50]);
// Devise par défaut
if (!defined('DONATION_CURRENCY')) define('DONATION_CURRENCY', 'EUR');
// =========================================
// Annonce du prochain live
// =========================================
// Activer/désactiver l'annonce du prochain live par défaut
if (!defined('NEXT_LIVE_ENABLED')) define('NEXT_LIVE_ENABLED', false);
// Titre de l'annonce du prochain live
if (!defined('NEXT_LIVE_TITLE')) define('NEXT_LIVE_TITLE', 'Prochain live');
// Description de l'annonce du prochain live
if (!defined('NEXT_LIVE_DESCRIPTION')) define('NEXT_LIVE_DESCRIPTION', 'Rejoignez-nous pour notre prochain live !');
// Date du prochain live (format: Y-m-d H:i:s)
if (!defined('NEXT_LIVE_DATE')) define('NEXT_LIVE_DATE', '');
// Chemin vers l'image d'annonce du prochain live (relatif à la racine du site)
// Exemple: 'uploads/next-live.jpg'
if (!defined('NEXT_LIVE_IMAGE')) define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg');
?>
+105 -8
View File
@@ -142,14 +142,111 @@ setSecurityHeaders();
</div>
<?php
} else {
// Aucun direct en cours
?>
<div class="hero-no-live">
<i class="fas fa-tv"></i>
<h2>Aucun direct en cours</h2>
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
</div>
<?php
// Aucun direct en cours - vérifier s'il y a une annonce configurée
$showNextLiveAnnouncement = defined('NEXT_LIVE_ENABLED') && NEXT_LIVE_ENABLED === true;
if ($showNextLiveAnnouncement) {
// Afficher l'annonce du prochain live
?>
<div class="hero-next-live">
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
<div class="hero-next-live-image-container">
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
alt="<?php echo htmlspecialchars(NEXT_LIVE_TITLE); ?>"
class="hero-next-live-image">
</div>
<?php endif; ?>
<div class="hero-next-live-content">
<i class="fas fa-calendar-alt"></i>
<?php
if (!empty(NEXT_LIVE_DATE)) {
$liveDate = new DateTime(NEXT_LIVE_DATE, new DateTimeZone('Indian/Reunion'));
$dayFormatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Indian/Reunion',
IntlDateFormatter::GREGORIAN,
'EEEE d MMMM'
);
$formattedDay = $dayFormatter->format($liveDate);
$formattedDay = ucfirst($formattedDay);
$dynamicTitle = NEXT_LIVE_TITLE . ' - ' . $formattedDay;
} else {
$dynamicTitle = NEXT_LIVE_TITLE;
}
?>
<h2><?php echo htmlspecialchars($dynamicTitle); ?></h2>
<?php
if (!empty(NEXT_LIVE_DATE)) {
$liveHour = $liveDate->format('H\hi');
$dynamicDescription = 'Rejoignez-nous à ' . $liveHour . '. ' . NEXT_LIVE_DESCRIPTION;
} else {
$dynamicDescription = NEXT_LIVE_DESCRIPTION;
}
?>
<p><?php echo nl2br(htmlspecialchars($dynamicDescription)); ?></p>
<?php if (!empty(NEXT_LIVE_DATE)): ?>
<div class="hero-next-live-datetime">
<p class="hero-next-live-date">
<i class="fas fa-clock"></i>
<?php
$formatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::SHORT,
'Indian/Reunion'
);
echo $formatter->format($liveDate);
$offset = $liveDate->format('P');
echo ' <span class="utc-offset">(UTC' . $offset . ')</span>';
?>
</p>
<!-- Autres fuseaux horaires -->
<div class="hero-next-live-timezones">
<?php
// Ordre croissant : du plus en retard au plus en avance
$timezones = [
'Ma\'ohi Nui' => 'Pacific/Tahiti',
'Martinique / Guadeloupe' => 'America/Martinique',
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
'Kanaky' => 'Pacific/Noumea'
];
foreach($timezones as $name => $timezone):
$liveDateLocal = clone $liveDate;
$liveDateLocal->setTimezone(new DateTimeZone($timezone));
$dayDiff = $liveDateLocal->format('j') - $liveDate->format('j');
$dayIndicator = '';
if ($dayDiff > 0) {
$dayIndicator = ' <span class="day-shift">+1j</span>';
} elseif ($dayDiff < 0) {
$dayIndicator = ' <span class="day-shift">-1j</span>';
}
?>
<span class="hero-timezone-item">
<strong><?php echo $name; ?> :</strong> <?php echo $liveDateLocal->format('H\hi'); ?><?php echo $dayIndicator; ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php
} else {
?>
<div class="hero-no-live">
<i class="fas fa-tv"></i>
<h2>Aucun direct en cours</h2>
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
</div>
<?php
}
}
?>
</section>
View File