Compare commits

..

11 Commits

21 changed files with 2513 additions and 1862 deletions
+197 -324
View File
@@ -1,148 +1,106 @@
= 🎬 kaubuntu.re - Plateforme Multimédia
= kaubuntu.re - Hub Multimédia
:toc: left
:toc-title: Sommaire
:toclevels: 3
🌍 *Une plateforme multimédia indépendante au service de la vision panafricaniste et indépendantiste réunionnaise*
*Agrégateur des réseaux sociaux du mouvement panafricaniste et indépendantiste réunionnais Ka-Ubuntu.*
== 📖 Description
== Description
kaubuntu.re est une interface web responsive qui permet de consulter et rechercher des vidéos hébergées sur une instance PeerTube. Il est également possible de suivre le fil du réseau social koze.kaubuntu.re (Mastodon). Développée par le mouvement politique *Ka-Ubuntu*, cette plateforme est conçue pour être légère, facilement déployable sur un serveur mutualisé, et optimisée pour les appareils mobiles et desktop.
kaubuntu.re centralise en une seule page les contenus publiés par Ka-Ubuntu sur YouTube, Instagram, TikTok et le site WordPress kaubuntu.com. Développée en PHP vanilla, sans framework, la plateforme est conçue pour fonctionner sur un hébergement mutualisé standard (o2Switch) et s'affiche correctement sur tous les appareils.
🎯 *Mission* : Offrir une alternative libre et décentralisée aux plateformes vidéo traditionnelles, en phase avec les valeurs d'indépendance et de souveraineté numérique défendues par Ka-Ubuntu.
== Fonctionnalités
== ✨ Fonctionnalités
- Grille YouTube avec détection automatique des Shorts (3 Shorts portrait + 6 vidéos paysage)
- Compteurs de followers/abonnés en temps réel (YouTube, Instagram, TikTok, articles WordPress), mis en cache 24h
- Profil embed Instagram et TikTok (iframes officiels)
- Grille d'articles WordPress via REST API (kaubuntu.com)
- Mode clair / sombre avec détection de la préférence système
- Scroll animé vers les sections avec highlight d'arrivée
- Bouton retour en haut de page
- Progressive Web App (PWA) : installation native, mode hors ligne, cache Service Worker
- Détection automatique de l'état de connexion
- Annonce du prochain live configurable (multi-fuseaux horaires)
- Système de dons PayPal Me
- Page de compte à rebours / maintenance
- Headers de sécurité (CSP, HSTS, X-Frame-Options…)
- 🎥 Affichage des vidéos à la une et récentes
- 📚 Navigation par catégories
- ▶️ Lecture de vidéos
- 🔍 Recherche de contenu
- 📱 Interface responsive (mobile et desktop)
- 🔗 Intégration avec une instance PeerTube
- 📲 Progressive Web App (PWA) avec installation native
- 🌐 Mode hors ligne avec cache intelligent
- 📡 Détection automatique d'état de connexion
- 📊 Analytics intégré avec Plausible (respectueux de la vie privée)
- 📰 *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
== 🛠️ Technologies utilisées
- PHP 8.0+ (backend, APIs, cache fichier)
- HTML5 / CSS3 / JavaScript vanilla
- YouTube Data API v3 (optionnel, avec fallback RSS public)
- WordPress REST API
- Font Awesome (icônes)
- Service Worker (PWA)
- 📄 HTML5
- 🎨 CSS3 avec Media Queries pour le responsive design
- 🐘 PHP pour le backend
- ⚡ JavaScript pour les interactions côté client
- 🔧 Service Worker pour le cache offline et PWA
- 📋 Web App Manifest pour l'installation native
- 📦 Bibliothèques externes via CDN:
* 🎯 Font Awesome (icônes)
* 📊 Plausible Analytics (statistiques respectueuses de la vie privée)
== 📁 Structure du projet
== Structure du projet
[source]
----
├── css/
│ ├── styles.css
── video-page.css
│ ├── categories.css
│ └── search.css
├── img/
│ ├── categories/
│ ├── video-thumbnails/
│ └── channels/
├── js/
│ └── main.js
│ ├── styles.css # Styles globaux, thème, composants
── social.css # Styles spécifiques à l'agrégateur
├── includes/
│ ├── social/
│ │ ├── youtube.php # YouTube Data API v3 + fallback RSS + détection Shorts
│ │ └── stats.php # Compteurs followers (YouTube, Instagram, TikTok)
│ ├── wordpress.php # Intégration WordPress REST API
│ ├── config.php # Chargement config (default + local)
│ ├── config.default.php # Valeurs par défaut
│ ├── config.local.php.sample # Modèle de configuration locale
│ ├── security.php # Headers HTTP et CSP
│ ├── header.php
│ ├── footer.php
│ ├── mobile-menu.php
── featured-videos.php
│ ├── recent-videos.php
── categories.php
│ ├── pwa-init.php
│ └── config.local.php.sample # Exemple de configuration locale
│ ├── sidebar.php
── mobile-menu.php
├── js/
── main.js # Thème, scroll animé, retour en haut, menu mobile
├── img/
├── conf/
│ ├── .htaccess.sample # Configuration Apache sécurisée
│ └── nginx.conf.sample # Configuration Nginx sécurisée
├── index.php
├── video.php
├── categories.php
├── search.php
├── sw.js # Service Worker pour PWA
├── site.webmanifest.sample # Exemple de manifest PWA
├── browserconfig.xml # Configuration Windows
├── sitemap.xml.sample # Exemple de sitemap
├── robots.txt.sample # Exemple de robots.txt
├── mentions-legales.php.sample # Exemple de mentions légales
│ ├── .htaccess.sample
│ └── nginx.conf.sample
├── index.php # Page principale
├── sw.js # Service Worker
├── site.webmanifest.sample
└── README.adoc
----
== 🚀 Installation
== Prérequis
. 📥 Clonez ce dépôt
. 🔧 Configurez votre serveur web (Apache, Nginx, etc.) pour pointer vers le répertoire racine
. 🔒 *Important :* Assurez-vous que votre serveur supporte HTTPS (requis pour PWA)
. 🛡️ *Configuration serveur sécurisée :* Copiez le fichier de configuration approprié depuis `conf/`
- *PHP 8.0+*
- Extensions PHP : `curl`, `json`, `intl`, `mbstring`, `simplexml`
- Serveur web Apache ou Nginx
- HTTPS (requis pour PWA)
== ⚙️ Configuration
[source,bash]
----
# Ubuntu/Debian
sudo apt-get install php-curl php-intl php-mbstring php-xml
Le site utilise un système de configuration en deux parties :
# Vérifier les extensions
php -m | grep -E 'curl|intl|mbstring|simplexml'
----
- 📋 `includes/config.php` : La configuration de base (versionnée)
- 🔧 `includes/config.local.php` : Votre configuration locale (non versionnée)
== Installation
Pour configurer votre environnement local :
. Copiez le fichier d'exemple vers le fichier local :
. Cloner le dépôt
. Copier le fichier de configuration :
+
[source,bash]
----
cp includes/config.local.php.sample includes/config.local.php
----
. Modifiez `includes/config.local.php` selon vos besoins :
* URL de l'instance PeerTube
* Clé API
* Catégories à afficher
* Nombre de vidéos par section
* etc.
Les modifications apportées à `config.local.php` ne seront pas suivies par Git, ce qui vous permet de personnaliser votre instance sans affecter le code source principal.
== 🏷️ Personnalisation des catégories
Pour personnaliser les catégories affichées sur la page d'accueil, modifiez la constante `PRIORITY_CATEGORIES` dans votre fichier `config.local.php` :
[source,php]
. Renseigner les variables dans `includes/config.local.php` (voir section Configuration)
. Copier la configuration serveur :
+
[source,bash]
----
define('PRIORITY_CATEGORIES', [
11 => 'Actualité & Politique',
14 => 'Activisme',
15 => 'Science & Technologie',
1 => 'Musique',
// Ajoutez d'autres catégories selon vos besoins
]);
cp conf/.htaccess.sample .htaccess
# ou pour Nginx :
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/kaubuntu.re
----
- Les clés sont les IDs des catégories dans PeerTube
- Les valeurs sont les noms personnalisés que vous souhaitez afficher
- L'ordre dans le tableau détermine l'ordre d'affichage sur la page
== 🎨 Personnalisation
Vous pouvez personnaliser l'apparence de la plateforme en modifiant les fichiers CSS dans le dossier `css/`. Pour changer le logo et les couleurs principales:
. 🖼️ Remplacez le fichier `img/logo.png` par votre propre logo
. 🎨 Modifiez les couleurs dans `css/styles.css`
== 📝 Personnalisation du sitemap, robots.txt, webmanifest et mentions légales
Les fichiers `sitemap.xml`, `robots.txt`, `site.webmanifest` et `mentions-legales.php` contiennent des données spécifiques au domaine (`kaubuntu.re`). Pour les adapter à votre domaine :
. Copiez les fichiers samples pour créer vos propres versions :
. Copier les fichiers domaine-spécifiques :
+
[source,bash]
----
@@ -152,245 +110,186 @@ cp site.webmanifest.sample site.webmanifest
cp mentions-legales.php.sample mentions-legales.php
----
. Remplacez les placeholders par vos informations réelles :
* `VOTRE-DOMAINE` par votre nom de domaine
* `VOTRE-DATE-MAJ` par la date de dernière mise à jour des mentions légales
== Configuration
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.
Le système de configuration est en deux couches :
== 📺 Annonce du prochain live
- `includes/config.default.php` — valeurs par défaut (versionné)
- `includes/config.local.php` — surcharges locales (non versionné, ignoré par Git)
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` :
=== Réseaux sociaux
[source,php]
----
// Activer l'annonce du prochain live
define('NEXT_LIVE_ENABLED', true);
define('YOUTUBE_URL', 'https://www.youtube.com/@votre-chaine');
define('YOUTUBE_HANDLE', 'votre-chaine');
// Titre (la date sera ajoutée automatiquement)
define('NEXT_LIVE_TITLE', 'Prochain live');
define('INSTAGRAM_URL', 'https://www.instagram.com/votre-compte/');
define('INSTAGRAM_HANDLE', 'votre-compte');
// 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');
define('TIKTOK_URL', 'https://www.tiktok.com/@votre-compte');
define('TIKTOK_HANDLE', 'votre-compte');
----
=== 📁 Gestion des images
=== YouTube — Option A : clé API (recommandée)
. 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é)
Permet la détection des Shorts, les compteurs d'abonnés et les meilleurs résultats de recherche.
=== 🎯 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.
=== ✨ Fonctionnalités
- 💳 *PayPal Me intégré* : Redirection sécurisée vers votre compte PayPal
- 🎯 *Montants prédéfinis* : Boutons rapides avec montants suggérés
- ✍️ *Montant personnalisé* : Champ libre pour des dons de montant libre
- 🔒 *Sécurisé* : Validation stricte des URLs et protection XSS
- 📱 *Responsive* : Interface optimisée mobile et desktop
- 🎨 *Intégré* : Design cohérent avec la charte graphique du site
- ♿ *Accessible* : Conforme aux standards d'accessibilité
=== ⚙️ Configuration
Pour activer le système de dons, ajoutez dans votre `config.local.php` :
. Créer un projet sur https://console.cloud.google.com
. Activer *YouTube Data API v3*
. Créer une clé API (*APIs & Services > Credentials*)
[source,php]
----
define('YOUTUBE_API_KEY', 'AIzaSy...');
define('YOUTUBE_CHANNEL_HANDLE', 'votre-chaine');
define('YOUTUBE_CHANNEL_ID', 'UC...'); // optionnel si handle défini
define('YOUTUBE_VIDEOS_COUNT', 9); // 3 Shorts + 6 vidéos normales
----
NOTE: Le quota gratuit est de 10 000 unités/jour. Chaque chargement non mis en cache coûte ~101 unités (100 pour `search.list` + 1 pour `videos.list`). Avec un cache de 15 minutes, la consommation réelle est très faible.
=== YouTube — Option B : flux RSS public (sans clé API)
Aucune détection des Shorts, ordre chronologique uniquement.
[source,php]
----
define('YOUTUBE_CHANNEL_ID', 'UC...');
define('YOUTUBE_VIDEOS_COUNT', 9);
----
=== WordPress
[source,php]
----
define('WORDPRESS_ENABLED', true);
define('WORDPRESS_URL', 'https://votre-site.com');
define('WORDPRESS_POSTS_COUNT', 6);
----
=== Contenu embarqué optionnel
Des posts Instagram ou vidéos TikTok spécifiques peuvent remplacer les iframes de profil :
[source,php]
----
define('INSTAGRAM_POST_URLS', [
'https://www.instagram.com/p/XXX/',
]);
define('TIKTOK_VIDEO_URLS', [
'https://www.tiktok.com/@compte/video/XXXXX',
]);
----
=== Fuseau horaire
[source,php]
----
define('DEFAULT_TIMEZONE', 'Indian/Reunion');
----
== Annonce du prochain live
Système d'annonce configurable avec calcul automatique multi-fuseaux (Ma'ohi Nui, Martinique/Guadeloupe, Guyane, France, Kanaky).
[source,php]
----
define('NEXT_LIVE_ENABLED', true);
define('NEXT_LIVE_TITLE', 'Prochain live');
define('NEXT_LIVE_DESCRIPTION', 'Description de l\'événement.');
define('NEXT_LIVE_DATE', '2025-10-11 10:00:00');
define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg');
----
Images supportées : portrait 4:5 (1080×1350px), carré 1:1, paysage 16:9.
== Système de dons
[source,php]
----
// Activer le système de dons
define('DONATIONS_ENABLED', true);
// URL PayPal Me (sans le montant)
define('PAYPAL_ME_URL', 'https://www.paypal.com/paypalme/votre-compte');
// Montants suggérés (optionnel)
define('DONATION_AMOUNTS', [5, 10, 20, 50, 100]);
// Devise (optionnel, EUR par défaut)
define('PAYPAL_ME_URL', 'https://www.paypal.com/paypalme/votre-compte');
define('DONATION_AMOUNTS', [5, 10, 20, 50, 100]);
define('DONATION_CURRENCY', 'EUR');
----
=== 📄 Personnalisation
. Copiez le fichier sample pour créer votre page de dons :
+
[source,bash]
----
cp dons.sample.php dons.php
----
. Personnalisez le contenu en remplaçant les placeholders :
* `[VOTRE ORGANISATION]` par le nom de votre organisation
* `[VOTRE CAUSE]` par votre cause/mission
* `[OBJECTIF X]` par vos objectifs spécifiques
== Système de compte à rebours
=== 🎯 Interface utilisateur
Page de maintenance ou de lancement avec compte à rebours multi-fuseaux.
Une fois activé, le système de dons ajoute :
[source,php]
----
define('COUNTDOWN_ENABLED', true);
define('COUNTDOWN_TARGET_DATE', '2025-10-11 00:00:00');
----
- 💝 *Icône cœur rouge* dans le header (sans cadre)
- 📋 *Lien "Soutenir"* dans la sidebar
- 📄 *Page dédiée* accessible via `/dons.php`
== Sécurité
=== 🔒 Sécurité
Le système intègre plusieurs protections :
- ✅ *Validation URL PayPal* : Seules les URLs PayPal Me valides sont acceptées
- ✅ *Protection XSS* : Échappement de toutes les sorties utilisateur
- ✅ *Validation montants* : Contrôle des montants min/max
- ✅ *Headers sécurisés* : CSP et autres headers de sécurité
- ✅ *Ouverture sécurisée* : Liens avec `noopener noreferrer`
== 🛡️ Configuration de sécurité Apache
Le fichier `conf/.htaccess.sample` fourni inclut des règles de sécurité importantes pour protéger votre installation :
=== Protections incluses :
- 🚫 *Blocage des fichiers de configuration* : Empêche l'accès direct aux fichiers `.php`, `.config`, etc.
- 🔒 *Protection des répertoires sensibles* : Bloque l'accès aux dossiers `/includes/`, `/cache/`, `/docs/`
- 🗂️ *Désactivation de l'exploration* : Empêche la liste des fichiers dans les répertoires
- 🔐 *Blocage des fichiers cachés* : Protège les fichiers commençant par `.`
- 📄 *Blocage des fichiers temporaires* : Empêche l'accès aux `.sample`, `.bak`, `.log`, etc.
=== Installation :
=== Apache
[source,bash]
----
cp conf/.htaccess.sample .htaccess
----
WARNING: Cette configuration est essentielle pour la sécurité de votre installation. Ne pas l'utiliser expose vos fichiers de configuration aux visiteurs.
Le fichier protège les répertoires sensibles (`/includes/`, `/cache/`), bloque l'accès aux `.sample`, `.bak`, `.log`, désactive l'exploration des répertoires.
== 🛡️ Configuration de sécurité Nginx
WARNING: Ne pas déployer sans ce fichier — les fichiers de configuration seraient accessibles publiquement.
Pour les serveurs Nginx, utilisez le fichier `conf/nginx.conf.sample` qui inclut les mêmes protections :
=== Protections incluses :
- 🚫 *Blocage des fichiers de configuration* : Empêche l'accès direct aux fichiers sensibles
- 🔒 *Protection des répertoires sensibles* : Bloque l'accès aux dossiers critiques
- 🗂️ *Désactivation de l'exploration* : `autoindex off`
- 🔐 *Blocage des fichiers cachés* : Protection des fichiers commençant par `.`
- 📄 *Optimisations* : Cache, compression gzip, headers de sécurité
=== Installation :
=== Nginx
[source,bash]
----
# Adaptez les chemins dans conf/nginx.conf.sample puis :
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/votre-site
sudo ln -s /etc/nginx/sites-available/votre-site /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
----
== 📱 Progressive Web App (PWA)
== Progressive Web App (PWA)
Cette plateforme est une PWA complète offrant :
- Installation native (bouton automatique dans le header)
- Mode hors ligne via Service Worker
- Indicateur visuel de perte de connexion
=== ✨ Fonctionnalités PWA
Compatibilité : Chrome/Edge (Android/Desktop), Safari (iOS 11.3+), Firefox Android, Samsung Internet.
- 📲 *Installation native* : Bouton d'installation automatique dans l'interface
- 🌐 *Mode hors ligne* : Cache intelligent des pages et ressources visitées
- 📡 *Détection d'état* : Indicateur visuel en cas de perte de connexion
- ⚡ *Performance* : Chargement instantané des ressources en cache
- 📱 *Responsive* : Interface adaptée pour l'utilisation en application mobile
[source,bash]
----
cp site.webmanifest.sample site.webmanifest
----
=== 📥 Comment installer l'application
== Déploiement (hébergement mutualisé)
. 🔄 *Automatique* : Un bouton "Installer" apparaît dans le header lors de la première visite
. 🔧 *Manuel* :
* 🌐 *Chrome/Edge* : Menu → "Installer kaubuntu.re"
* 🍎 *Safari iOS* : Partager → "Ajouter à l'écran d'accueil"
* 🦊 *Firefox Android* : Menu → "Installer"
. Vérifier que PHP 8.0+ avec les extensions requises est disponible
. Transférer les fichiers via FTP/SFTP
. Créer `includes/config.local.php` sur le serveur
. Vérifier que le dossier `cache/` est accessible en écriture : `chmod 755 cache/`
. Configurer HTTPS
=== 🌐 Compatibilité PWA
== Développement
- ✅ Chrome/Edge (Android/Desktop)
- ✅ Safari (iOS 11.3+)
- ✅ Firefox (Android)
- ✅ Samsung Internet
[source,bash]
----
git checkout -b ma-fonctionnalite
# ... modifications ...
git commit -m "feat: description courte"
git push origin ma-fonctionnalite
----
=== 📄 Fichiers PWA
== Licence
- 🔧 `sw.js` : Service Worker gérant le cache et mode offline
- 📋 `site.webmanifest` : Configuration de l'application (nom, icônes, etc.)
- 🪟 `browserconfig.xml` : Support des tuiles Windows
== 🚀 Déploiement
Pour déployer sur un serveur mutualisé:
. 🐘 Assurez-vous que votre hébergeur supporte PHP (version 7.0 minimum recommandée)
. 🔒 *Configurez HTTPS* (obligatoire pour les fonctionnalités PWA)
. 📤 Transférez tous les fichiers via FTP dans le répertoire racine de votre site
. 🔧 Vérifiez que les permissions des fichiers sont correctement définies (644 pour les fichiers, 755 pour les dossiers)
. 🌐 Configurez votre domaine pour pointer vers le dossier où vous avez installé l'application
. 🧪 Testez l'installation PWA via les outils de développement du navigateur
== 👨‍💻 Développement
Si vous souhaitez contribuer au développement:
. 🌿 Créez une branche pour vos modifications: `git checkout -b ma-nouvelle-fonctionnalité`
. 💾 Committez vos changements: `git commit -m 'Ajout d'une nouvelle fonctionnalité'`
. 📤 Poussez vers la branche: `git push origin ma-nouvelle-fonctionnalité`
. 🔀 Soumettez une pull request
== 📜 License
Copyright (C) 2025 Cédric Famibelle-Pronzola & *Ka-Ubuntu*
Copyright (C) 2025 *ORGANISATION KA INTERNATIONALE* (OKI) & *Ka-Ubuntu*
NOTE: *Ka-Ubuntu* est un parti politique panafricaniste et indépendantiste réunionnais, œuvrant pour la souveraineté numérique et technologique de La Réunion dans une perspective panafricaniste.
=== 🇫🇷 FR
=== FR
Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la licence publique générale GNU Affero publiée par la Free Software Foundation, soit la version 3 de la licence, soit (à votre choix) toute version ultérieure.
@@ -398,7 +297,7 @@ Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GAR
Vous devriez avoir reçu une copie de la licence publique générale GNU Affero avec ce programme. Si ce n'est pas le cas, consultez https://www.gnu.org/licenses/.
=== 🇺🇸 EN
=== EN
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -406,36 +305,10 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
== 📊 Analytics et Statistiques
== Contact
=== 🔒 Respect de la vie privée
Cette plateforme utilise *Plausible Analytics*, une solution d'analyse web respectueuse de la vie privée qui :
- ✅ *Sans cookies* : Aucun cookie de suivi n'est installé
- ✅ *Conforme RGPD* : Respecte les réglementations européennes sur la protection des données
- ✅ *Open Source* : Code source ouvert et auditable
- ✅ *Données anonymes* : Aucune donnée personnelle collectée
- ✅ *Sans collecte inter-sites* : Pas de profilage des utilisateurs
=== 📈 Données collectées
Plausible collecte uniquement des statistiques anonymes :
- 📍 Pages visitées
- 🌍 Pays d'origine (basé sur l'IP, sans stockage)
- 📱 Type d'appareil (mobile, desktop, tablette)
- 🌐 Navigateur utilisé
- 📊 Temps passé sur le site
=== ⚙️ Configuration
Les analytics sont automatiquement activés via le script Plausible intégré dans le `<head>` de chaque page. La configuration est gérée dans `includes/security.php` avec les autorisations CSP appropriées.
== 📞 Contact
Pour toute question ou suggestion concernant cette plateforme, veuillez nous contacter à mailto:multimedia@kaubuntu.re[multimedia@kaubuntu.re].
mailto:zinfoskaubuntu@gmail.com[zinfoskaubuntu@gmail.com]
---
🌍 *Ka-Ubuntu* - _Pour une Réunion libre et souveraine dans l'unité panafricaniste_ ✊🏿
*Ka-Ubuntu* - _Pour une Réunion libre et souveraine dans l'unité panafricaniste_
+7 -1
View File
@@ -1,4 +1,9 @@
<?php
// Démarrer la session avant tout
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Inclure la configuration
require_once '../includes/config.php';
@@ -44,7 +49,8 @@ if ($type === 'category' && $categoryId <= 0) {
// Récupérer les vidéos en fonction du type
$videos = [];
$offset = $page * LOAD_MORE_COUNT;
// Utiliser offset directement s'il est fourni, sinon calculer à partir de page
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : $page * LOAD_MORE_COUNT;
switch ($type) {
case 'recent':
+2 -2
View File
@@ -164,7 +164,7 @@ if ($categoryId && isset($allCategories[$categoryId])) {
<?php include 'includes/footer.php'; ?>
<?php include 'includes/mobile-menu.php'; ?>
<script src="js/main.js"></script>
<script src="js/categories.js"></script>
<script src="js/main.js?v=<?php echo filemtime('js/main.js'); ?>"></script>
<script src="js/categories.js?v=<?php echo filemtime('js/categories.js'); ?>"></script>
</body>
</html>
+1 -1
View File
@@ -113,7 +113,7 @@ setSecurityHeaders();
];
foreach($timezones as $name => $timezone):
$targetDateLocal = new DateTime(COUNTDOWN_TARGET_DATE, new DateTimeZone('Indian/Reunion'));
$targetDateLocal = new DateTime(COUNTDOWN_TARGET_DATE, new DateTimeZone(DEFAULT_TIMEZONE));
$targetDateLocal->setTimezone(new DateTimeZone($timezone));
// Vérifier si c'est un jour différent
+784
View File
@@ -0,0 +1,784 @@
/* ============================================================
HERO SECTION
============================================================ */
.social-hero {
background: linear-gradient(135deg, #0a0a0a 0%, #1a0000 60%, #2d0000 100%);
border-radius: 12px;
padding: 48px 40px;
color: white;
margin-bottom: 0;
}
.social-hero-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
text-align: center;
}
.social-hero-brand {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
}
.social-hero-logo {
height: 90px;
width: auto;
filter: drop-shadow(0 4px 16px rgba(255, 0, 0, 0.4));
}
.social-hero-text h1 {
font-size: 2.2rem;
font-weight: 800;
color: white;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.social-hero-text p {
font-size: 1rem;
color: rgba(255, 255, 255, 0.75);
max-width: 480px;
line-height: 1.5;
}
/* Platform Navigation Badges */
.platform-nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
align-items: flex-start;
}
.platform-badge-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.platform-badge-count {
font-size: 12px;
font-weight: 700;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.3px;
line-height: 1;
}
.platform-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 22px;
border-radius: 50px;
font-size: 15px;
font-weight: 600;
text-decoration: none;
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 2px solid transparent;
color: white !important;
letter-spacing: 0.2px;
}
.platform-badge:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.platform-badge:active {
transform: scale(0.93) translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition-duration: 0.08s;
}
.platform-badge.platform-youtube { background: #FF0000; }
.platform-badge.platform-instagram {
background: linear-gradient(135deg, #F58529, #DD2A7B, #8134AF);
}
.platform-badge.platform-tiktok {
background: #000;
border-color: #EE1D52;
}
.platform-badge.platform-wp {
background: linear-gradient(135deg, #1a0000, #2d0000);
border-color: rgba(255, 255, 255, 0.2);
}
/* ============================================================
SOCIAL SECTIONS
============================================================ */
.social-section {
margin-bottom: 0;
}
.social-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
border-radius: 10px 10px 0 0;
flex-wrap: wrap;
gap: 12px;
}
.social-section-header-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.social-section-header h2 {
font-size: 22px;
font-weight: 700;
color: white;
margin: 0;
white-space: nowrap;
}
.social-section-header > .social-section-header-left > i {
font-size: 26px;
color: white;
flex-shrink: 0;
}
.platform-handle {
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.platform-stat-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: white;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 20px;
padding: 3px 10px;
white-space: nowrap;
flex-shrink: 0;
}
.platform-stat-badge i {
font-size: 11px;
opacity: 0.85;
}
/* Platform header gradients */
.platform-youtube-header { background: linear-gradient(135deg, #a80000, #FF0000); }
.platform-facebook-header { background: linear-gradient(135deg, #0d47a1, #1877F2); }
.platform-instagram-header {
background: linear-gradient(135deg, #6a0dad, #DD2A7B, #F77737);
}
.platform-tiktok-header {
background: linear-gradient(135deg, #010101, #1a1a1a);
border-bottom: 3px solid #EE1D52;
}
.platform-x-header {
background: linear-gradient(135deg, #000, #1a1a1a);
border-bottom: 3px solid rgba(255, 255, 255, 0.15);
}
/* CTA Buttons */
.platform-cta-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
color: white !important;
flex-shrink: 0;
white-space: nowrap;
}
.platform-cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.platform-youtube-btn { background: rgba(255, 255, 255, 0.2); }
.platform-youtube-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-facebook-btn { background: rgba(255, 255, 255, 0.2); }
.platform-facebook-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-instagram-btn { background: rgba(255, 255, 255, 0.2); }
.platform-instagram-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-tiktok-btn { background: rgba(238, 29, 82, 0.7); }
.platform-tiktok-btn:hover { background: #EE1D52; }
.platform-x-btn { background: rgba(255, 255, 255, 0.15); }
.platform-x-btn:hover { background: rgba(255, 255, 255, 0.25); }
/* ============================================================
TWO-COLUMN LAYOUT
============================================================ */
.social-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 0;
}
/* Colonnes alignées en haut (chacune prend sa hauteur naturelle) */
.social-two-col--top {
align-items: start;
}
@media (max-width: 900px) {
.social-two-col {
grid-template-columns: 1fr;
}
}
/* ============================================================
EMBED CONTAINERS (Facebook, X)
============================================================ */
.social-embed-container {
background: var(--card-bg);
border-radius: 0 0 10px 10px;
overflow: hidden;
min-height: 500px;
display: flex;
align-items: flex-start;
justify-content: center;
box-shadow: var(--card-shadow);
padding: 12px;
max-width: 100%;
box-sizing: border-box;
}
/* Empêche les iframes tierces de dépasser du conteneur */
.social-embed-container iframe,
.social-embed-container > div {
max-width: 100%;
}
.social-embed-container .twitter-timeline {
display: block;
width: 100%;
}
/* Profile iframes (TikTok creator embed, Instagram profile embed) */
.platform-profile-embed {
padding: 0;
min-height: unset;
align-items: stretch;
}
.platform-profile-iframe {
width: 100%;
height: 560px;
border: 0;
display: block;
}
.platform-profile-iframe--instagram {
height: 360px;
}
@media (max-width: 768px) {
.platform-profile-iframe {
height: 480px;
}
.platform-profile-iframe--instagram {
height: 300px;
}
}
/* ============================================================
YOUTUBE VIDEO GRID
============================================================ */
.social-video-grid {
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 20px;
box-shadow: var(--card-shadow);
}
.social-video-card {
display: flex;
flex-direction: column;
text-decoration: none !important;
color: var(--text-color) !important;
transition: transform 0.25s ease, box-shadow 0.25s ease;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
box-shadow: var(--card-shadow);
}
.social-video-card:hover {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
color: var(--text-color) !important;
}
.social-video-card .video-thumbnail {
position: relative;
padding-top: 56.25%;
overflow: hidden;
background: #000;
}
.social-video-card .video-thumbnail img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.social-video-card:hover .video-thumbnail img {
transform: scale(1.05);
}
/* Sous-titre de section (Shorts / Vidéos) */
.yt-subgrid-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-muted, #888);
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 16px 20px 4px;
}
.yt-subgrid-label i {
font-size: 12px;
}
/* Grille Shorts : cartes portrait centrées */
.social-shorts-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 220px));
justify-content: center;
}
/* Shorts — ratio portrait 9:16 */
.video-card--short .video-thumbnail {
padding-top: 177.78%;
}
.short-badge {
position: absolute;
bottom: 8px;
left: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
background: #FF0000;
color: white;
font-size: 11px;
font-weight: 700;
padding: 3px 8px;
border-radius: 4px;
letter-spacing: 0.3px;
pointer-events: none;
text-transform: uppercase;
}
.youtube-play-icon {
font-size: 44px !important;
color: #FF0000 !important;
filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.6));
}
/* ============================================================
FALLBACK CARDS
============================================================ */
.platform-fallback-card {
display: flex;
align-items: center;
gap: 32px;
padding: 48px 40px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
justify-content: center;
text-align: center;
min-height: 240px;
}
.platform-fallback-icon {
font-size: 90px;
line-height: 1;
flex-shrink: 0;
opacity: 0.9;
}
.youtube-fallback-icon i { color: #FF0000; }
.instagram-fallback-icon i {
background: linear-gradient(45deg, #F58529, #DD2A7B, #8134AF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tiktok-fallback-icon i { color: #010101; }
[data-theme="dark"] .tiktok-fallback-icon i { color: #ffffff; }
.platform-fallback-content h3 {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
}
.platform-fallback-content p {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 24px;
max-width: 380px;
line-height: 1.5;
}
/* ============================================================
INSTAGRAM POSTS GRID
============================================================ */
.instagram-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.instagram-post-wrapper {
display: flex;
justify-content: center;
}
/* ============================================================
TIKTOK VIDEOS GRID
============================================================ */
.tiktok-videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.tiktok-video-wrapper {
display: flex;
justify-content: center;
}
/* ============================================================
WORDPRESS ARTICLES
============================================================ */
.wp-section-header { background: linear-gradient(135deg, #0a0a0a 0%, #1a0000 60%, #2d0000 100%); }
.wp-section-logo {
height: 28px;
width: auto;
filter: drop-shadow(0 2px 6px rgba(255, 0, 0, 0.4));
flex-shrink: 0;
}
.wp-cta-btn { background: rgba(255, 255, 255, 0.2); }
.wp-cta-btn:hover { background: rgba(255, 255, 255, 0.3); }
.wp-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.wp-post-card {
display: flex;
flex-direction: column;
text-decoration: none !important;
color: var(--text-color) !important;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
box-shadow: var(--card-shadow);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.wp-post-card:hover {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
color: var(--text-color) !important;
}
.wp-post-thumbnail {
position: relative;
padding-top: 56.25%;
overflow: hidden;
background: var(--border-color);
}
.wp-post-thumbnail img {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.wp-post-card:hover .wp-post-thumbnail img {
transform: scale(1.05);
}
.wp-post-thumbnail--placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.wp-post-thumbnail--placeholder i {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 40px;
color: var(--text-secondary);
opacity: 0.4;
}
.wp-post-info {
padding: 14px 16px 16px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.wp-post-title {
font-size: 15px;
font-weight: 700;
line-height: 1.4;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.wp-post-excerpt {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.wp-post-meta {
display: flex;
gap: 14px;
font-size: 12px;
color: var(--text-secondary);
margin-top: auto;
flex-wrap: wrap;
}
.wp-post-meta span {
display: flex;
align-items: center;
gap: 5px;
}
@media (max-width: 768px) {
.wp-posts-grid {
grid-template-columns: 1fr;
padding: 16px;
}
}
/* ============================================================
HEADER BRAND
============================================================ */
.header-brand {
display: flex;
align-items: center;
flex-shrink: 0;
}
.header-logo-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--text-color);
}
.header-logo-img {
height: 40px;
width: auto;
}
.header-site-name {
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}
/* ============================================================
DARK THEME
============================================================ */
[data-theme="dark"] .social-embed-container,
[data-theme="dark"] .platform-fallback-card,
[data-theme="dark"] .social-video-grid,
[data-theme="dark"] .instagram-posts-grid,
[data-theme="dark"] .tiktok-videos-grid {
background: var(--card-bg);
}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 1000px) {
.header-site-name {
display: none;
}
}
@media (max-width: 768px) {
.social-hero {
padding: 28px 20px;
border-radius: 8px;
}
.social-hero-text h1 {
font-size: 1.6rem;
}
.social-hero-logo {
height: 70px;
}
.platform-badge span {
display: none;
}
.platform-badge {
width: 50px;
height: 50px;
padding: 0;
justify-content: center;
border-radius: 50%;
font-size: 18px;
}
.platform-badge-count {
font-size: 10px;
}
.social-section-header {
padding: 14px 16px;
}
.social-section-header h2 {
font-size: 18px;
}
.platform-handle {
display: none;
}
.platform-fallback-card {
padding: 32px 20px;
flex-direction: column;
gap: 20px;
}
.platform-fallback-icon {
font-size: 70px;
}
.platform-fallback-content h3 {
font-size: 20px;
}
.instagram-posts-grid,
.tiktok-videos-grid {
grid-template-columns: 1fr;
padding: 16px;
}
.social-embed-container {
min-height: 400px;
padding: 4px;
}
.platform-profile-embed {
padding: 0;
}
}
@media (max-width: 480px) {
.platform-nav {
gap: 8px;
}
.social-hero-brand {
flex-direction: column;
}
.social-hero-logo {
height: 60px;
}
}
/* ============================================================
SCROLL ARRIVAL HIGHLIGHT
============================================================ */
@keyframes section-arrive {
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.5); }
40% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0.15); }
100% { box-shadow: 0 0 0 18px rgba(255, 0, 0, 0); }
}
.section-arrive {
animation: section-arrive 0.9s cubic-bezier(0.22, 1, 0.36, 1) forwards;
border-radius: 12px;
}
+223 -106
View File
@@ -347,7 +347,7 @@ img {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 25px 20px;
padding: 16px 40px;
background-color: var(--main-bg);
position: sticky;
top: 0;
@@ -357,72 +357,8 @@ img {
max-width: calc(100vw - 250px);
margin-bottom: 20px;
margin-left: -20px;
padding-left: 40px;
padding-right: 40px;
box-sizing: border-box;
}
.search-container {
flex: 1;
max-width: 600px;
margin: 0 auto;
}
.search-container form {
display: flex;
position: relative;
}
.search-container input {
width: 100%;
padding: 12px 45px 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 25px;
background-color: #f8f8f8;
font-size: 16px;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
color: var(--text-color);
}
.search-container input:focus {
outline: none;
border-color: var(--primary-red);
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-container input::placeholder {
color: #9e9e9e;
transition: opacity 0.3s ease;
}
.search-container input:focus::placeholder {
opacity: 0.7;
}
.search-container button {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
background-color: var(--primary-red);
color: white;
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.3s ease;
}
.search-container button:hover {
background-color: #d40000;
transform: translateY(-50%) scale(1.05);
gap: 16px;
}
.social-icons, .action-icons {
@@ -641,7 +577,7 @@ img {
.hero-video-info {
position: absolute;
bottom: 70px;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
@@ -750,6 +686,40 @@ img {
display: flex;
flex-direction: row;
overflow: hidden;
/* Arrière-plan flou - sera défini en ligne avec l'URL de l'image */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* Overlay d'assombrissement pour améliorer la lisibilité */
.hero-next-live::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.6) 50%,
rgba(0, 0, 0, 0.85) 100%
);
z-index: 1;
}
/* Effet de flou sur l'arrière-plan */
.hero-next-live::after {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: inherit;
filter: blur(20px);
z-index: 0;
}
.hero-next-live-image-container {
@@ -757,8 +727,9 @@ img {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-dark);
padding: 15px;
position: relative;
z-index: 2;
}
.hero-next-live-image {
@@ -766,55 +737,65 @@ img {
height: 100%;
object-fit: contain;
object-position: center;
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.6));
position: relative;
z-index: 2;
}
.hero-next-live-content {
flex: 0 0 50%;
padding: 20px 15px;
padding: 25px 20px;
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%);
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(10px);
color: white;
position: relative;
z-index: 2;
}
.hero-next-live-content i.fa-calendar-alt {
font-size: 35px;
margin-bottom: 8px;
margin-bottom: 10px;
color: var(--primary-red);
}
.hero-next-live-content h2 {
font-size: 20px;
font-size: 19px;
font-weight: 700;
margin-bottom: 8px;
margin-bottom: 12px;
color: white;
line-height: 1.2;
line-height: 1.4;
max-width: 90%;
}
.hero-next-live-content p {
font-size: 14px;
margin: 0 auto 12px;
color: rgba(255, 255, 255, 0.95);
line-height: 1.3;
line-height: 1.4;
max-width: 95%;
}
.hero-next-live-date {
font-size: 15px !important;
font-size: 13px !important;
font-weight: 600;
color: white !important;
background: var(--primary-red);
padding: 7px 14px;
padding: 8px 12px;
border-radius: 8px;
display: inline-block;
margin-bottom: 12px !important;
margin-bottom: 10px !important;
box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3);
max-width: 90%;
line-height: 1.4;
}
.hero-next-live-date i {
margin-right: 8px;
margin-right: 6px;
}
.hero-next-live-datetime {
@@ -824,21 +805,24 @@ img {
.hero-next-live-timezones {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 6px;
justify-content: center;
margin-top: 12px;
padding: 12px 16px;
margin-top: 10px;
padding: 10px 12px;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 8px;
max-width: 95%;
margin-left: auto;
margin-right: auto;
}
.hero-timezone-item {
font-size: 13px;
font-size: 11px;
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;
padding: 4px 8px;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.15);
flex: 0 1 auto;
@@ -2006,19 +1990,18 @@ img {
.sidebar {
width: 70px;
}
.main-content, .footer {
margin-left: 70px;
width: calc(100% - 70px);
}
/* Ajuster le header pour le mode sidebar compact */
.header {
width: 100%;
max-width: calc(100vw - 70px);
margin-left: -20px;
padding-left: 40px;
padding-right: 40px;
padding-left: 24px;
padding-right: 24px;
box-sizing: border-box;
}
@@ -2079,26 +2062,21 @@ img {
width: 100%;
}
/* Ajuster le header pour l'affichage mobile */
.header {
width: 100vw;
max-width: 100vw;
margin-left: -20px;
padding-left: 20px;
padding-right: 20px;
padding-left: 16px;
padding-right: 16px;
box-sizing: border-box;
position: relative;
left: 0;
}
.mobile-menu-toggle {
display: block;
}
.search-container {
max-width: 60%;
}
.social-icons {
display: none;
}
@@ -2281,8 +2259,14 @@ i.icon-tiktok,
}
[data-theme="dark"] .header i.icon-tiktok,
[data-theme="dark"] .header .fab.fa-tiktok.icon-tiktok {
color: #ffffff !important; /* Blanc TikTok en mode sombre - header seulement */
[data-theme="dark"] .header .fab.fa-tiktok.icon-tiktok,
[data-theme="dark"] .sidebar i.icon-tiktok,
[data-theme="dark"] .sidebar .fab.fa-tiktok.icon-tiktok,
[data-theme="dark"] .mobile-menu i.icon-tiktok,
[data-theme="dark"] .mobile-menu .fab.fa-tiktok.icon-tiktok,
[data-theme="dark"] .footer i.icon-tiktok,
[data-theme="dark"] .footer .fab.fa-tiktok.icon-tiktok {
color: #ffffff !important;
}
i.icon-tiktok-accent {
@@ -2492,8 +2476,43 @@ i.icon-mastodon,
flex-direction: row;
border-radius: 8px;
overflow: hidden;
background-color: var(--card-bg);
min-height: 500px;
position: relative;
/* Arrière-plan flou - sera défini en ligne avec l'URL de l'image */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* Overlay d'assombrissement pour améliorer la lisibilité */
.next-live-announcement::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.6) 50%,
rgba(0, 0, 0, 0.85) 100%
);
z-index: 1;
}
/* Effet de flou sur l'arrière-plan */
.next-live-announcement::after {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: inherit;
filter: blur(20px);
z-index: 0;
border-radius: 8px;
}
.next-live-image-container {
@@ -2501,8 +2520,9 @@ i.icon-mastodon,
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-dark);
padding: 20px;
position: relative;
z-index: 2;
}
.next-live-image {
@@ -2511,6 +2531,9 @@ i.icon-mastodon,
object-fit: contain;
object-position: center;
max-height: 600px;
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.7));
position: relative;
z-index: 2;
}
.next-live-content {
@@ -2521,7 +2544,10 @@ i.icon-mastodon,
justify-content: center;
align-items: center;
text-align: center;
background: linear-gradient(135deg, var(--card-bg) 0%, var(--bg-dark) 100%);
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(15px);
position: relative;
z-index: 2;
}
.next-live-content i.fa-calendar-alt {
@@ -2534,16 +2560,18 @@ i.icon-mastodon,
font-size: 32px;
font-weight: 700;
margin-bottom: 20px;
color: var(--text-color);
color: white;
line-height: 1.3;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.next-live-content p {
font-size: 20px;
margin: 0 auto 25px;
color: var(--text-muted);
color: rgba(255, 255, 255, 0.95);
line-height: 1.5;
max-width: 100%;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.next-live-date {
@@ -2813,6 +2841,42 @@ i.icon-mastodon,
}
}
/* Media query pour très petits écrans (iPhone 4S, etc.) */
@media (max-width: 360px) {
/* Réduction de l'icône calendar pour .next-live-announcement sur direct.php */
.next-live-content i.fa-calendar-alt {
font-size: 35px;
margin-bottom: 8px;
}
.next-live-content h2 {
font-size: 20px;
margin-bottom: 12px;
}
.next-live-content p {
font-size: 14px;
margin-bottom: 15px;
}
.next-live-date {
font-size: 14px !important;
padding: 8px 14px;
margin-bottom: 12px !important;
}
.next-live-timezones {
gap: 6px;
padding: 10px 12px;
margin-top: 10px;
}
.timezone-item {
font-size: 11px;
padding: 4px 8px;
}
}
/* Catégories */
.category-page {
width: 100%;
@@ -3170,4 +3234,57 @@ i.icon-mastodon,
}
}
/* ============================================================
BACK TO TOP BUTTON
============================================================ */
#back-to-top {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 900;
width: 46px;
height: 46px;
border-radius: 50%;
border: none;
background: #FF0000;
color: white;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(255, 0, 0, 0.45);
opacity: 0;
visibility: hidden;
transform: translateY(12px);
transition: opacity 0.25s ease, visibility 0.25s ease, transform 0.25s ease, background 0.2s ease;
}
#back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
#back-to-top:hover {
background: #cc0000;
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(255, 0, 0, 0.5);
}
#back-to-top:active {
transform: scale(0.9);
transition-duration: 0.08s;
}
@media (max-width: 768px) {
#back-to-top {
bottom: 20px;
right: 16px;
width: 40px;
height: 40px;
font-size: 15px;
}
}
+43 -23
View File
@@ -14,8 +14,9 @@ require_once 'includes/structured-data.php';
// Appliquer les en-têtes de sécurité
setSecurityHeaders();
// Vérifier s'il y a un direct en cours
$liveStream = getLiveStream();
// Récupérer la vidéo à afficher selon le mode configuré
$displayData = getDisplayVideo();
$video = $displayData['video'];
?>
<!DOCTYPE html>
<html lang="fr">
@@ -113,40 +114,54 @@ $liveStream = getLiveStream();
</div>
<div class="live-container">
<?php
// Vérifier s'il y a un direct en cours
$liveStream = getLiveStream();
if ($liveStream) {
// Afficher le direct en cours
<?php
// Récupérer les données d'affichage
$isLive = $displayData['isLive'];
$badge = $displayData['badge'];
$mode = $displayData['mode'];
if ($video) {
// Construire les paramètres de l'iframe selon le mode
$iframeParams = '';
if ($mode === 'auto' && $isLive) {
// Mode auto avec direct : autoplay
$iframeParams = '?autoplay=1';
}
// Mode static : pas d'autoplay
// Afficher la vidéo
?>
<div class="live-badge large">
<i class="fas fa-circle"></i> EN DIRECT
<?php if ($isLive): ?>
<i class="fas fa-circle"></i> <?php echo htmlspecialchars($badge); ?>
<?php else: ?>
<?php echo htmlspecialchars($badge); ?>
<?php endif; ?>
</div>
<div class="live-player">
<iframe
src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $liveStream['id']; ?>?autoplay=1"
frameborder="0"
<iframe
src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $video['id']; ?><?php echo $iframeParams; ?>"
frameborder="0"
allowfullscreen="allowfullscreen"
allow="autoplay; fullscreen"
title="<?php echo htmlspecialchars($liveStream['title']); ?>">
title="<?php echo htmlspecialchars($video['title']); ?>">
</iframe>
</div>
<div class="live-info">
<h1 class="live-title"><?php echo htmlspecialchars($liveStream['title']); ?></h1>
<h1 class="live-title"><?php echo htmlspecialchars($video['title']); ?></h1>
<div class="live-channel-info">
<?php if (strpos($liveStream['channelAvatar'], 'default-avatar.png') !== false || empty($liveStream['channelAvatar'])): ?>
<?php if (strpos($video['channelAvatar'], 'default-avatar.png') !== false || empty($video['channelAvatar'])): ?>
<div class="channel-avatar-placeholder">
<i class="fas fa-user-circle"></i>
</div>
<?php else: ?>
<img src="<?php echo $liveStream['channelAvatar']; ?>" alt="<?php echo $liveStream['channel']; ?>" class="channel-avatar">
<img src="<?php echo $video['channelAvatar']; ?>" alt="<?php echo $video['channel']; ?>" class="channel-avatar">
<?php endif; ?>
<span class="channel-name"><?php echo $liveStream['channel']; ?></span>
<span class="channel-name"><?php echo $video['channel']; ?></span>
</div>
<?php if (!empty($liveStream['description'])): ?>
<?php if (!empty($video['description'])): ?>
<div class="live-description">
<?php echo markdown_to_html($liveStream['description']); ?>
<?php echo markdown_to_html($video['description']); ?>
</div>
<?php endif; ?>
</div>
@@ -157,8 +172,13 @@ $liveStream = getLiveStream();
if ($showNextLiveAnnouncement) {
// Afficher l'annonce du prochain live
// Définir l'image de fond si disponible
$bgImageStyle = '';
if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)) {
$bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
}
?>
<div class="next-live-announcement">
<div class="next-live-announcement" style="<?php echo $bgImageStyle; ?>">
<?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); ?>"
@@ -170,12 +190,12 @@ $liveStream = getLiveStream();
<i class="fas fa-calendar-alt"></i>
<?php
if (!empty(NEXT_LIVE_DATE)) {
$liveDate = new DateTime(NEXT_LIVE_DATE, new DateTimeZone('Indian/Reunion'));
$liveDate = new DateTime(NEXT_LIVE_DATE, new DateTimeZone(DEFAULT_TIMEZONE));
$dayFormatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Indian/Reunion',
DEFAULT_TIMEZONE,
IntlDateFormatter::GREGORIAN,
'EEEE d MMMM'
);
@@ -205,7 +225,7 @@ $liveStream = getLiveStream();
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::SHORT,
'Indian/Reunion'
DEFAULT_TIMEZONE
);
echo $formatter->format($liveDate);
+82 -141
View File
@@ -3,187 +3,128 @@
/**
* Configuration par défaut de kaubuntu.re
*
* Ce fichier contient les paramètres de configuration par défaut.
* Il est utilisé pour initialiser les variables non définies dans config.local.php.
* Ce fichier contient les valeurs de repli pour toutes les constantes.
* Surchargez-les dans config.local.php.
*/
if (!defined('APP_HOST_NAME')) define('APP_HOST_NAME', 'kaubuntu.re');
// Configuration de base - ces valeurs seront utilisées si elles ne sont pas définies dans config.local.php
if (!defined('PEERTUBE_URL')) define('PEERTUBE_URL', 'https://vizyon.kaubuntu.re');
if (!defined('PEERTUBE_DISPLAY_NAME')) define('PEERTUBE_DISPLAY_NAME', 'vizyon.kaubuntu.re');
if (!defined('API_KEY')) define('API_KEY', '');
if (!defined('TAG_INDEPENDENCE')) define('TAG_INDEPENDENCE', 'indépendance');
if (!defined('SHORTS_MAX_DURATION')) define('SHORTS_MAX_DURATION', 120); // 2 minutes max pour les shorts
// Pagination et affichage
if (!defined('COUNT_VIDEO_SEARCH')) define('COUNT_VIDEO_SEARCH', 20);
if (!defined('VIDEOS_PER_PAGE')) define('VIDEOS_PER_PAGE', 12);
if (!defined('FEATURED_VIDEOS_COUNT')) define('FEATURED_VIDEOS_COUNT', 6);
if (!defined('RECENT_VIDEOS_COUNT')) define('RECENT_VIDEOS_COUNT', 6);
if (!defined('SHORTS_COUNT')) define('SHORTS_COUNT', 6);
if (!defined('SHORTS_COUNT_SEARCH')) define('SHORTS_COUNT_SEARCH', 100);
if (!defined('TRENDING_VIDEOS_COUNT')) define('TRENDING_VIDEOS_COUNT', 6);
if (!defined('INDEPENDENCE_VIDEOS_COUNT')) define('INDEPENDENCE_VIDEOS_COUNT', 6);
if (!defined('CATEGORY_VIDEOS_COUNT')) define('CATEGORY_VIDEOS_COUNT', 6);
if (!defined('LOAD_MORE_COUNT')) define('LOAD_MORE_COUNT', 6);
// Catégories prioritaires avec noms personnalisés (dans l'ordre d'affichage souhaité)
// format: [ID catégorie => Nom personnalisé]
if (!defined('PRIORITY_CATEGORIES')) {
define('PRIORITY_CATEGORIES', [
11 => 'Actualité & Politique', // News & Politique
14 => 'Activisme', // Activism
1 => 'Musique', // Musique
]);
}
// PeerTube désactivé — court-circuite les appels API dans config.php
if (!defined('PEERTUBE_ENABLED')) define('PEERTUBE_ENABLED', false);
// =========================================
// Configuration Mastodon
// =========================================
if (!defined('MASTODON_INSTANCE_URL')) define('MASTODON_INSTANCE_URL', 'https://koze.kaubuntu.re');
if (!defined('MASTODON_DATE_FORMAT')) define('MASTODON_DATE_FORMAT', 'fr-FR');
if (!defined('MASTODON_BTN_SEE_MORE')) define('MASTODON_BTN_SEE_MORE', 'Voir plus de post');
if (!defined('MASTODON_BTN_RELOAD')) define('MASTODON_BTN_RELOAD', 'Rafraichir');
if (!defined('MASTODON_MAX_POST_FETCH')) define('MASTODON_MAX_POST_FETCH', '10');
if (!defined('MASTODON_MAX_POST_SHOW')) define('MASTODON_MAX_POST_SHOW', '10');
// URL du stockage S3 pour les médias Mastodon (laissez vide pour désactiver)
// Format: https://votre-bucket.s3.region.provider.com
if (!defined('MASTODON_S3_MEDIA_URL')) define('MASTODON_S3_MEDIA_URL', 'https://s3.eu-central-003.backblazeb2.com');
// Informations du site
if (!defined('SITE_NAME')) define('SITE_NAME', 'kaubuntu.re');
if (!defined('SITE_DESCRIPTION')) define('SITE_DESCRIPTION', 'Votre plateforme de médias libres');
if (!defined('SITE_LOGO')) define('SITE_LOGO', 'img/logo.png');
if (!defined('SITE_FAVICON')) define('SITE_FAVICON', 'img/favicon.png');
// =========================================
// Réseaux sociaux
if (!defined('FACEBOOK_URL')) define('FACEBOOK_URL', '#');
if (!defined('X_URL')) define('X_URL', '#');
if (!defined('INSTAGRAM_URL')) define('INSTAGRAM_URL', '#');
if (!defined('YOUTUBE_URL')) define('YOUTUBE_URL', '#');
if (!defined('TIKTOK_URL')) define('TIKTOK_URL', '#');
if (!defined('MASTODON_URL')) define('MASTODON_URL', 'https://koze.kaubuntu.re/@admin');
if (!defined('SITE_NAME')) define('SITE_NAME', 'kaubuntu.re');
if (!defined('SITE_DESCRIPTION')) define('SITE_DESCRIPTION', 'Hub multimédia du mouvement Ka-Ubuntu');
if (!defined('SITE_LOGO')) define('SITE_LOGO', 'img/logo.png');
if (!defined('SITE_FAVICON')) define('SITE_FAVICON', 'img/favicon.png');
// Contacts
if (!defined('CONTACT_EMAIL')) define('CONTACT_EMAIL', 'multimedia@kaubuntu.re');
if (!defined('DEFAULT_TIMEZONE')) define('DEFAULT_TIMEZONE', 'Indian/Reunion');
// Mentions légales
if (!defined('LEGAL_COPYRIGHT')) define('LEGAL_COPYRIGHT', 'Ka-Ubuntu');
if (!defined('LEGAL_WEBMASTER_NAME')) define('LEGAL_WEBMASTER_NAME', 'Cédric Famibelle-Pronzola');
if (!defined('LEGAL_WEBMASTER_EMAIL')) define('LEGAL_WEBMASTER_EMAIL', 'contact@cedric-pronzola.dev');
if (!defined('LEGAL_HOST_NAME')) define('LEGAL_HOST_NAME', 'o2Switch');
if (!defined('LEGAL_HOST_COMPANY')) define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €');
if (!defined('LEGAL_HOST_RCS')) define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807');
if (!defined('LEGAL_HOST_ADDRESS')) define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France');
if (!defined('LEGAL_CONTACT_EMAIL')) define('LEGAL_CONTACT_EMAIL', 'zinfos@kaubuntu.com');
if (!defined('LEGAL_LICENSE')) define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)');
if (!defined('LEGAL_LICENSE_URL')) define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html');
if (!defined('LEGAL_SOURCE_CODE_URL')) define('LEGAL_SOURCE_CODE_URL', 'https://codeberg.org/Ka-Ubuntu/kaubuntu.re');
// =========================================
// Réseaux sociaux — URLs
// =========================================
if (!defined('FACEBOOK_URL')) define('FACEBOOK_URL', 'https://www.facebook.com/zinfos.ubuntu');
if (!defined('X_URL')) define('X_URL', 'https://x.com/ka_ubuntu');
if (!defined('INSTAGRAM_URL')) define('INSTAGRAM_URL', 'https://www.instagram.com/ka_ubuntu/');
if (!defined('YOUTUBE_URL')) define('YOUTUBE_URL', 'https://www.youtube.com/@kaubuntu4546');
if (!defined('TIKTOK_URL')) define('TIKTOK_URL', 'https://www.tiktok.com/@kaubuntu');
// =========================================
// Réseaux sociaux — Handles (sans @)
// =========================================
if (!defined('YOUTUBE_HANDLE')) define('YOUTUBE_HANDLE', 'kaubuntu4546');
if (!defined('FACEBOOK_PAGE')) define('FACEBOOK_PAGE', 'zinfos.ubuntu');
if (!defined('INSTAGRAM_HANDLE')) define('INSTAGRAM_HANDLE', 'ka_ubuntu');
if (!defined('TIKTOK_HANDLE')) define('TIKTOK_HANDLE', 'kaubuntu');
if (!defined('X_HANDLE')) define('X_HANDLE', 'ka_ubuntu');
// =========================================
// YouTube Data API v3
// =========================================
if (!defined('YOUTUBE_API_KEY')) define('YOUTUBE_API_KEY', '');
if (!defined('YOUTUBE_CHANNEL_HANDLE')) define('YOUTUBE_CHANNEL_HANDLE', 'kaubuntu4546');
if (!defined('YOUTUBE_CHANNEL_ID')) define('YOUTUBE_CHANNEL_ID', '');
if (!defined('YOUTUBE_VIDEOS_COUNT')) define('YOUTUBE_VIDEOS_COUNT', 9);
// =========================================
// Contenu embarqué optionnel
// =========================================
if (!defined('INSTAGRAM_POST_URLS')) define('INSTAGRAM_POST_URLS', []);
if (!defined('TIKTOK_VIDEO_URLS')) define('TIKTOK_VIDEO_URLS', []);
// =========================================
// Contact & mentions légales
// =========================================
if (!defined('CONTACT_EMAIL')) define('CONTACT_EMAIL', 'zinfoskaubuntu@gmail.com');
if (!defined('LEGAL_COPYRIGHT')) define('LEGAL_COPYRIGHT', 'Ka-Ubuntu');
if (!defined('LEGAL_WEBMASTER_NAME')) define('LEGAL_WEBMASTER_NAME', 'Cédric Famibelle-Pronzola');
if (!defined('LEGAL_WEBMASTER_EMAIL')) define('LEGAL_WEBMASTER_EMAIL', 'contact@cedric-pronzola.dev');
if (!defined('LEGAL_HOST_NAME')) define('LEGAL_HOST_NAME', 'o2Switch');
if (!defined('LEGAL_HOST_COMPANY')) define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €');
if (!defined('LEGAL_HOST_RCS')) define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807');
if (!defined('LEGAL_HOST_ADDRESS')) define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France');
if (!defined('LEGAL_CONTACT_EMAIL')) define('LEGAL_CONTACT_EMAIL', 'zinfos@kaubuntu.com');
if (!defined('LEGAL_LICENSE')) define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)');
if (!defined('LEGAL_LICENSE_URL')) define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html');
if (!defined('LEGAL_SOURCE_CODE_URL')) define('LEGAL_SOURCE_CODE_URL', 'https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/kaubuntu.re');
if (!defined('LEGAL_SERVICE_DESCRIPTION')) define('LEGAL_SERVICE_DESCRIPTION', 'est une plateforme multimédia proposant des contenus vidéo, des actualités et des informations liées au mouvement politique panafricaniste et indépendantiste réunionnais Ka-Ubuntu.');
// Fonctionnalités
define('ENABLE_SEARCH', true);
if (!defined('ENABLE_USER_ACCOUNTS')) define('ENABLE_USER_ACCOUNTS', false);
// =========================================
// Cache
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', false);
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure)
// =========================================
// Compte pour les lives
if (!defined('LIVE_ACCOUNT_NAME')) define('LIVE_ACCOUNT_NAME', 'admin');
// Tags pour filtrer les vidéos selon les catégories
if (!defined('TAG_SHORT')) define('TAG_SHORT', 'short');
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
if (!defined('IMPORTANT_TAGS')) {
define('IMPORTANT_TAGS', [
'Colonialisme',
'La Réunion',
'Panafricanisme',
'Conférence'
]);
}
// Hashtags populaires à afficher sur la page d'accueil
if (!defined('POPULAR_TAGS')) {
define('POPULAR_TAGS', [
'Justice',
'Anticolonial',
'Kanaky',
'Océan Indien'
]);
}
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', false);
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600);
// =========================================
// Système de compte à rebours / maintenance
// =========================================
// Activer le mode compte à rebours (true/false) - valeur par défaut
if (!defined('COUNTDOWN_ENABLED')) define('COUNTDOWN_ENABLED', false);
// Date de fin du compte à rebours par défaut (format: Y-m-d H:i:s)
if (!defined('COUNTDOWN_ENABLED')) define('COUNTDOWN_ENABLED', false);
if (!defined('COUNTDOWN_TARGET_DATE')) define('COUNTDOWN_TARGET_DATE', '2025-10-11 00:00:00');
// Territoires et fuseaux horaires par défaut pour la page de compte à rebours
if (!defined('COUNTDOWN_TIMEZONES')) {
define('COUNTDOWN_TIMEZONES', [
'Martinique / Guadeloupe' => 'America/Martinique',
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
'Ma\'ohi Nui' => 'Pacific/Tahiti',
'Kanaky' => 'Pacific/Noumea'
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
"Ma'ohi Nui" => 'Pacific/Tahiti',
'Kanaky' => 'Pacific/Noumea',
]);
}
// =========================================
// Intégration WordPress par défaut
// Intégration WordPress (optionnel)
// =========================================
// URL du site WordPress par défaut
if (!defined('WORDPRESS_URL')) define('WORDPRESS_URL', '');
// Nombre d'articles WordPress à afficher par défaut
if (!defined('WORDPRESS_POSTS_COUNT')) define('WORDPRESS_POSTS_COUNT', 6);
// Activation des articles WordPress par défaut
if (!defined('WORDPRESS_ENABLED')) define('WORDPRESS_ENABLED', false);
if (!defined('WORDPRESS_URL')) define('WORDPRESS_URL', '');
if (!defined('WORDPRESS_POSTS_COUNT')) define('WORDPRESS_POSTS_COUNT', 6);
if (!defined('WORDPRESS_ENABLED')) define('WORDPRESS_ENABLED', false);
// =========================================
// Système de dons par défaut
// Système de dons
// =========================================
// Activation du système de dons par défaut
if (!defined('DONATIONS_ENABLED')) define('DONATIONS_ENABLED', false);
// URL PayPal Me par défaut (sans le montant)
if (!defined('PAYPAL_ME_URL')) define('PAYPAL_ME_URL', '');
// Montants de dons suggérés par défaut
if (!defined('DONATION_AMOUNTS')) define('DONATION_AMOUNTS', [5, 10, 20, 50]);
// Devise par défaut
if (!defined('PAYPAL_ME_URL')) define('PAYPAL_ME_URL', '');
if (!defined('DONATION_AMOUNTS')) define('DONATION_AMOUNTS', [5, 10, 20, 50]);
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_ENABLED')) define('NEXT_LIVE_ENABLED', false);
if (!defined('NEXT_LIVE_TITLE')) define('NEXT_LIVE_TITLE', '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');
if (!defined('NEXT_LIVE_DATE')) define('NEXT_LIVE_DATE', '');
if (!defined('NEXT_LIVE_IMAGE')) define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg');
?>
+93 -243
View File
@@ -1,242 +1,122 @@
<?php
/**
* Configuration locale pour l'instance de PeerTube
* Configuration locale — kaubuntu.re
*
* Ce fichier est un exemple de configuration locale.
* Pour l'utiliser:
* 1. Copiez ce fichier vers config.local.php
* Ce fichier est un exemple. Pour l'utiliser :
* 1. Copiez-le vers config.local.php
* 2. Décommentez et modifiez les valeurs selon vos besoins
*
* Note: config.local.php ne doit pas être versionné dans git
* Note : config.local.php ne doit pas être versionné dans git.
*/
// define('APP_HOST_NAME', 'kaubuntu.re');
// =========================================
// Configuration de l'API PeerTube
// Réseaux sociaux — URLs et handles
// =========================================
// URL de l'API PeerTube (obligatoire)
// define('PEERTUBE_URL', 'https://votre-instance.fr');
// define('PEERTUBE_DISPLAY_NAME', 'votre-instance.fr');
// URLs complètes des pages/comptes (obligatoires pour les liens et widgets)
// define('YOUTUBE_URL', 'https://www.youtube.com/@votrechannel');
// define('FACEBOOK_URL', 'https://www.facebook.com/votrepage');
// define('INSTAGRAM_URL', 'https://www.instagram.com/votrecompte/');
// define('TIKTOK_URL', 'https://www.tiktok.com/@votrecompte');
// define('X_URL', 'https://x.com/votrecompte');
// Clé d'API PeerTube (optionnelle)
// define('API_KEY', 'votre_cle_api');
// Compte PeerTube pour les lives
// define('LIVE_ACCOUNT_NAME', 'admin');
// Handles (sans @) — utilisés pour les widgets et embeds
// define('YOUTUBE_HANDLE', 'votrechannel');
// define('FACEBOOK_PAGE', 'votrepage'); // ex: zinfos.ubuntu
// define('INSTAGRAM_HANDLE', 'votrecompte');
// define('TIKTOK_HANDLE', 'votrecompte');
// define('X_HANDLE', 'votrecompte');
// =========================================
// Filtres et tags
// YouTube — affichage des vidéos
// =========================================
// Option A — clé API Google Cloud (qualité maximale) :
// 1. https://console.cloud.google.com/ → activer "YouTube Data API v3"
// 2. Créer une clé API (APIs & Services > Credentials)
// define('YOUTUBE_API_KEY', 'AIza...');
// Tag pour les vidéos sur l'indépendance
// define('TAG_INDEPENDENCE', 'indépendance');
// Option B — flux RSS public, SANS clé API (recommandé pour démarrer) :
// Renseigner l'ID de chaîne (YouTube Studio > Paramètres > Informations sur la chaîne)
// Format : UC suivi de 22 caractères
// define('YOUTUBE_CHANNEL_ID', 'UC...');
// Tag pour les shorts
// define('TAG_SHORT', 'short');
// Si les deux sont définis, l'API a la priorité sur le RSS.
// Sans aucun des deux, une carte de profil YouTube est affichée.
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
// define('IMPORTANT_TAGS', [
// 'Colonialisme',
// 'La Réunion',
// 'Panafricanisme',
// 'Conférence',
// 'Indépendance',
// 'Histoire'
// Nombre de vidéos à afficher (défaut : 6)
// define('YOUTUBE_VIDEOS_COUNT', 6);
// =========================================
// Contenu embarqué optionnel
// =========================================
// Sans ces URLs, des cartes de profil statiques sont affichées à la place.
// Posts Instagram spécifiques à embarquer (URLs complètes)
// define('INSTAGRAM_POST_URLS', [
// 'https://www.instagram.com/p/ABC123/',
// 'https://www.instagram.com/p/DEF456/',
// ]);
// Hashtags populaires à afficher sur la page d'accueil
// define('POPULAR_TAGS', [
// 'Justice',
// 'Anticolonial',
// 'Kanaky',
// 'Océan Indien'
// Vidéos TikTok spécifiques à embarquer (URLs complètes)
// define('TIKTOK_VIDEO_URLS', [
// 'https://www.tiktok.com/@votrecompte/video/123456789',
// ]);
// Durée maximale des shorts en secondes
// define('SHORTS_MAX_DURATION', 120); // 2 minutes
// =========================================
// Pagination et nombre de vidéos à afficher
// =========================================
// Nombre de vidéos par page
// define('VIDEOS_PER_PAGE', 12);
// Limite de vidéo à chercher
// define('COUNT_VIDEO_SEARCH', 6);
// Nombre de vidéos à la une
// define('FEATURED_VIDEOS_COUNT', 6);
// Nombre de vidéos récentes
// define('RECENT_VIDEOS_COUNT', 6);
// Nombre de shorts
// define('SHORTS_COUNT', 6);
// define('SHORTS_COUNT_SEARCH', 50)
// Nombre de vidéos tendances
// define('TRENDING_VIDEOS_COUNT', 6);
// Nombre de vidéos indépendance
// define('INDEPENDENCE_VIDEOS_COUNT', 6);
// Nombre de vidéos par catégorie
// define('CATEGORY_VIDEOS_COUNT', 6);
// Nombre de vidéos chargées avec "Voir plus"
// define('LOAD_MORE_COUNT', 6);
// =========================================
// Catégories à afficher
// =========================================
// Catégories prioritaires avec noms personnalisés (dans l'ordre d'affichage souhaité)
// Liste des catégories : (disponible ici => https://mon_instance_peertube.fr/api/v1/videos/categories)
// 1 : Music
// 2 : Films
// 3 : Vehicles
// 4 : Art
// 5 : Sports
// 6 : Travels
// 7 : Gaming
// 8 : People
// 9 : Comedy
// 10 : Entertainment
// 11 : News & Politics
// 12 : How To
// 13 : Education
// 14 : Activism
// 15 : Science & Technology
// 16 : Animals
// 17 : Kids
// 18 : Food
define('PRIORITY_CATEGORIES', [
11 => 'Actualités & Politique',
14 => 'Activisme',
1 => 'Musique',
// Ajoutez d'autres catégories selon vos besoins
]);
// =========================================
// Informations du site
// =========================================
// Nom du site
// define('SITE_NAME', 'kaubuntu.re');
// define('SITE_NAME', 'kaubuntu.re');
// define('SITE_DESCRIPTION', 'Hub multimédia du mouvement Ka-Ubuntu');
// define('SITE_LOGO', 'img/logo.png');
// define('SITE_FAVICON', 'img/favicon.png');
// Description du site
// define('SITE_DESCRIPTION', 'Votre plateforme de médias libres');
// Logo du site
// define('SITE_LOGO', 'img/logo.png');
// Favicon du site
// define('SITE_FAVICON', 'img/favicon.png');
// =========================================
// Réseaux sociaux
// =========================================
// URL de la page Facebook
// define('FACEBOOK_URL', 'https://facebook.com/votrepage');
// URL du compte X (anciennement Twitter)
// define('X_URL', 'https://x.com/votrecompte');
// URL du compte Instagram
// define('INSTAGRAM_URL', 'https://instagram.com/votrecompte');
// URL de la chaîne YouTube
// define('YOUTUBE_URL', 'https://youtube.com/votrechaine');
// URL du compte TikTok
// define('TIKTOK_URL', 'https://tiktok.com/@votrecompte');
// URL du compte Mastodon
// define('MASTODON_URL', 'https://koze.kaubuntu.re/@admin');
// Fuseau horaire (liste : https://www.php.net/manual/fr/timezones.php)
// define('DEFAULT_TIMEZONE', 'Indian/Reunion');
// =========================================
// Contact
// =========================================
// Email de contact
// define('CONTACT_EMAIL', 'contact@votredomaine.com');
// =========================================
// Fonctionnalités
// =========================================
// Activer/désactiver les commentaires
// define('ENABLE_COMMENTS', true);
// Activer/désactiver la recherche
// define('ENABLE_SEARCH', true);
// Activer/désactiver les comptes utilisateurs
// define('ENABLE_USER_ACCOUNTS', false);
// =========================================
// Cache
// =========================================
// Activer/désactiver le cache
// define('CACHE_ENABLED', false);
// Durée du cache en secondes
// define('CACHE_DURATION', 3600); // 1 heure
// =========================================
// Configuration Mastodon
// =========================================
// URL de l'instance Mastodon
// define('MASTODON_INSTANCE_URL', 'https://mastodon.social');
// Format de date pour l'affichage des posts
// define('MASTODON_DATE_FORMAT', 'fr-FR');
// Texte du bouton "Voir plus"
// define('MASTODON_BTN_SEE_MORE', 'Voir plus de post');
// Texte du bouton "Rafraichir"
// define('MASTODON_BTN_RELOAD', 'Rafraichir');
// Nombre maximum de posts à récupérer
// define('MASTODON_MAX_POST_FETCH', '10');
// Nombre maximum de posts à afficher
// define('MASTODON_MAX_POST_SHOW', '10');
// URL du stockage S3 pour les médias Mastodon (optionnel)
// Format: https://votre-bucket.s3.region.provider.com
// Laissez vide ou commentez pour désactiver
// define('MASTODON_S3_MEDIA_URL', 'https://s3.eu-central-003.backblazeb2.com');
// =========================================
// Contact
// =========================================
// define('CONTACT_EMAIL', 'multimedia@kaubuntu.re');
// define('CONTACT_EMAIL', 'contact@votredomaine.re');
// =========================================
// Mentions légales
// =========================================
// define('LEGAL_COPYRIGHT', 'Ka-Ubuntu');
// define('LEGAL_WEBMASTER_NAME', 'Cédric Famibelle-Pronzola');
// define('LEGAL_WEBMASTER_EMAIL', 'contact@cedric-pronzola.dev');
// define('LEGAL_HOST_NAME', 'o2Switch');
// define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €');
// define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807');
// define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France');
// define('LEGAL_CONTACT_EMAIL', 'multimedia@kaubuntu.re');
// define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)');
// define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html');
// define('LEGAL_SOURCE_CODE_URL', 'https://codeberg.org/Ka-Ubuntu/kaubuntu.re');
// define('LEGAL_SERVICE_DESCRIPTION', 'est une plateforme multimédia proposant des contenus vidéo, des actualités et des informations liées au mouvement politique indépendantiste et panafricaniste réunionnais Ka-Ubuntu.');
// define('LEGAL_COPYRIGHT', 'Ka-Ubuntu');
// define('LEGAL_WEBMASTER_NAME', 'Prénom Nom');
// define('LEGAL_WEBMASTER_EMAIL', 'contact@votredomaine.re');
// define('LEGAL_HOST_NAME', 'o2Switch');
// define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €');
// define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807');
// define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France');
// define('LEGAL_CONTACT_EMAIL', 'contact@votredomaine.re');
// define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)');
// define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html');
// define('LEGAL_SOURCE_CODE_URL', 'https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/kaubuntu.re');
// define('LEGAL_SERVICE_DESCRIPTION', 'est une plateforme multimédia proposant des contenus vidéo, des actualités et des informations liées au mouvement politique panafricaniste et indépendantiste réunionnais Ka-Ubuntu.');
// =========================================
// Système de dons (PayPal Me)
// =========================================
// define('DONATIONS_ENABLED', true);
// define('PAYPAL_ME_URL', 'https://www.paypal.com/paypalme/votrecompte');
// define('DONATION_AMOUNTS', [5, 10, 20, 50]);
// define('DONATION_CURRENCY', 'EUR');
// =========================================
// Annonce du prochain live
// =========================================
// define('NEXT_LIVE_ENABLED', true);
// define('NEXT_LIVE_TITLE', 'Prochain live');
// define('NEXT_LIVE_DESCRIPTION', 'Rejoignez-nous pour notre prochain live !');
// define('NEXT_LIVE_DATE', '2025-12-01 20:00:00'); // format: Y-m-d H:i:s
// define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg');
// =========================================
// Système de compte à rebours / maintenance
@@ -248,56 +128,26 @@ define('COUNTDOWN_ENABLED', false);
// Date de fin du compte à rebours (format: Y-m-d H:i:s)
define('COUNTDOWN_TARGET_DATE', '2025-10-11 00:00:00');
// Territoires et fuseaux horaires à afficher sur la page de compte à rebours
// Territoires et fuseaux horaires affichés sur la page de compte à rebours
define('COUNTDOWN_TIMEZONES', [
'Martinique / Guadeloupe' => 'America/Martinique',
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
'Ma\'ohi Nui' => 'Pacific/Tahiti',
'Kanaky' => 'Pacific/Noumea'
'Guyane' => 'America/Cayenne',
'France' => 'Europe/Paris',
"Ma'ohi Nui" => 'Pacific/Tahiti',
'Kanaky' => 'Pacific/Noumea',
]);
// =========================================
// Intégration WordPress
// Intégration WordPress (optionnel)
// =========================================
// URL du site WordPress pour récupérer les articles (sans trailing slash)
// define('WORDPRESS_URL', 'https://votre-site-wordpress.com');
// Nombre d'articles WordPress à afficher
// define('WORDPRESS_POSTS_COUNT', 6);
// Activer/désactiver l'affichage des articles WordPress
// define('WORDPRESS_ENABLED', true);
// define('WORDPRESS_ENABLED', true);
// define('WORDPRESS_URL', 'https://votre-site-wordpress.com'); // sans trailing slash
// define('WORDPRESS_POSTS_COUNT', 6);
// =========================================
// Système de dons
// Cache
// =========================================
// Activer/désactiver le système de dons
// define('DONATIONS_ENABLED', true);
// URL PayPal Me (exemple: https://www.paypal.com/paypalme/kubuntu)
// define('PAYPAL_ME_URL', 'https://www.paypal.com/paypalme/votre-compte');
// Montants de dons suggérés (en euros par défaut)
// define('DONATION_AMOUNTS', [5, 10, 20, 50, 100]);
// Devise pour les dons
// define('DONATION_CURRENCY', 'EUR');
// =========================================
// Texte de présentation du mouvement
// =========================================
// Pour désactiver le bloc de présentation, commentez cette ligne:
// define('MOVEMENT_DESCRIPTION', 'KA UBUNTU est un mouvement politique panafricaniste et indépendantiste réunionnais qui a 5 objectifs :');
// Image du mouvement à afficher dans la section de présentation
// define('MOVEMENT_IMAGE', 'img/movement_presentation.png');
// Texte alternatif pour l'image du mouvement (accessibilité)
// define('MOVEMENT_IMAGE_ALT', 'Les 5 objectifs de Ka-Ubuntu');
// Légende de l'image (peut contenir du HTML simple comme <br>)
// define('MOVEMENT_CAPTION', 'Nos 5 points de lutte.<br>1. Activer la conscience politique et historique de notre peuple<br>2. Défendre le droit à I\'autodétermination des peuples africains et afro-descendants<br>3. Arracher I\'indépendance de notre pays La Réunion<br>4. Établir une unité politique, économique et culturelle de l\'Afrique<br>5. Construire une solidarité entre les peuples opprimés');
// define('CACHE_ENABLED', true);
// define('CACHE_DURATION', 3600); // en secondes (1 heure)
+69 -4
View File
@@ -23,11 +23,15 @@ if (file_exists($config_default_file)) {
// Locale et fuseau horaire
setlocale(LC_TIME, 'fr_FR.UTF-8');
date_default_timezone_set('Indian/Reunion');
date_default_timezone_set(DEFAULT_TIMEZONE);
// Initialisation des catégories de vidéo depuis l'API
$peertube_categories = initCategories();
define('PEERTUBE_CATEGORIES', $peertube_categories);
// Initialisation des catégories PeerTube (seulement si PeerTube est activé)
if (defined('PEERTUBE_ENABLED') && PEERTUBE_ENABLED) {
$peertube_categories = initCategories();
define('PEERTUBE_CATEGORIES', $peertube_categories);
} else {
define('PEERTUBE_CATEGORIES', []);
}
/**
* Initialise et récupère les catégories depuis l'API PeerTube
@@ -397,6 +401,67 @@ function getLiveStream() {
return !empty($activeLives) ? reset($activeLives) : null;
}
/**
* Récupère une vidéo spécifique par son ID
*
* @param string $videoId ID de la vidéo
* @return array|null Informations sur la vidéo ou null si non trouvée
*/
function getVideoById($videoId) {
if (empty($videoId)) {
return null;
}
$data = callPeerTubeApi('videos/' . $videoId);
// Vérifier si on a des résultats
if (empty($data)) {
return null;
}
// Formater les données de la vidéo
$videoData = formatVideosData([$data]);
// Retourner la première vidéo formatée
return !empty($videoData) ? reset($videoData) : null;
}
/**
* Récupère la vidéo à afficher selon le mode configuré (auto ou static)
*
* En mode 'auto' : détecte automatiquement un direct en cours
* En mode 'static' : affiche une vidéo spécifique définie par STATIC_VIDEO_ID
*
* @return array|null Tableau avec 'video' (données de la vidéo ou null), 'mode' ('auto'|'static'), 'isLive' (bool), 'badge' (texte du badge)
*/
function getDisplayVideo() {
$mode = defined('LIVE_MODE') ? LIVE_MODE : 'auto';
if ($mode === 'static') {
// Mode vidéo statique
$videoId = defined('STATIC_VIDEO_ID') ? STATIC_VIDEO_ID : '';
$video = getVideoById($videoId);
$badge = defined('STATIC_VIDEO_BADGE') ? STATIC_VIDEO_BADGE : 'À LA UNE';
return [
'video' => $video,
'mode' => 'static',
'isLive' => false,
'badge' => $badge
];
} else {
// Mode automatique (détection de direct)
$video = getLiveStream();
return [
'video' => $video,
'mode' => 'auto',
'isLive' => $video !== null,
'badge' => 'DIRECT'
];
}
}
/**
* Formate les données brutes des vidéos venant de l'API
*
+48 -56
View File
@@ -1,80 +1,72 @@
<!-- Footer -->
<?php
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<div class="footer">
<div class="footer-header">
<div class="footer-logo">
<img src="img/logo.png" alt="kaubuntu.re">
<img src="img/logo.png" alt="<?php echo htmlspecialchars(SITE_NAME); ?>">
</div>
<div class="footer-contact-info">
<div class="footer-contact">CONTACT</div>
<div class="footer-email"><a href="mailto:<?php echo CONTACT_EMAIL; ?>"><?php echo CONTACT_EMAIL; ?></a></div>
<div class="footer-email">
<a href="mailto:<?php echo htmlspecialchars(CONTACT_EMAIL); ?>"><?php echo htmlspecialchars(CONTACT_EMAIL); ?></a>
</div>
</div>
</div>
<div class="footer-columns">
<div class="footer-column">
<h3 class="footer-title">Catégories</h3>
<div>
<ul class="footer-links">
<li><a href="index.php" <?php echo ($currentPage === 'index.php') ? 'class="active"' : ''; ?>>Accueil</a></li>
<li><a href="direct.php" <?php echo ($currentPage === 'direct.php') ? 'class="active"' : ''; ?>>Direct</a></li>
<?php
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
echo '<li><a href="categories.php?id=' . $id . '"' . ($isActive ? ' class="active"' : '') . '>' . htmlspecialchars($name) . '</a></li>';
}
}
?>
</ul>
</div>
</div>
<div class="footer-column">
<h3 class="footer-title">Hashtags</h3>
<div>
<ul class="footer-links">
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<li><a href="recherche.php?q=<?php echo $encodedTag; ?>" <?php echo $isActive ? 'class="active"' : ''; ?>><?php echo htmlspecialchars($tag); ?></a></li>
<?php
endforeach;
endif;
?>
</ul>
</div>
<h3 class="footer-title">Réseaux sociaux</h3>
<ul class="footer-links">
<li>
<a href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> TikTok
</a>
</li>
</ul>
</div>
<div class="footer-column">
<h3 class="footer-title">Informations légales</h3>
<div>
<ul class="footer-links">
<li><a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>Mentions légales</a></li>
<li>
<a href="<?php echo LEGAL_SOURCE_CODE_URL; ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-git-alt"></i> Code source
</a>
</li>
</ul>
</div>
<ul class="footer-links">
<li>
<a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>
Mentions légales
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(LEGAL_SOURCE_CODE_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-git-alt" aria-hidden="true"></i> Code source
</a>
</li>
</ul>
</div>
</div>
<div class="footer-social">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
</div>
<div class="footer-copyright">
<?php echo LEGAL_COPYRIGHT; ?> <?php echo date('Y'); ?> - Licence libre <a href="<?php echo LEGAL_LICENSE_URL; ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a>
<?php echo htmlspecialchars(LEGAL_COPYRIGHT); ?> <?php echo date('Y'); ?> &mdash;
Licence libre <a href="<?php echo htmlspecialchars(LEGAL_LICENSE_URL); ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a>
</div>
</div>
+15 -32
View File
@@ -1,47 +1,30 @@
<!-- Header avec barre de recherche et icônes -->
<!-- Header -->
<header class="header" role="banner">
<div class="search-container">
<form action="recherche.php" method="get" role="search" aria-label="Recherche de vidéos">
<label for="search-input" class="sr-only">Rechercher des vidéos</label>
<input type="text" id="search-input" name="q" placeholder="Rechercher..." aria-describedby="search-help">
<button type="submit" aria-label="Lancer la recherche">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
<div id="search-help" class="sr-only">Tapez vos mots-clés pour rechercher des vidéos</div>
</form>
<div class="header-brand">
<a href="/" class="header-logo-link" aria-label="Accueil <?php echo htmlspecialchars(SITE_NAME); ?>">
<img src="img/logo.png" alt="<?php echo htmlspecialchars(SITE_NAME); ?>" class="header-logo-img">
<span class="header-site-name"><?php echo htmlspecialchars(SITE_NAME); ?></span>
</a>
</div>
<nav class="social-icons" aria-label="Réseaux sociaux">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>" class="icon-button" aria-label="Suivre sur Mastodon">
<i class="fab fa-mastodon icon-mastodon" aria-hidden="true"></i>
<nav class="social-icons" aria-label="Nos réseaux sociaux">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>"
class="icon-button" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>" class="icon-button" aria-label="Suivre sur Instagram">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>"
class="icon-button" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>" class="icon-button" aria-label="Suivre sur TikTok">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>"
class="icon-button" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
<div class="more-social-container">
<button type="button" class="icon-button more-social-toggle" aria-expanded="false" aria-controls="social-dropdown" aria-label="Voir plus de réseaux sociaux">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button>
<div id="social-dropdown" class="more-social-dropdown" role="menu">
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-facebook icon-facebook" aria-hidden="true"></i> Facebook
</a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-x-twitter icon-x" aria-hidden="true"></i> X
</a>
</div>
</div>
</nav>
<div class="action-icons">
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<a href="dons.php" class="icon-button donation-link" aria-label="Soutenir KA UBUNTU" title="Faire un don">
<a href="dons.php" class="icon-button donation-link" aria-label="Soutenir <?php echo htmlspecialchars(SITE_NAME); ?>" title="Faire un don">
<i class="fas fa-heart" aria-hidden="true"></i>
</a>
<?php endif; ?>
+46 -76
View File
@@ -1,92 +1,62 @@
<!-- Menu mobile (masqué par défaut) -->
<div class="mobile-menu">
<button class="mobile-menu-close">
<i class="fas fa-times"></i>
<?php
$currentPage = basename($_SERVER['PHP_SELF']);
$isHome = ($currentPage === 'index.php' || $currentPage === '/');
?>
<!-- Menu mobile -->
<div class="mobile-menu" id="mobile-menu" role="dialog" aria-label="Menu de navigation" aria-modal="true">
<button class="mobile-menu-close" aria-label="Fermer le menu">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
<div class="search-container">
<form action="recherche.php" method="get">
<input type="text" name="q" placeholder="Rechercher...">
<button type="submit"><i class="fas fa-search"></i></button>
</form>
</div>
<div>
<a href="index.php" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>">
<i class="fas fa-home"></i> Accueil
</a>
<a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>">
<i class="fas fa-broadcast-tower"></i> Direct
<nav>
<a href="/"
class="nav-item <?php echo $isHome ? 'active' : ''; ?>">
<i class="fas fa-home" aria-hidden="true"></i> Accueil
</a>
<hr class="nav-divider">
<?php
// Afficher les catégories prioritaires
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
// Tableau associatif des icônes pour les catégories
$categoryIcons = [
1 => 'fas fa-music', // Musique
2 => 'fas fa-film', // Films
3 => 'fas fa-car', // Véhicules
4 => 'fas fa-palette', // Jeux
5 => 'fas fa-running', // Sport
6 => 'fas fa-laugh', // Humour
7 => 'fas fa-gamepad', // Art
8 => 'fas fa-person', // Personnalités
9 => 'fas fa-face-grin-tears', // Comédie
10 => 'fas fa-tv', // Divertissement
11 => 'fas fa-globe', // Actualité & Politique
12 => 'fas fa-chalkboard-user', // Tutoriel
13 => 'fas fa-graduation-cap', // Education
14 => 'fas fa-fist-raised', // Activisme
15 => 'fas fa-microscope', // Science & Technologie
16 => 'fas fa-paw', // Animaux
17 => 'fas fa-child', // Enfants
18 => 'fas fa-utensils' // Cuisine
];
<p class="mobile-section-title">Nos réseaux</p>
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
$icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder';
echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '">';
echo '<i class="' . $icon . '"></i> ' . htmlspecialchars($name);
echo '</a>';
}
}
?>
<hr class="nav-divider">
</div>
<div>
<h3 class="mobile-section-title">Hashtags populaires</h3>
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<a href="recherche.php?q=<?php echo $encodedTag; ?>" class="nav-item <?php echo $isActive ? 'active' : ''; ?>">
<i class="fas fa-hashtag"></i> <?php echo htmlspecialchars($tag); ?>
<a href="<?php echo $isHome ? '#youtube' : 'index.php#youtube'; ?>"
class="nav-item">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
<?php
endforeach;
endif;
?>
</div>
<a href="<?php echo $isHome ? '#instagram' : 'index.php#instagram'; ?>"
class="nav-item">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
</a>
<a href="<?php echo $isHome ? '#tiktok' : 'index.php#tiktok'; ?>"
class="nav-item">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> TikTok
</a>
<a href="<?php echo $isHome ? '#actualites' : 'index.php#actualites'; ?>"
class="nav-item">
<i class="fas fa-newspaper" aria-hidden="true"></i> Actualités
</a>
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<hr class="nav-divider">
<a href="dons.php" class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>">
<i class="fas fa-heart" aria-hidden="true"></i> Soutenir
</a>
<?php endif; ?>
</nav>
<hr class="nav-divider">
<div>
<h3 class="mobile-section-title">Suivez-nous</h3>
<p class="mobile-section-title">Suivez-nous</p>
<div class="mobile-social-icons">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
+52 -99
View File
@@ -181,107 +181,47 @@ function validateCSRFToken($token) {
* Applique des en-têtes de sécurité HTTP
*/
function setSecurityHeaders() {
// Protection contre le clickjacking (permettre les iframes du même site)
header('X-Frame-Options: SAMEORIGIN');
// Protection contre le MIME sniffing
header('X-Content-Type-Options: nosniff');
// Protection XSS basique
header('X-XSS-Protection: 1; mode=block');
// Politique de référent
header('Referrer-Policy: strict-origin-when-cross-origin');
// Content Security Policy avec support Mastodon et PeerTube
$mastodonDomain = '';
$peertubeDomain = '';
// Extraire le domaine Mastodon si configuré
if (defined('MASTODON_INSTANCE_URL')) {
$mastodonParsed = parse_url(MASTODON_INSTANCE_URL);
if ($mastodonParsed && isset($mastodonParsed['host'])) {
$mastodonDomain = $mastodonParsed['scheme'] . '://' . $mastodonParsed['host'];
}
}
// Extraire le domaine PeerTube si configuré
if (defined('PEERTUBE_URL')) {
$peertubeParsed = parse_url(PEERTUBE_URL);
if ($peertubeParsed && isset($peertubeParsed['host'])) {
$peertubeDomain = $peertubeParsed['scheme'] . '://' . $peertubeParsed['host'];
}
}
// Détecter si on est en développement local
$isLocalDev = in_array($_SERVER['HTTP_HOST'] ?? '', ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']);
$csp = "default-src 'self'; ";
$isLocalDev = in_array(
$_SERVER['HTTP_HOST'] ?? '',
['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']
);
// Domaines des 5 plateformes sociales
$fbScripts = 'https://connect.facebook.net';
$fbFrames = 'https://www.facebook.com https://staticxx.facebook.com https://www.facebook.net';
$xScripts = 'https://platform.twitter.com';
$xFrames = 'https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com';
$igScripts = 'https://www.instagram.com';
$igFrames = 'https://www.instagram.com';
$ttScripts = 'https://www.tiktok.com https://lf16-tiktok-web.ttwstatic.com';
$ttFrames = 'https://www.tiktok.com';
$ytImages = 'https://i.ytimg.com https://yt3.ggpht.com';
$csp = "default-src 'self'; ";
$csp .= "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; ";
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io; "; // PLAUSIBLE UPDATED
// Images : autoriser les domaines externes plus HTTPS général en dev
$imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) {
$imgSrc .= " https: http:";
} else {
$imgSrc .= " https:";
}
$csp .= "img-src " . $imgSrc . "; ";
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
// Frames : autoriser PeerTube et HTTPS général
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) {
$frameSrc .= " https: http:";
} else {
$frameSrc .= " https:";
}
$csp .= "frame-src " . $frameSrc . "; ";
// Connexions : autoriser Mastodon et PeerTube
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) {
$connectSrc .= " ws: wss:"; // WebSockets pour le dev
}
$csp .= "connect-src " . $connectSrc . "; ";
// Médias : toujours autoriser 'self', Mastodon et PeerTube
$mediaSrc = "'self'";
// Ajouter l'instance Mastodon (pour les médias stockés sur l'instance)
if ($mastodonDomain) {
$mediaSrc .= " " . $mastodonDomain;
}
// Ajouter PeerTube
if ($peertubeDomain) {
$mediaSrc .= " " . $peertubeDomain;
}
// Ajouter l'URL S3 Mastodon si configurée (pour les médias externalisés)
if (defined('MASTODON_S3_MEDIA_URL') && !empty(MASTODON_S3_MEDIA_URL)) {
$s3Parsed = parse_url(MASTODON_S3_MEDIA_URL);
if ($s3Parsed && isset($s3Parsed['host'])) {
$s3Domain = $s3Parsed['scheme'] . '://' . $s3Parsed['host'];
$mediaSrc .= " " . $s3Domain;
}
}
if ($isLocalDev) {
$mediaSrc .= " https: http:";
} else {
$mediaSrc .= " https:";
}
$csp .= "media-src " . $mediaSrc . "; ";
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io "
. "{$fbScripts} {$xScripts} {$igScripts} {$ttScripts}; ";
$csp .= "img-src 'self' data: {$ytImages} https://www.facebook.com https://pbs.twimg.com https://abs.twimg.com"
. ($isLocalDev ? " https: http:" : " https:") . "; ";
$csp .= "frame-src 'self' {$fbFrames} {$xFrames} {$igFrames} {$ttFrames} https://www.youtube.com"
. ($isLocalDev ? " http:" : "") . "; ";
$csp .= "connect-src 'self' https://plausible.io https://www.googleapis.com https://www.youtube.com "
. "https://www.facebook.com https://graph.facebook.com https://connect.facebook.net "
. "https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com https://api.twitter.com "
. "https://www.instagram.com https://www.tiktok.com"
. ($isLocalDev ? " ws: wss:" : "") . "; ";
$csp .= "media-src 'self' https:; ";
$csp .= "object-src 'none'; ";
$csp .= "base-uri 'self';";
header('Content-Security-Policy: ' . $csp);
// HTTPS strict transport security (seulement si HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
@@ -289,19 +229,32 @@ function setSecurityHeaders() {
/**
* Valide l'origine de la requête pour les requêtes AJAX
*
*
* @return bool True si l'origine est valide
*/
function validateAjaxOrigin() {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
if (empty($origin) || empty($host)) {
if (empty($host)) {
return false;
}
$expectedOrigin = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $host;
return $origin === $expectedOrigin;
// Vérifier l'en-tête Origin si présent
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!empty($origin)) {
return $origin === $expectedOrigin;
}
// Si Origin est absent (requête same-origin), vérifier le Referer
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (!empty($referer)) {
return strpos($referer, $expectedOrigin) === 0;
}
// Accepter si ni Origin ni Referer (certains navigateurs/configs)
// La protection CSRF reste active via le token
return true;
}
?>
+33 -68
View File
@@ -1,88 +1,53 @@
<!-- Sidebar de navigation -->
<nav class="sidebar" role="navigation" aria-label="Navigation principale">
<a href="/" class="logo" aria-label="Retour à l'accueil">
<img src="img/logo.png" alt="Logo kaubuntu.re">
<img src="img/logo.png" alt="Logo <?php echo htmlspecialchars(SITE_NAME); ?>">
</a>
<?php
// Détecter la page courante et ses paramètres
$currentPage = basename($_SERVER['PHP_SELF']);
$currentCategoryId = isset($_GET['id']) ? intval($_GET['id']) : null;
$currentQuery = isset($_GET['q']) ? trim($_GET['q']) : '';
$isTagSearch = !empty($currentQuery) && substr($currentQuery, 0, 1) === '#';
$currentTag = $isTagSearch ? substr($currentQuery, 1) : '';
$isHome = ($currentPage === 'index.php' || $currentPage === '/');
?>
<div class="sidebar-nav">
<a href="/" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>" data-title="Accueil" aria-current="<?php echo ($currentPage === 'index.php') ? 'page' : 'false'; ?>">
<a href="/"
class="nav-item <?php echo $isHome ? 'active' : ''; ?>"
data-title="Accueil"
aria-current="<?php echo $isHome ? 'page' : 'false'; ?>">
<i class="fas fa-home" aria-hidden="true"></i> <span>Accueil</span>
</a>
<a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>" data-title="Direct" aria-current="<?php echo ($currentPage === 'direct.php') ? 'page' : 'false'; ?>">
<i class="fas fa-broadcast-tower" aria-hidden="true"></i> <span>Direct</span>
<div class="nav-divider"></div>
<a href="<?php echo $isHome ? '#youtube' : 'index.php#youtube'; ?>"
class="nav-item"
data-title="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> <span>YouTube</span>
</a>
<a href="<?php echo $isHome ? '#instagram' : 'index.php#instagram'; ?>"
class="nav-item"
data-title="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> <span>Instagram</span>
</a>
<a href="<?php echo $isHome ? '#tiktok' : 'index.php#tiktok'; ?>"
class="nav-item"
data-title="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> <span>TikTok</span>
</a>
<a href="<?php echo $isHome ? '#actualites' : 'index.php#actualites'; ?>"
class="nav-item"
data-title="Actualités">
<i class="fas fa-newspaper" aria-hidden="true"></i> <span>Actualités</span>
</a>
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<a href="dons.php" class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>" data-title="Soutenir" aria-current="<?php echo ($currentPage === 'dons.php') ? 'page' : 'false'; ?>">
<div class="nav-divider"></div>
<a href="dons.php"
class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>"
data-title="Soutenir"
aria-current="<?php echo ($currentPage === 'dons.php') ? 'page' : 'false'; ?>">
<i class="fas fa-heart" aria-hidden="true"></i> <span>Soutenir</span>
</a>
<?php endif; ?>
<div class="nav-divider"></div>
<?php
// Afficher les catégories prioritaires
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
// Tableau associatif des icônes pour les catégories
$categoryIcons = [
1 => 'fas fa-music', // Musique
2 => 'fas fa-film', // Films
3 => 'fas fa-car', // Véhicules
4 => 'fas fa-palette', // Jeux
5 => 'fas fa-running', // Sport
6 => 'fas fa-laugh', // Humour
7 => 'fas fa-gamepad', // Art
8 => 'fas fa-person', // Personnalités
9 => 'fas fa-face-grin-tears', // Comédie
10 => 'fas fa-tv', // Divertissement
11 => 'fas fa-globe', // Actualité & Politique
12 => 'fas fa-chalkboard-user', // Tutoriel
13 => 'fas fa-graduation-cap', // Education
14 => 'fas fa-fist-raised', // Activisme
15 => 'fas fa-microscope', // Science & Technologie
16 => 'fas fa-paw', // Animaux
17 => 'fas fa-child', // Enfants
18 => 'fas fa-utensils' // Cuisine
];
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
$icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder';
echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '" data-title="' . htmlspecialchars($name) . '" aria-current="' . ($isActive ? 'page' : 'false') . '">';
echo '<i class="' . $icon . '" aria-hidden="true"></i> <span>' . htmlspecialchars($name) . '</span>';
echo '</a>';
}
}
?>
<div class="nav-divider"></div>
<div class="category-title" role="heading" aria-level="2">
<i class="fas fa-hashtag" aria-hidden="true"></i> <span>Hashtags</span>
</div>
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<a href="recherche.php?q=<?php echo $encodedTag; ?>" class="tag-item <?php echo $isActive ? 'active' : ''; ?>" data-title="<?php echo htmlspecialchars($tag); ?>" aria-current="<?php echo $isActive ? 'page' : 'false'; ?>">
<i class="fas fa-hashtag tag-icon" aria-hidden="true"></i> <span><?php echo htmlspecialchars($tag); ?></span>
</a>
<?php
endforeach;
endif;
?>
</div>
</nav>
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* Récupération des statistiques de followers pour chaque plateforme sociale.
* Toutes les fonctions retournent null en cas d'échec — l'affichage reste optionnel.
* Cache 24h pour limiter les appels externes.
*/
function formatFollowerCount(int $n): string {
if ($n >= 1_000_000) return number_format($n / 1_000_000, 1, '.', '') . 'M';
if ($n >= 1_000) return number_format($n / 1_000, 1, '.', '') . 'K';
return (string) $n;
}
function _statsCurlFetch(string $url, array $headers = []): ?string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36',
CURLOPT_HTTPHEADER => $headers,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($body && $httpCode === 200) ? $body : null;
}
function getYouTubeSubscriberCount(): ?int {
// Priority 1: YouTube Data API v3 (exact count)
if (defined('YOUTUBE_API_KEY') && !empty(YOUTUBE_API_KEY)) {
$channelId = getYouTubeChannelId();
if ($channelId) {
$cacheKey = 'stat_yt_' . $channelId;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$data = callYouTubeApi('channels', ['part' => 'statistics', 'id' => $channelId]);
$count = isset($data['items'][0]['statistics']['subscriberCount'])
? (int) $data['items'][0]['statistics']['subscriberCount']
: null;
if ($count !== null) {
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
return $count;
}
}
}
// Priority 2: parse ytInitialData from public channel page
$handle = '';
if (defined('YOUTUBE_HANDLE') && !empty(YOUTUBE_HANDLE)) {
$handle = ltrim(YOUTUBE_HANDLE, '@');
} elseif (defined('YOUTUBE_CHANNEL_HANDLE') && !empty(YOUTUBE_CHANNEL_HANDLE)) {
$handle = ltrim(YOUTUBE_CHANNEL_HANDLE, '@');
}
$pageUrl = $handle
? 'https://www.youtube.com/@' . urlencode($handle)
: (defined('YOUTUBE_CHANNEL_ID') && YOUTUBE_CHANNEL_ID
? 'https://www.youtube.com/channel/' . urlencode(YOUTUBE_CHANNEL_ID)
: null);
if (!$pageUrl) return null;
$cacheKey = 'stat_yt_page_' . preg_replace('/[^a-z0-9]/i', '_', $handle ?: YOUTUBE_CHANNEL_ID);
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch($pageUrl, ['Accept-Language: fr-FR,fr;q=0.9,en;q=0.8']);
if (!$body) return null;
// ytInitialData embeds exact subscriberCount as a quoted integer string
if (preg_match('/"subscriberCount"\s*:\s*"(\d+)"/', $body, $m)) {
$count = (int) $m[1];
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
return $count;
}
return null;
}
function getInstagramFollowers(): ?int {
if (!defined('INSTAGRAM_HANDLE') || empty(INSTAGRAM_HANDLE)) return null;
$handle = ltrim(INSTAGRAM_HANDLE, '@');
$cacheKey = 'stat_ig_' . $handle;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch(
'https://www.instagram.com/api/v1/users/web_profile_info/?username=' . urlencode($handle),
['X-IG-App-ID: 936619743392459', 'Accept: application/json']
);
if (!$body) return null;
$data = json_decode($body, true);
$count = $data['data']['user']['edge_followed_by']['count'] ?? null;
if ($count !== null) {
$count = (int) $count;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
}
return $count;
}
function getTikTokFollowers(): ?int {
if (!defined('TIKTOK_HANDLE') || empty(TIKTOK_HANDLE)) return null;
$handle = ltrim(TIKTOK_HANDLE, '@');
$cacheKey = 'stat_tt_' . $handle;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch('https://www.tiktok.com/embed/@' . urlencode($handle));
if (!$body) return null;
preg_match('/"followerCount"\s*:\s*(\d+)/', $body, $m);
$count = isset($m[1]) ? (int) $m[1] : null;
if ($count !== null) {
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
}
return $count;
}
+203
View File
@@ -0,0 +1,203 @@
<?php
/**
* Intégration YouTube Data API v3
*/
function callYouTubeApi(string $endpoint, array $params = []): array {
if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return [];
$params['key'] = YOUTUBE_API_KEY;
$url = 'https://www.googleapis.com/youtube/v3/' . $endpoint . '?' . http_build_query($params);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe/2.0',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || !empty($error) || $httpCode < 200 || $httpCode >= 300) {
error_log('YouTube API error: ' . $error . ' (HTTP ' . $httpCode . ')');
return [];
}
return json_decode($response, true) ?: [];
}
function getYouTubeChannelId(): string {
if (defined('YOUTUBE_CHANNEL_ID') && !empty(YOUTUBE_CHANNEL_ID)) {
return YOUTUBE_CHANNEL_ID;
}
if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return '';
if (!defined('YOUTUBE_CHANNEL_HANDLE') || empty(YOUTUBE_CHANNEL_HANDLE)) return '';
$handle = ltrim(YOUTUBE_CHANNEL_HANDLE, '@');
$cached = $GLOBALS['simple_api_cache']->get('yt_channel_id_' . $handle, []);
if ($cached !== null) return (string) $cached;
$data = callYouTubeApi('channels', ['part' => 'snippet', 'forHandle' => $handle]);
$channelId = $data['items'][0]['id'] ?? '';
if ($channelId) {
$GLOBALS['simple_api_cache']->set('yt_channel_id_' . $handle, [], $channelId, 86400);
}
return $channelId;
}
function isYouTubeShort(string $isoDuration): bool {
preg_match('/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', $isoDuration, $m);
$seconds = (int)($m[1] ?? 0) * 3600 + (int)($m[2] ?? 0) * 60 + (int)($m[3] ?? 0);
return $seconds > 0 && $seconds <= 60;
}
function getYouTubeLatestVideos(int $count = 6): array {
// Priorité 1 : YouTube Data API v3
if (defined('YOUTUBE_API_KEY') && !empty(YOUTUBE_API_KEY)) {
$channelId = getYouTubeChannelId();
if ($channelId) {
$shortsTarget = min(3, $count - 1);
$regularTarget = $count - $shortsTarget;
$cacheKey = 'yt_videos2_' . $channelId . '_' . $count;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return $cached;
// On récupère 50 vidéos pour avoir suffisamment de chaque type
$searchData = callYouTubeApi('search', [
'part' => 'snippet',
'channelId' => $channelId,
'order' => 'date',
'type' => 'video',
'maxResults' => 50,
]);
if (!empty($searchData['items'])) {
$videoIds = array_map(fn($i) => $i['id']['videoId'], $searchData['items']);
// videos.list coûte 1 unité quota (vs 100 pour search)
$detailData = callYouTubeApi('videos', [
'part' => 'contentDetails',
'id' => implode(',', $videoIds),
]);
$durations = [];
foreach ($detailData['items'] ?? [] as $item) {
$durations[$item['id']] = $item['contentDetails']['duration'] ?? '';
}
$shorts = [];
$regular = [];
foreach ($searchData['items'] as $item) {
$videoId = $item['id']['videoId'];
$isShort = isset($durations[$videoId])
? isYouTubeShort($durations[$videoId])
: false;
$video = [
'id' => $videoId,
'title' => html_entity_decode($item['snippet']['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'thumbnail' => $item['snippet']['thumbnails']['high']['url']
?? $item['snippet']['thumbnails']['default']['url'],
'publishedAt' => $item['snippet']['publishedAt'],
'url' => $isShort
? 'https://www.youtube.com/shorts/' . $videoId
: 'https://www.youtube.com/watch?v=' . $videoId,
'isShort' => $isShort,
];
if ($isShort && count($shorts) < $shortsTarget) {
$shorts[] = $video;
} elseif (!$isShort && count($regular) < $regularTarget) {
$regular[] = $video;
}
if (count($shorts) >= $shortsTarget && count($regular) >= $regularTarget) break;
}
// Shorts en premier, puis vidéos normales
$videos = array_merge($shorts, $regular);
if (!empty($videos)) {
$ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 900;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl);
return $videos;
}
}
}
}
// Priorité 2 : flux RSS public YouTube (sans clé API, nécessite YOUTUBE_CHANNEL_ID)
return getYouTubeVideosFromRSS($count);
}
function getYouTubeVideosFromRSS(int $count = 6): array {
if (!defined('YOUTUBE_CHANNEL_ID') || empty(YOUTUBE_CHANNEL_ID)) return [];
$cacheKey = 'yt_rss_' . YOUTUBE_CHANNEL_ID . '_' . $count;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return $cached;
$feedUrl = 'https://www.youtube.com/feeds/videos.xml?channel_id=' . urlencode(YOUTUBE_CHANNEL_ID);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $feedUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe/2.0',
]);
$xmlString = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$xmlString || $httpCode !== 200) {
error_log('YouTube RSS error: HTTP ' . $httpCode);
return [];
}
$xml = @simplexml_load_string($xmlString);
if (!$xml) return [];
$videos = [];
$ytNs = 'http://www.youtube.com/xml/schemas/2015';
foreach ($xml->entry as $entry) {
if (count($videos) >= $count) break;
$yt = $entry->children($ytNs);
$videoId = (string) $yt->videoId;
if (!$videoId) continue;
$videos[] = [
'id' => $videoId,
'title' => html_entity_decode((string) $entry->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'thumbnail' => "https://i.ytimg.com/vi/{$videoId}/hqdefault.jpg",
'publishedAt' => (string) $entry->published,
'url' => "https://www.youtube.com/watch?v={$videoId}",
'isShort' => false,
];
}
if (!empty($videos)) {
$ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 3600;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl);
}
return $videos;
}
+41 -1
View File
@@ -111,6 +111,46 @@ function isValidWordPressUrl($url) {
return true;
}
/**
* Retourne le nombre total d'articles publiés sur WordPress.
* Lit le header X-WP-Total renvoyé par l'API REST.
*/
function getWordPressPostCount(): ?int {
if (!defined('WORDPRESS_ENABLED') || !WORDPRESS_ENABLED || empty(WORDPRESS_URL)) return null;
if (!isValidWordPressUrl(WORDPRESS_URL)) return null;
$cached = $GLOBALS['simple_api_cache']->get('wp_post_count', []);
if ($cached !== null) return (int) $cached ?: null;
$url = WORDPRESS_URL . '/wp-json/wp/v2/posts?per_page=1&_fields=id';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe-WordPress-Integration/1.0',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
if (!$response || $httpCode < 200 || $httpCode >= 300) return null;
$headers = substr($response, 0, $headerSize);
if (!preg_match('/X-WP-Total:\s*(\d+)/i', $headers, $m)) return null;
$count = (int) $m[1];
$GLOBALS['simple_api_cache']->set('wp_post_count', [], $count, 86400);
return $count;
}
/**
* Récupère les articles WordPress récents
*
@@ -157,7 +197,7 @@ function formatWordPressPosts($posts) {
// Nettoyer l'extrait HTML
$excerpt = '';
if (isset($post['excerpt']['rendered'])) {
$excerpt = wp_strip_all_tags($post['excerpt']['rendered']);
$excerpt = html_entity_decode(wp_strip_all_tags($post['excerpt']['rendered']), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$excerpt = trim($excerpt);
// Limiter à 150 caractères
if (strlen($excerpt) > 150) {
+350 -562
View File
File diff suppressed because it is too large Load Diff
+10 -90
View File
@@ -1,97 +1,17 @@
document.addEventListener("DOMContentLoaded", () => {
// Gestion des clics sur les vidéos
// Gestion des clics sur les vidéos initiales de la page catégorie
// Note: Le bouton "Voir Plus" est géré par main.js via data-category-id
const videoCards = document.querySelectorAll(".video-card");
for (const videoCard of videoCards) {
videoCard.addEventListener("click", function () {
const videoId = this.dataset.videoId;
if (videoId) {
window.location.href = `video.php?id=${videoId}`;
}
});
}
// Gestion du bouton "Voir plus"
const viewMoreBtn = document.querySelector(".view-more");
if (viewMoreBtn) {
viewMoreBtn.addEventListener("click", function () {
const page = Number.parseInt(this.dataset.page);
const categoryId =
document.querySelector(".video-section").dataset.categoryId;
// Changer le texte du bouton pendant le chargement
this.textContent = "Chargement...";
this.disabled = true;
// Préparer les données avec token CSRF
const formData = new FormData();
formData.append('csrf_token', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
// Faire la requête AJAX
fetch(
`ajax/load-more-videos.php?type=category&page=${page}&category=${categoryId}`,
{
method: 'POST',
headers: {
"X-Requested-With": "XMLHttpRequest",
},
body: formData
if (!videoCard.hasAttribute("data-click-initialized")) {
videoCard.setAttribute("data-click-initialized", "true");
videoCard.addEventListener("click", function () {
const videoId = this.dataset.videoId;
if (videoId) {
window.location.href = `video.php?id=${videoId}`;
}
)
.then((response) => response.json())
.then((data) => {
if (data.success) {
// Ajouter les nouvelles vidéos à la grille
const videoGrid = document.querySelector(".video-grid");
const tempDiv = document.createElement("div");
tempDiv.innerHTML = data.html;
// Ajouter chaque vidéo à la grille
while (tempDiv.firstChild) {
videoGrid.appendChild(tempDiv.firstChild);
}
// Mettre à jour le numéro de page
this.dataset.page = data.page + 1;
// Réinitialiser le texte du bouton
this.textContent = "Voir plus";
this.disabled = false;
// Si plus de vidéos à charger, masquer le bouton
if (!data.hasMore) {
this.style.display = "none";
}
// Initialiser les clics sur les nouvelles vidéos
const cards = document.querySelectorAll(".video-card:not([data-click-initialized])")
for (const card of cards) {
card.setAttribute("data-click-initialized", "true");
card.addEventListener("click", function () {
const videoId = this.dataset.videoId;
if (videoId) {
window.location.href = `video.php?id=${videoId}`;
}
});
}
} else {
// Gérer l'erreur
this.textContent = "Erreur lors du chargement";
setTimeout(() => {
this.textContent = "Voir plus";
this.disabled = false;
}, 2000);
}
})
.catch((error) => {
console.error("Erreur:", error);
this.textContent = "Erreur lors du chargement";
setTimeout(() => {
this.textContent = "Voir plus";
this.disabled = false;
}, 2000);
});
});
});
}
}
});
+85 -33
View File
@@ -320,51 +320,42 @@ document.addEventListener('DOMContentLoaded', function() {
viewMoreButtons.forEach(button => {
// Déterminer le type de vidéos à charger
const section = button.closest('.video-section');
const sectionTitle = section.querySelector('.section-title').textContent.trim().toLowerCase();
const videoGrid = section.querySelector('.video-grid');
let videoType = '';
let categoryId = null;
if (sectionTitle.includes('dernières')) {
videoType = 'recent';
} else if (sectionTitle.includes('tendances')) {
videoType = 'trending';
} else if (sectionTitle.includes('indépendance')) {
videoType = 'independence';
} else {
// Vérifier si c'est une section de catégorie
const categorySection = section.querySelector('[data-category-id]');
if (categorySection) {
videoType = 'category';
categoryId = categorySection.dataset.categoryId;
} else if (section.hasAttribute('data-category-id')) {
videoType = 'category';
categoryId = section.dataset.categoryId;
}
// Utiliser l'attribut data-video-type si présent (méthode prioritaire)
if (section.hasAttribute('data-video-type')) {
videoType = section.dataset.videoType;
} else if (section.hasAttribute('data-category-id')) {
// Section de catégorie
videoType = 'category';
categoryId = section.dataset.categoryId;
}
// Si aucun type reconnu, ne pas configurer l'événement
if (!videoType) return;
// Stocker le numéro de page actuel
button.dataset.page = '1';
button.addEventListener('click', function() {
const page = parseInt(this.dataset.page);
// Compter les vidéos déjà affichées pour calculer l'offset
const currentCount = videoGrid.querySelectorAll('.video-card').length;
// Changer le texte du bouton pendant le chargement
button.textContent = 'Chargement...';
button.disabled = true;
// Préparer l'URL avec les paramètres
let url = `ajax/load-more-videos.php?type=${videoType}&page=${page}`;
// Préparer l'URL avec les paramètres (utiliser offset au lieu de page)
let url = `ajax/load-more-videos?type=${videoType}&offset=${currentCount}`;
if (videoType === 'category' && categoryId) {
url += `&category=${categoryId}`;
}
// Préparer les données avec token CSRF
const formData = new FormData();
formData.append('csrf_token', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
if (csrfMeta) {
formData.append('csrf_token', csrfMeta.getAttribute('content'));
}
// Faire la requête AJAX
fetch(url, {
@@ -372,7 +363,8 @@ document.addEventListener('DOMContentLoaded', function() {
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
body: formData,
credentials: 'same-origin'
})
.then(response => response.json())
.then(data => {
@@ -386,9 +378,6 @@ document.addEventListener('DOMContentLoaded', function() {
videoGrid.appendChild(tempDiv.firstChild);
}
// Mettre à jour le numéro de page
this.dataset.page = data.page + 1;
// Réinitialiser le texte du bouton
button.textContent = 'Voir plus';
button.disabled = false;
@@ -417,6 +406,50 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// Scroll animé vers les sections depuis les badges hero et les liens de navigation
document.querySelectorAll('a[href^="#"]').forEach(function(link) {
link.addEventListener('click', function(e) {
const targetId = this.getAttribute('href').slice(1);
const target = document.getElementById(targetId);
if (!target) return;
e.preventDefault();
// Fermer le menu mobile si ouvert
if (mobileMenu && mobileMenu.classList.contains('active')) {
mobileMenu.classList.remove('active');
document.body.style.overflow = '';
}
const offset = 24;
const top = target.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: top, behavior: 'smooth' });
// Highlight de la section à l'arrivée
const duration = Math.min(
600 + Math.abs(top - window.scrollY) / 4,
1200
);
setTimeout(function() {
target.classList.add('section-arrive');
setTimeout(function() { target.classList.remove('section-arrive'); }, 900);
}, duration);
});
});
// Bouton retour en haut
const backToTop = document.getElementById('back-to-top');
if (backToTop) {
window.addEventListener('scroll', function() {
backToTop.classList.toggle('visible', window.scrollY > 300);
}, { passive: true });
backToTop.addEventListener('click', function() {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// Scroll vers le haut avec effet moderne quand on clique sur le logo du footer
const footerLogo = document.querySelector('.footer-logo');
if (footerLogo) {
@@ -427,4 +460,23 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Auto-resize des iframes de profil social (TikTok, Instagram)
// Ces plateformes envoient des postMessage avec leur hauteur réelle
window.addEventListener('message', function(e) {
if (!e.data || !e.origin) return;
try {
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
const height = data.height ?? data.contentHeight ?? data['height'];
if (!height || typeof height !== 'number' || height < 100) return;
document.querySelectorAll('.platform-profile-iframe').forEach(function(iframe) {
try {
if (new URL(iframe.src).origin === e.origin) {
iframe.style.height = Math.ceil(height) + 'px';
}
} catch (_) {}
});
} catch (_) {}
});
});