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 # Fichiers de cache
cache/ cache/
# Dossier pour les images d'annonces (tout ignorer sauf .gitkeep)
uploads/*
!uploads/.gitkeep
# Fichiers de dépendances (si nécessaire) # Fichiers de dépendances (si nécessaire)
# vendor/ # vendor/
# node_modules/ # 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 - 📰 *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 dons* : Interface PayPal Me configurable pour collecter des dons
- ⏰ *Système de countdown* : Page de lancement configurable avec compte à rebours multi-fuseaux - ⏰ *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 == 🛠️ 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. 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 == 💝 Système de dons
kaubuntu.re intègre un système de dons configurable qui permet de collecter des contributions via PayPal Me. 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; align-items: start;
} }
/* Responsive breakpoint intermédiaire pour les 3 éléments */ /* Responsive breakpoint pour écrans entre 1025px et 1699px */
@media (max-width: 1200px) and (min-width: 1025px) { @media (max-width: 1699px) and (min-width: 1025px) {
.hero-mastodon-wrapper { .hero-mastodon-wrapper {
grid-template-columns: 1fr; display: grid;
grid-template-rows: auto auto; grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.hero {
grid-column: 1 / -1;
width: 100%;
} }
.timeline-wordpress-container { .timeline-wordpress-container {
grid-column: 1 / -1; grid-column: 1 / -1;
grid-template-columns: 1fr; display: grid;
gap: 15px; grid-template-columns: repeat(2, 1fr);
gap: 20px;
} }
.timeline-wordpress-container .wordpress-section { #mt-container {
width: 100%; grid-column: 1 / 2;
}
.wordpress-section {
grid-column: 2 / 3;
} }
} }
@@ -729,6 +740,116 @@ img {
opacity: 0.9; 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 { #mt-container {
width: 100%; width: 100%;
height: 400px; height: 400px;
@@ -1983,6 +2104,16 @@ img {
height: 300px; 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 { .hero-mastodon-wrapper #mt-container {
height: 600px; height: 600px;
} }
@@ -2342,6 +2473,129 @@ i.icon-mastodon,
margin: 0 auto 30px; 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 { .btn-primary {
display: inline-block; display: inline-block;
padding: 12px 25px; padding: 12px 25px;
@@ -2361,6 +2615,32 @@ i.icon-mastodon,
box-shadow: 0 4px 8px rgba(0,0,0,0.1); 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 */ /* Responsive pour la page de direct */
@media (max-width: 768px) { @media (max-width: 768px) {
.live-title { .live-title {
@@ -2399,6 +2679,125 @@ i.icon-mastodon,
.no-live-message p { .no-live-message p {
font-size: 16px; 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 */ /* Catégories */
+101 -1
View File
@@ -152,7 +152,106 @@ $liveStream = getLiveStream();
</div> </div>
<?php <?php
} else { } else {
// Aucun direct en cours // 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"> <div class="no-live-message">
<i class="fas fa-tv"></i> <i class="fas fa-tv"></i>
@@ -162,6 +261,7 @@ $liveStream = getLiveStream();
</div> </div>
<?php <?php
} }
}
?> ?>
</div> </div>
</div> </div>
+20
View File
@@ -166,4 +166,24 @@ if (!defined('DONATION_AMOUNTS')) define('DONATION_AMOUNTS', [5, 10, 20, 50]);
// Devise par défaut // Devise par défaut
if (!defined('DONATION_CURRENCY')) define('DONATION_CURRENCY', 'EUR'); 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');
?> ?>
+98 -1
View File
@@ -142,7 +142,103 @@ setSecurityHeaders();
</div> </div>
<?php <?php
} else { } else {
// Aucun direct en cours // 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"> <div class="hero-no-live">
<i class="fas fa-tv"></i> <i class="fas fa-tv"></i>
@@ -151,6 +247,7 @@ setSecurityHeaders();
</div> </div>
<?php <?php
} }
}
?> ?>
</section> </section>
View File