Ma version ok

This commit is contained in:
Flatlogic Bot 2026-05-01 08:39:24 +00:00
parent 7358b13032
commit b1894c71eb
9 changed files with 2174 additions and 526 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,397 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
const COOKIE_NAME = 'ptcs_consent';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 180;
const STORAGE_KEYS = {
personalization: 'ptcs_ui_preferences',
audience: 'ptcs_local_audience'
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
const banner = document.getElementById('cookie-banner');
const reopenButton = document.getElementById('cookie-reopen');
const reopenLabel = document.getElementById('cookie-reopen-label');
const footerCookieSettings = document.getElementById('footer-cookie-settings');
const dismissScrollHint = document.getElementById('scroll-hint-dismiss');
const scrollHint = document.getElementById('scroll-hint');
const toastStack = document.getElementById('toast-stack');
const personalizationControls = Array.from(document.querySelectorAll('[data-consent-control="personalization"]'));
const audienceControls = Array.from(document.querySelectorAll('[data-consent-control="audience"]'));
const consentActionButtons = Array.from(document.querySelectorAll('[data-cookie-action]'));
const trackedSections = Array.from(document.querySelectorAll('[data-track-section]'));
appendMessage(message, 'visitor');
chatInput.value = '';
const state = {
prefs: {
essential: true,
personalization: false,
audience: false,
timestamp: null
},
audienceCountedThisSession: false,
visibleSections: new Set()
};
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
function safeParse(rawValue) {
if (!rawValue) {
return null;
}
try {
return JSON.parse(rawValue);
} catch (error) {
return null;
}
}
function storageGet(key) {
try {
return window.localStorage.getItem(key);
} catch (error) {
return null;
}
}
function storageSet(key, value) {
try {
window.localStorage.setItem(key, value);
} catch (error) {
// no-op
}
}
function storageRemove(key) {
try {
window.localStorage.removeItem(key);
} catch (error) {
// no-op
}
}
function getCookie(name) {
const prefix = `${name}=`;
const cookies = document.cookie ? document.cookie.split('; ') : [];
for (const row of cookies) {
if (row.startsWith(prefix)) {
return decodeURIComponent(row.substring(prefix.length));
}
}
return null;
}
function setCookie(name, value, maxAge) {
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
}
function normalizePrefs(rawPrefs = {}) {
return {
essential: true,
personalization: Boolean(rawPrefs.personalization),
audience: Boolean(rawPrefs.audience),
timestamp: rawPrefs.timestamp || null
};
}
function readSavedPrefs() {
const parsed = safeParse(getCookie(COOKIE_NAME));
return parsed ? normalizePrefs(parsed) : null;
}
function setControlState(controls, value) {
controls.forEach((control) => {
control.checked = Boolean(value);
});
}
function setBadge(key, label, status) {
document.querySelectorAll(`[data-consent-badge="${key}"]`).forEach((badge) => {
badge.textContent = label;
badge.dataset.state = status;
});
}
function formatDate(dateString) {
if (!dateString) {
return '—';
}
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
return '—';
}
return new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'short',
timeStyle: 'short'
}).format(date);
}
function updateSummaryText() {
const summary = state.prefs.personalization || state.prefs.audience
? `Réglages actifs : essentiels, ${state.prefs.personalization ? 'personnalisation' : 'personnalisation coupée'} et ${state.prefs.audience ? 'mesure locale activée' : 'mesure locale désactivée'}.`
: 'Le site conserve uniquement l\'essentiel tant que vous n\'avez pas choisi d\'options supplémentaires.';
document.querySelectorAll('[data-consent-summary]').forEach((node) => {
node.textContent = summary;
});
}
function updateReminderLabel() {
if (!reopenLabel) {
return;
}
if (state.prefs.personalization && state.prefs.audience) {
reopenLabel.textContent = 'Cookies : choix personnalisés';
return;
}
if (state.prefs.personalization || state.prefs.audience) {
reopenLabel.textContent = 'Cookies : options partielles';
return;
}
reopenLabel.textContent = 'Cookies : essentiels';
}
function updateAudiencePanel(store) {
const audienceEnabled = state.prefs.audience;
const visits = audienceEnabled && store ? Number(store.visits || 0) : 0;
const sections = audienceEnabled && store && Array.isArray(store.sections) ? store.sections.length : 0;
const lastVisit = audienceEnabled && store ? formatDate(store.lastVisit || null) : '—';
const statusText = audienceEnabled
? 'La mesure locale d\'audience est activée sur cet appareil. Les compteurs ci-dessous sont stockés uniquement dans votre navigateur.'
: 'La mesure locale d\'audience est désactivée. Aucun service tiers n\'est utilisé.';
document.querySelectorAll('[data-audience-status]').forEach((node) => {
node.textContent = statusText;
});
document.querySelectorAll('[data-audience-visits]').forEach((node) => {
node.textContent = String(visits);
});
document.querySelectorAll('[data-audience-sections]').forEach((node) => {
node.textContent = String(sections);
});
document.querySelectorAll('[data-audience-last-visit]').forEach((node) => {
node.textContent = lastVisit;
});
}
function showToast(message) {
if (!toastStack) {
return;
}
const toast = document.createElement('div');
toast.className = 'app-toast';
toast.setAttribute('role', 'status');
toast.textContent = message;
toastStack.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add('is-visible');
});
window.setTimeout(() => {
toast.classList.remove('is-visible');
window.setTimeout(() => {
toast.remove();
}, 220);
}, 3200);
}
function readAudienceStore() {
const parsed = safeParse(storageGet(STORAGE_KEYS.audience));
if (!parsed || typeof parsed !== 'object') {
return {
visits: 0,
sections: [],
lastVisit: null
};
}
return {
visits: Number(parsed.visits || 0),
sections: Array.isArray(parsed.sections) ? parsed.sections : [],
lastVisit: parsed.lastVisit || null
};
}
function saveAudienceStore(store) {
storageSet(STORAGE_KEYS.audience, JSON.stringify(store));
}
function refreshPersonalizationUI() {
const stored = safeParse(storageGet(STORAGE_KEYS.personalization)) || {};
if (!scrollHint) {
return;
}
if (!state.prefs.personalization) {
scrollHint.hidden = false;
return;
}
scrollHint.hidden = stored.scrollHintDismissed === true;
}
function applyAudienceState() {
if (!state.prefs.audience) {
storageRemove(STORAGE_KEYS.audience);
state.audienceCountedThisSession = false;
updateAudiencePanel(null);
return;
}
const store = readAudienceStore();
if (!state.audienceCountedThisSession) {
store.visits += 1;
store.lastVisit = new Date().toISOString();
state.audienceCountedThisSession = true;
}
if (state.visibleSections.size > 0) {
const merged = new Set([...(store.sections || []), ...state.visibleSections]);
store.sections = Array.from(merged);
}
saveAudienceStore(store);
updateAudiencePanel(store);
}
function applyPrefs(prefs) {
state.prefs = normalizePrefs(prefs);
document.body.dataset.consentPersonalization = state.prefs.personalization ? 'on' : 'off';
document.body.dataset.consentAudience = state.prefs.audience ? 'on' : 'off';
setControlState(personalizationControls, state.prefs.personalization);
setControlState(audienceControls, state.prefs.audience);
setBadge('essential', 'Toujours actif', 'locked');
setBadge('personalization', state.prefs.personalization ? 'Activée' : 'Désactivée', state.prefs.personalization ? 'on' : 'off');
setBadge('audience', state.prefs.audience ? 'Activée' : 'Désactivée', state.prefs.audience ? 'on' : 'off');
if (!state.prefs.personalization) {
storageRemove(STORAGE_KEYS.personalization);
}
refreshPersonalizationUI();
applyAudienceState();
updateSummaryText();
updateReminderLabel();
}
function openBanner() {
if (!banner) {
return;
}
banner.hidden = false;
if (reopenButton) {
reopenButton.setAttribute('aria-expanded', 'true');
}
}
function closeBanner() {
if (!banner) {
return;
}
banner.hidden = true;
if (reopenButton) {
reopenButton.setAttribute('aria-expanded', 'false');
}
}
function savePrefs(nextPrefs, notice) {
const normalized = normalizePrefs(nextPrefs);
normalized.timestamp = new Date().toISOString();
setCookie(COOKIE_NAME, JSON.stringify(normalized), COOKIE_MAX_AGE);
applyPrefs(normalized);
closeBanner();
showToast(notice);
}
if (dismissScrollHint && scrollHint) {
dismissScrollHint.addEventListener('click', () => {
scrollHint.hidden = true;
if (state.prefs.personalization) {
storageSet(STORAGE_KEYS.personalization, JSON.stringify({
scrollHintDismissed: true,
updatedAt: new Date().toISOString()
}));
showToast('Le rappel mobile a été masqué pour cet appareil.');
}
});
}
consentActionButtons.forEach((button) => {
button.addEventListener('click', () => {
const action = button.dataset.cookieAction;
if (action === 'accept') {
savePrefs({
essential: true,
personalization: true,
audience: true
}, 'Toutes les options facultatives ont été activées.');
return;
}
if (action === 'reject') {
savePrefs({
essential: true,
personalization: false,
audience: false
}, 'Seuls les cookies essentiels sont conservés.');
return;
}
if (action === 'save') {
savePrefs({
essential: true,
personalization: personalizationControls.some((control) => control.checked),
audience: audienceControls.some((control) => control.checked)
}, 'Vos préférences cookies ont été enregistrées.');
}
});
});
[reopenButton, footerCookieSettings].forEach((trigger) => {
if (!trigger) {
return;
}
trigger.addEventListener('click', () => {
openBanner();
});
});
if ('IntersectionObserver' in window && trackedSections.length > 0) {
const observer = new IntersectionObserver((entries) => {
let changed = false;
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const id = entry.target.id || entry.target.dataset.trackSection || '';
if (!id) {
return;
}
if (!state.visibleSections.has(id)) {
state.visibleSections.add(id);
changed = true;
}
});
if (changed && state.prefs.audience) {
applyAudienceState();
}
}, {
threshold: 0.45
});
trackedSections.forEach((section) => observer.observe(section));
}
const savedPrefs = readSavedPrefs();
if (savedPrefs) {
applyPrefs(savedPrefs);
closeBanner();
} else {
applyPrefs({ essential: true, personalization: false, audience: false });
openBanner();
}
});

506
index.php
View File

@ -1,150 +1,378 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/site.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$site = site_settings();
$pageTitle = 'Programme TV ce soir et en ce moment | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
$fallbackDescription = 'Programme TV ce soir, direct en ce moment, chaînes TNT, films, séries, sport, documentaires et prime time : consultez rapidement ce qu\'il y a à la télé sur programmetelecesoir.fr.';
$keywords = 'programme tv ce soir, programme télé ce soir, en ce moment, ce soir à la télé, programme tv tnt, film ce soir, série ce soir, match ce soir, émission ce soir, programme tf1, programme france 2, programme m6, programme arte, direct tv, grille tv';
$updatedAt = date('d/m/Y');
$quickLinks = [
[
'title' => 'Voir le direct immédiatement',
'copy' => 'Accédez au widget “en ce moment” pour savoir ce qui passe maintenant sur les chaînes les plus consultées.',
'href' => '#widget-section',
],
[
'title' => 'Comparer le prime time',
'copy' => 'Parcourez les mots-clés, les chaînes et les catégories éditoriales pour préparer votre soirée TV.',
'href' => '#guide',
],
[
'title' => 'Repérer un film, une série ou un match',
'copy' => 'Les sections éditoriales ciblent les recherches les plus fréquentes liées au programme TV ce soir.',
'href' => '#recherches',
],
[
'title' => 'Contrôler vos cookies',
'copy' => 'Utilisez la bannière et le bouton flottant en bas à gauche pour modifier vos choix à tout moment.',
'href' => '#legal',
],
];
$benefits = [
'Programme TV ce soir et en ce moment sur une page simple, rapide et lisible.',
'Accès prioritaire aux chaînes TNT, aux grandes chaînes nationales et aux rendez-vous de prime time.',
'Lecture mobile optimisée avec défilement horizontal visible sous le widget TV.',
'Conformité vie privée avec bannière de consentement, réglages persistants et documents légaux dédiés.',
];
$channelCards = [
['name' => 'TF1', 'copy' => 'Programme TF1 ce soir : divertissement, fiction populaire, sport et grands événements en prime time.'],
['name' => 'France 2', 'copy' => 'Programme France 2 ce soir : séries, magazines, infos, culture et soirées événementielles.'],
['name' => 'France 3', 'copy' => 'Programme France 3 ce soir : patrimoine, régions, cinéma français et documentaires accessibles.'],
['name' => 'Canal+', 'copy' => 'Programme Canal+ ce soir : cinéma, créations originales, sport premium et événements exclusifs.'],
['name' => 'M6', 'copy' => 'Programme M6 ce soir : divertissements, magazines, séries et rendez-vous familiaux.'],
['name' => 'Arte', 'copy' => 'Programme Arte ce soir : films dauteur, documentaires, culture, histoire et créations européennes.'],
['name' => 'France 5', 'copy' => 'Programme France 5 ce soir : documentaires, débats, société, science et découverte.'],
['name' => 'C8 / CStar', 'copy' => 'Programme C8 et CStar ce soir : magazines, talk-shows, divertissements et musique.'],
['name' => 'TMC / TFX', 'copy' => 'Programme TMC et TFX ce soir : films, séries populaires, talks et télé-réalité.'],
['name' => 'W9 / 6ter', 'copy' => 'Programme W9 et 6ter ce soir : cinéma, séries, clips, magazines et programmes feel-good.'],
['name' => 'RMC Story / RMC Découverte', 'copy' => 'Programme RMC Story et RMC Découverte : enquêtes, découverte, mécanique, société et histoire.'],
['name' => 'Gulli / Jeunesse', 'copy' => 'Programme jeunesse ce soir : dessins animés, films familiaux et rendez-vous enfants.'],
];
$keywordBadges = [
'programme tv ce soir', 'programme télé ce soir', 'ce soir à la télé', 'programme tv en ce moment', 'programme tnt ce soir',
'film ce soir', 'série ce soir', 'match ce soir', 'sport à la tv', 'documentaire ce soir', 'émission ce soir',
'prime time', 'deuxième partie de soirée', 'direct tv', 'grille tv', 'chaînes tnt', 'programme tf1 ce soir',
'programme france 2 ce soir', 'programme m6 ce soir', 'programme arte ce soir', 'télé ce soir', 'que regarder ce soir',
'programme canal+', 'programme france 3', 'programme france 5', 'programme tmc', 'programme w9', 'programme c8',
];
$faqItems = [
[
'question' => 'Où voir rapidement le programme TV ce soir ?',
'answer' => 'La zone “Programme TV en ce moment” placée en haut de la page donne un accès direct au widget, puis les sections éditoriales vous aident à comparer les chaînes, les genres et les créneaux du soir.',
],
[
'question' => 'Comment savoir ce qu\'il y a à la télé en ce moment ?',
'answer' => 'Le widget affiche le direct des chaînes les plus consultées. Sur mobile et tablette, un défilement horizontal est prévu sous le widget pour conserver une lecture confortable.',
],
[
'question' => 'Quelles chaînes sont mises en avant sur la page ?',
'answer' => 'La page couvre les grandes chaînes généralistes et TNT recherchées le plus souvent : TF1, France 2, France 3, M6, Arte, Canal+, France 5, TMC, W9, C8 et dautres chaînes populaires.',
],
[
'question' => 'Quels cookies sont utilisés sur programmetelecesoir.fr ?',
'answer' => 'Cette première version dépose un cookie essentiel pour mémoriser vos choix de consentement. Les autres fonctions optionnelles sont désactivées par défaut et peuvent être activées ou refusées depuis la bannière ou le bouton flottant.',
],
[
'question' => 'Qui contacter pour les questions de confidentialité ?',
'answer' => 'Le DPO déclaré sur le site est M LORENTE CHRISTOPHE, 7 rue Lucien Deneau 28300 Mainvilliers, téléphone 06 58 22 59 16. Les détails figurent aussi dans la politique de confidentialité.',
],
];
$faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => array_map(static function (array $item): array {
return [
'@type' => 'Question',
'name' => $item['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $item['answer'],
],
];
}, $faqItems),
];
$organizationSchema = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
'name' => $site['domain'],
'url' => $site['base_url'] . '/',
'description' => $fallbackDescription,
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => '7 rue Lucien Deneau',
'postalCode' => '28300',
'addressLocality' => 'Mainvilliers',
'addressCountry' => 'FR',
],
'contactPoint' => [
'@type' => 'ContactPoint',
'contactType' => 'data protection officer',
'name' => $site['dpo_name'],
'telephone' => '+33 6 58 22 59 16',
'availableLanguage' => ['fr'],
],
];
?>
<!doctype html>
<html lang="en">
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<?php render_site_head($pageTitle, $fallbackDescription, $keywords); ?>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<body class="app-body" data-page="home">
<?php render_site_nav('home'); ?>
<noscript>
<div class="container pt-3">
<div class="alert alert-secondary border-0">JavaScript est nécessaire pour la gestion fine des cookies et pour le chargement du widget TV.</div>
</div>
</noscript>
<main class="site-main">
<section class="hero-section" id="top" data-track-section>
<div class="container">
<div class="hero-grid">
<div class="hero-copy">
<span class="eyebrow">Programme TV France Mise à jour éditoriale du <?= e($updatedAt) ?></span>
<h1>Programme TV ce soir et en ce moment&nbsp;: votre accès rapide aux chaînes et aux rendez-vous du soir</h1>
<p class="lead">Programmetelecesoir.fr centralise l'intention de recherche la plus importante du secteur&nbsp;: savoir <strong>ce qu'il y a à la télé ce soir</strong> et <strong>ce qui passe en ce moment</strong>. La page met immédiatement le widget TV en avant, puis complète la lecture avec un guide éditorial pensé pour le prime time, les films, les séries, le sport, les documentaires et les chaînes TNT les plus consultées.</p>
<div class="hero-actions">
<a class="btn btn-dark btn-refined" href="#widget-section">Voir le programme en ce moment</a>
<a class="btn btn-outline-dark btn-refined" href="#guide">Lire le guide TV du soir</a>
<a class="btn btn-outline-dark btn-refined" href="#faq">Questions fréquentes</a>
</div>
<div class="hero-proof">
<?php foreach ($benefits as $benefit): ?>
<div class="proof-item">
<span class="proof-dot" aria-hidden="true"></span>
<span><?= e($benefit) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<aside class="stacked-panels" aria-label="Informations complémentaires">
<article class="info-panel compact-panel">
<div class="panel-heading">
<span class="panel-kicker">Préférences</span>
<h2>Consentement en direct</h2>
</div>
<p class="panel-copy" data-consent-summary>Le site conserve uniquement l'essentiel tant que vous n'avez pas choisi d'options supplémentaires.</p>
<ul class="status-list list-unstyled mb-0">
<li><span>Essentiels</span><span class="status-pill" data-consent-badge="essential">Toujours actif</span></li>
<li><span>Personnalisation</span><span class="status-pill" data-consent-badge="personalization">Désactivée</span></li>
<li><span>Audience locale</span><span class="status-pill" data-consent-badge="audience">Désactivée</span></li>
</ul>
</article>
<article class="info-panel compact-panel">
<div class="panel-heading">
<span class="panel-kicker">Mesure locale</span>
<h2>Effet visible des toggles</h2>
</div>
<p class="panel-copy" data-audience-status>La mesure locale d'audience est désactivée. Aucun service tiers n'est utilisé.</p>
<div class="stat-grid">
<div>
<strong data-audience-visits>0</strong>
<span>visites locales</span>
</div>
<div>
<strong data-audience-sections>0</strong>
<span>sections vues</span>
</div>
<div>
<strong data-audience-last-visit></strong>
<span>dernière activité</span>
</div>
</div>
</article>
<article class="info-panel compact-panel">
<div class="panel-heading">
<span class="panel-kicker">Conformité</span>
<h2>DPO et hébergement</h2>
</div>
<p class="panel-copy mb-2"><strong><?= e($site['dpo_name']) ?></strong><br><?= e($site['dpo_address']) ?><br>Tél. <?= e($site['dpo_phone']) ?></p>
<p class="panel-copy mb-0">Hébergeur indiqué sur le site&nbsp;: <strong><?= e($site['host_name']) ?></strong>.</p>
</article>
</aside>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</section>
<section class="section-block" id="widget-section" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading with-meta">
<div>
<span class="eyebrow">Accès direct</span>
<h2>Voir ce qu'il y a à la télé maintenant</h2>
<p>Le widget est volontairement placé juste sous le titre pour répondre immédiatement à la requête “programme TV en ce moment”. Sur mobile et tablette, la zone est contenue dans un défilement horizontal visible en bas.</p>
</div>
<div class="section-meta">Défilement horizontal activé sur mobile et tablette</div>
</div>
<div class="widget-card">
<div class="widget-scroll" role="region" aria-label="Widget programme TV avec défilement horizontal sur mobile et tablette">
<div class="widget-scroll__inner">
<a class="widget-tv2" href="https://tv-programme.com" data-type="en-ce-moment" data-nb-chaines="12" data-color="#DFF0D8" data-width="100%">Programme TV</a><script async src="https://tv-programme.com/widget.js"></script><p class="tvp-widget-attribution">Source TV : <a href="https://tv-programme.com">Programme TV</a></p>
</div>
</div>
<div class="scroll-note" id="scroll-hint">
<div>
<strong>Astuce mobile :</strong> si toute la largeur n'est pas visible, faites glisser horizontalement la zone du widget. Ce rappel peut être mémorisé uniquement si la personnalisation est activée.
</div>
<button type="button" id="scroll-hint-dismiss">Masquer ce conseil</button>
</div>
</div>
</div>
</div>
</section>
<section class="section-block" id="guide" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading">
<span class="eyebrow">Guide TV du soir</span>
<h2>Une longue page SEO orientée sur les recherches réelles des internautes</h2>
<p>Cette page cible les requêtes majeures du secteur&nbsp;: <strong>programme TV ce soir</strong>, <strong>programme télé ce soir</strong>, <strong>ce soir à la télé</strong>, <strong>programme TV TNT</strong>, <strong>film ce soir</strong>, <strong>série ce soir</strong>, <strong>match ce soir</strong>, <strong>documentaire ce soir</strong> et <strong>programme TV en ce moment</strong>.</p>
</div>
<div class="content-grid">
<article class="article-panel">
<p>Si vous cherchez <strong>quoi regarder ce soir</strong>, la logique de programmetelecesoir.fr est simple&nbsp;: offrir d'abord le direct, puis un accompagnement éditorial lisible. En un seul endroit, vous pouvez surveiller le programme TV des grandes chaînes, comparer le prime time, repérer un film à la télévision ce soir, vérifier la présence d'une série, d'un documentaire, d'un match, d'une émission d'information, d'un magazine ou d'un divertissement familial.</p>
<p>La structure éditoriale de la page reprend les formulations les plus recherchées autour du <strong>programme télé ce soir</strong>&nbsp;: programme TV TNT ce soir, programme TV maintenant, télé ce soir, programme des chaînes en soirée, directs TV, grille TV du soir, deuxième partie de soirée, films et séries du prime time. L'objectif n'est pas d'encombrer la lecture, mais de répondre précisément aux intentions qui reviennent le plus souvent sur mobile comme sur desktop.</p>
<p>Les internautes qui tapent <strong>programme TF1 ce soir</strong>, <strong>programme France 2 ce soir</strong>, <strong>programme M6 ce soir</strong> ou <strong>programme Arte ce soir</strong> recherchent généralement un accès immédiat, sans détour, avec une page rapide, claire et fiable. C'est pour cette raison que le site priorise la lisibilité, le widget en tête de page, des cartes chaînes synthétiques, une FAQ structurée et des documents légaux facilement accessibles.</p>
</article>
<aside class="aside-panels">
<?php foreach ($quickLinks as $item): ?>
<article class="info-panel">
<h3><?= e($item['title']) ?></h3>
<p><?= e($item['copy']) ?></p>
<a class="text-link" href="<?= e($item['href']) ?>">Accéder à la section</a>
</article>
<?php endforeach; ?>
</aside>
</div>
</div>
</div>
</section>
<section class="section-block" id="chaines" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading">
<span class="eyebrow">Chaînes recherchées</span>
<h2>Programme TV du soir chaîne par chaîne</h2>
<p>La page reprend les grandes intentions liées aux principales chaînes consultées en France pour le programme de ce soir, le direct actuel, les films, les séries, les magazines, la culture et le sport.</p>
</div>
<div class="card-grid card-grid--three">
<?php foreach ($channelCards as $channel): ?>
<article class="channel-card">
<div class="channel-card__name"><?= e($channel['name']) ?></div>
<p><?= e($channel['copy']) ?></p>
</article>
<?php endforeach; ?>
</div>
</div>
</div>
</section>
<section class="section-block" id="recherches" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading">
<span class="eyebrow">Intentions fréquentes</span>
<h2>Mots-clés éditoriaux et besoins de lecture</h2>
<p>Pour couvrir un maximum d'intentions, la page déploie un champ lexical riche mais propre, sans agressivité visuelle. Les badges ci-dessous représentent les expressions que le visiteur recherche le plus souvent avant de choisir son programme.</p>
</div>
<div class="keyword-cloud" aria-label="Mots-clés du programme TV ce soir">
<?php foreach ($keywordBadges as $keyword): ?>
<span class="keyword-pill"><?= e($keyword) ?></span>
<?php endforeach; ?>
</div>
<div class="content-grid mt-4">
<article class="info-panel article-like">
<h3>Comment choisir rapidement le bon programme ce soir&nbsp;?</h3>
<ol class="ordered-steps">
<li>Commencez par le widget “en ce moment” pour identifier l'offre en direct.</li>
<li>Repérez ensuite la chaîne qui correspond à votre envie&nbsp;: film, série, documentaire, sport, culture ou divertissement.</li>
<li>Consultez les cartes chaînes pour accélérer la comparaison entre TF1, France 2, M6, Arte, Canal+ et TNT.</li>
<li>Utilisez la FAQ et les liens légaux si vous souhaitez comprendre la confidentialité, les cookies et les règles d'utilisation.</li>
</ol>
</article>
<article class="info-panel article-like">
<h3>Pourquoi cette page fonctionne bien sur mobile&nbsp;?</h3>
<p>Le design a été allégé pour rester très lisible&nbsp;: navigation compacte, sections régulières, contraste fort, cartes sobres, bouton flottant pour les cookies et zone horizontale spécifique sous le widget afin d'éviter toute coupure sur smartphone ou tablette. Le résultat est plus confortable pour les utilisateurs qui cherchent vite “ce soir à la télé” depuis leur mobile.</p>
<p>Le site évite aussi les scripts marketing et les effets visuels inutiles. Cela aide à conserver une expérience rapide, rassurante et facile à parcourir, ce qui renforce la consultation répétée lorsqu'on veut simplement savoir quel programme TV regarder ce soir.</p>
</article>
</div>
</div>
</div>
</section>
<section class="section-block" id="faq" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading">
<span class="eyebrow">FAQ</span>
<h2>Questions fréquentes sur le programme TV ce soir</h2>
<p>Les réponses ci-dessous renforcent l'intention utilisateur tout en gardant une lecture utile, concise et structurée.</p>
</div>
<div class="accordion custom-accordion" id="faqAccordion">
<?php foreach ($faqItems as $index => $item): ?>
<div class="accordion-item">
<h3 class="accordion-header" id="faq-heading-<?= $index ?>">
<button class="accordion-button <?= $index === 0 ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#faq-collapse-<?= $index ?>" aria-expanded="<?= $index === 0 ? 'true' : 'false' ?>" aria-controls="faq-collapse-<?= $index ?>">
<?= e($item['question']) ?>
</button>
</h3>
<div id="faq-collapse-<?= $index ?>" class="accordion-collapse collapse <?= $index === 0 ? 'show' : '' ?>" aria-labelledby="faq-heading-<?= $index ?>" data-bs-parent="#faqAccordion">
<div class="accordion-body">
<?= e($item['answer']) ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</section>
<section class="section-block" id="legal" data-track-section>
<div class="container">
<div class="section-shell">
<div class="section-heading">
<span class="eyebrow">Confiance & conformité</span>
<h2>Politique de cookies, confidentialité et règlement</h2>
<p>Les documents juridiques ont été séparés en pages dédiées pour rester clairs et faciles à consulter. Le bouton flottant en bas à gauche rouvre les préférences cookies à tout moment.</p>
</div>
<div class="card-grid card-grid--three">
<article class="legal-card">
<h3>Politique de cookies</h3>
<p>Détail du cookie essentiel de consentement, des options facultatives et du rôle du widget TV nécessaire au service.</p>
<a class="text-link" href="/politique-cookies.php">Lire la politique de cookies</a>
</article>
<article class="legal-card">
<h3>Politique de confidentialité</h3>
<p>Présentation du responsable, du DPO, de l'hébergement, des données traitées et des droits des personnes.</p>
<a class="text-link" href="/politique-confidentialite.php">Lire la politique de confidentialité</a>
</article>
<article class="legal-card">
<h3>Règlement d'utilisation</h3>
<p>Cadre d'usage du site, responsabilité, disponibilité, propriété intellectuelle et rappel des règles de consultation.</p>
<a class="text-link" href="/reglement.php">Lire le règlement</a>
</article>
</div>
</div>
</div>
</section>
</main>
<?php render_site_footer(); ?>
<?php render_cookie_controls(); ?>
<script type="application/ld+json"><?= json_encode($organizationSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?></script>
<script type="application/ld+json"><?= json_encode($faqSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) ?></script>
<?php render_site_scripts(); ?>
</body>
</html>

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/site.php';
$site = site_settings();
$pageTitle = 'Politique de confidentialité | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
$fallbackDescription = 'Politique de confidentialité de programmetelecesoir.fr : responsable de traitement, DPO, hébergement, données traitées et droits des personnes.';
$keywords = 'politique de confidentialité, données personnelles, dpo, programmetelecesoir.fr';
?>
<!doctype html>
<html lang="fr">
<head>
<?php render_site_head($pageTitle, $fallbackDescription, $keywords, true); ?>
</head>
<body class="app-body legal-body" data-page="privacy">
<?php render_site_nav('privacy'); ?>
<main class="site-main legal-main">
<section class="legal-hero" data-track-section>
<div class="container">
<div class="legal-shell">
<nav aria-label="Fil d'Ariane" class="breadcrumb-wrap">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/">Accueil</a></li>
<li class="breadcrumb-item active" aria-current="page">Politique de confidentialité</li>
</ol>
</nav>
<span class="eyebrow mt-4">Protection des données</span>
<h1>Politique de confidentialité</h1>
<p class="lead legal-lead">Cette politique décrit les traitements de données susceptibles d'intervenir lors de la consultation de programmetelecesoir.fr, les finalités poursuivies, les durées de conservation, les destinataires et vos droits.</p>
</div>
</div>
</section>
<section class="section-block" data-track-section>
<div class="container">
<div class="section-shell legal-shell">
<div class="content-grid legal-grid">
<article class="article-panel">
<h2>1. Responsable du traitement</h2>
<p>Le responsable du traitement et DPO déclaré pour le site est <strong><?= e($site['dpo_name']) ?></strong>, domicilié au <strong><?= e($site['dpo_address']) ?></strong>, joignable au <strong><?= e($site['dpo_phone']) ?></strong>.</p>
<h2>2. Hébergement</h2>
<p>Le site est hébergé par <strong><?= e($site['host_name']) ?></strong>. L'hébergement technique implique le traitement de journaux serveur et d'informations de connexion nécessaires à la sécurité, à la disponibilité du service et à la résolution d'éventuels incidents.</p>
<h2>3. Catégories de données susceptibles d'être traitées</h2>
<p>Le site a été conçu pour limiter au strict nécessaire la collecte de données. Dans cette version, les traitements potentiels concernent principalement&nbsp;:</p>
<ul class="legal-list">
<li>les données techniques de connexion indispensables au fonctionnement du site et à la sécurité de l'infrastructure (adresse IP, user-agent, requêtes HTTP, horodatage, erreurs techniques) ;</li>
<li>vos préférences de consentement, enregistrées dans le cookie <code>ptcs_consent</code> ;</li>
<li>les informations échangées avec le prestataire du widget TV, nécessaires à l'affichage du service “en ce moment” lorsque vous consultez la page d'accueil ;</li>
<li>les préférences locales optionnelles d'interface ou le compteur local d'audience, si vous activez ces catégories dans la bannière.</li>
</ul>
<h2>4. Finalités et bases juridiques</h2>
<div class="table-responsive">
<table class="table legal-table align-middle">
<thead>
<tr>
<th>Traitement</th>
<th>Finalité</th>
<th>Base juridique</th>
</tr>
</thead>
<tbody>
<tr>
<td>Logs techniques et sécurité</td>
<td>Garantir la stabilité, la sécurité et la disponibilité du site.</td>
<td>Intérêt légitime du responsable et nécessité opérationnelle.</td>
</tr>
<tr>
<td>Cookie de consentement</td>
<td>Mémoriser vos préférences et prouver le choix exprimé.</td>
<td>Intérêt légitime / conformité en matière de gestion du consentement.</td>
</tr>
<tr>
<td>Chargement du widget TV</td>
<td>Afficher le service principal de programme TV demandé par l'utilisateur.</td>
<td>Nécessité liée au service expressément sollicité.</td>
</tr>
<tr>
<td>Personnalisation et audience locale</td>
<td>Améliorer le confort de navigation uniquement si vous l'autorisez.</td>
<td>Consentement.</td>
</tr>
</tbody>
</table>
</div>
<h2>5. Destinataires</h2>
<p>Les destinataires potentiels des données techniques sont le responsable du site, l'hébergeur <strong><?= e($site['host_name']) ?></strong> pour l'exploitation de l'infrastructure et, pour le service TV embarqué, le fournisseur du widget nécessaire à l'affichage du programme.</p>
<h2>6. Durées de conservation</h2>
<p>Le cookie de consentement est conservé 6 mois. Les préférences purement locales et facultatives sont effacées si vous désactivez la catégorie correspondante. Les durées de conservation des journaux techniques dépendent des règles d'exploitation et de sécurité de l'hébergement.</p>
</article>
<aside class="aside-panels">
<article class="info-panel">
<h3>Vos droits</h3>
<p>Vous disposez des droits d'accès, de rectification, d'effacement, de limitation, d'opposition et, le cas échéant, de portabilité. Vous pouvez également retirer votre consentement pour les options facultatives à tout moment.</p>
</article>
<article class="info-panel">
<h3>Réclamation</h3>
<p>En cas de difficulté persistante, vous pouvez contacter le DPO puis, si nécessaire, saisir l'autorité de contrôle compétente.</p>
</article>
<article class="info-panel">
<h3>Mesures de minimisation</h3>
<p>Le site ne propose ni espace membre, ni formulaire de collecte marketing, ni profilage publicitaire dans cette version initiale. L'objectif est de maintenir un traitement réduit et lisible.</p>
</article>
</aside>
</div>
</div>
</div>
</section>
</main>
<?php render_site_footer(); ?>
<?php render_cookie_controls(); ?>
<?php render_site_scripts(); ?>
</body>
</html>

119
politique-cookies.php Normal file
View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/site.php';
$site = site_settings();
$pageTitle = 'Politique de cookies | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
$fallbackDescription = 'Politique de cookies de programmetelecesoir.fr : inventaire des cookies first-party, description des préférences et informations sur le widget TV.';
$keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr';
?>
<!doctype html>
<html lang="fr">
<head>
<?php render_site_head($pageTitle, $fallbackDescription, $keywords, true); ?>
</head>
<body class="app-body legal-body" data-page="cookies">
<?php render_site_nav('cookies'); ?>
<main class="site-main legal-main">
<section class="legal-hero" data-track-section>
<div class="container">
<div class="legal-shell">
<nav aria-label="Fil d'Ariane" class="breadcrumb-wrap">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/">Accueil</a></li>
<li class="breadcrumb-item active" aria-current="page">Politique de cookies</li>
</ol>
</nav>
<span class="eyebrow mt-4">Document légal</span>
<h1>Politique de cookies de programmetelecesoir.fr</h1>
<p class="lead legal-lead">Cette politique explique de manière détaillée quels cookies et traceurs sont utilisés sur le site, à quoi ils servent, combien de temps ils sont conservés et comment vous pouvez modifier vos préférences à tout moment.</p>
</div>
</div>
</section>
<section class="section-block" data-track-section>
<div class="container">
<div class="section-shell legal-shell">
<div class="section-heading">
<span class="eyebrow">Inventaire</span>
<h2>Liste des cookies maîtrisés par le site</h2>
<p>La présente version du site limite volontairement les cookies first-party à l'essentiel. Aucun cookie publicitaire n'est mis en place par programmetelecesoir.fr.</p>
</div>
<div class="table-responsive">
<table class="table legal-table align-middle">
<thead>
<tr>
<th>Nom</th>
<th>Type</th>
<th>Finalité</th>
<th>Durée</th>
<th>Nécessaire</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ptcs_consent</code></td>
<td>Cookie first-party</td>
<td>Mémoriser vos choix de consentement (essentiels, personnalisation, audience locale) et éviter de vous redemander vos préférences à chaque visite.</td>
<td>6 mois</td>
<td>Oui</td>
</tr>
<tr>
<td>Aucun autre cookie first-party</td>
<td></td>
<td>La version actuelle ne déploie ni cookie publicitaire, ni cookie de retargeting, ni solution d'analyse tierce.</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="section-block" data-track-section>
<div class="container">
<div class="section-shell legal-shell">
<div class="content-grid legal-grid">
<article class="article-panel">
<h2>1. Pourquoi une bannière cookies sur ce site&nbsp;?</h2>
<p>La bannière permet d'expliquer clairement la présence du cookie essentiel de consentement et de vous donner la main sur les traitements optionnels disponibles dans cette première version. Le bouton <strong>“Continuer sans accepter”</strong> enregistre un réglage minimal&nbsp;: seuls les éléments essentiels restent actifs. Le bouton <strong>“Tout accepter”</strong> active les préférences optionnelles prévues. Le bouton <strong>“Enregistrer mes choix”</strong> tient compte précisément de vos toggles.</p>
<h2>2. À quoi correspondent les catégories proposées&nbsp;?</h2>
<ul class="legal-list">
<li><strong>Essentiels&nbsp;:</strong> ils assurent la mémorisation de votre choix de confidentialité et le fonctionnement minimum de l'interface de consentement. Cette catégorie ne peut pas être désactivée.</li>
<li><strong>Personnalisation&nbsp;:</strong> elle permet de mémoriser localement des préférences d'interface sur votre appareil, par exemple le rappel concernant le défilement horizontal du widget sur mobile.</li>
<li><strong>Mesure locale d'audience&nbsp;:</strong> elle active uniquement un compteur local dans votre navigateur pour démontrer l'effet du réglage. Aucun service tiers d'analyse n'est branché dans cette version.</li>
</ul>
<h2>3. Et le widget TV externe&nbsp;?</h2>
<p>Le service principal du site consiste à afficher un programme TV “en ce moment”. Le widget TV est donc traité comme un élément nécessaire à la prestation expressément attendue sur la page d'accueil. Il est chargé depuis un service tiers, <strong>tv-programme.com</strong>, afin d'afficher les grilles et contenus correspondants. Lorsqu'un service tiers est sollicité, il peut techniquement recevoir certaines informations de connexion liées à votre navigateur. Pour connaître les règles exactes applicables à ce prestataire, il convient également de consulter sa propre documentation.</p>
<h2>4. Comment modifier vos choix plus tard&nbsp;?</h2>
<p>Un bouton flottant en bas à gauche de l'écran permet de rouvrir à tout moment le centre de préférences. Les toggles sont resynchronisés avec votre choix enregistré afin de vous permettre de l'ajuster immédiatement, sans rechargement complexe ni perte de navigation.</p>
</article>
<aside class="aside-panels">
<article class="info-panel">
<h3>Durée de conservation</h3>
<p>Le cookie de consentement est conservé pendant <strong>6 mois</strong>, puis vos préférences peuvent être redemandées. Les traces locales purement optionnelles sont effacées lorsque vous désactivez la catégorie correspondante.</p>
</article>
<article class="info-panel">
<h3>Base légale</h3>
<p>Les traceurs essentiels reposent sur l'intérêt légitime et la nécessité de conserver la preuve de votre choix. Les fonctionnalités optionnelles ne sont activées qu'après votre action explicite.</p>
</article>
<article class="info-panel">
<h3>Contact DPO</h3>
<p><strong><?= e($site['dpo_name']) ?></strong><br><?= e($site['dpo_address']) ?><br>Tél. <?= e($site['dpo_phone']) ?></p>
</article>
</aside>
</div>
</div>
</div>
</section>
</main>
<?php render_site_footer(); ?>
<?php render_cookie_controls(); ?>
<?php render_site_scripts(); ?>
</body>
</html>

92
reglement.php Normal file
View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/site.php';
$site = site_settings();
$pageTitle = 'Règlement d\'utilisation | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
$fallbackDescription = 'Règlement d\'utilisation de programmetelecesoir.fr : accès au site, propriété intellectuelle, responsabilité, disponibilité et règles de consultation.';
$keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions essentielles';
?>
<!doctype html>
<html lang="fr">
<head>
<?php render_site_head($pageTitle, $fallbackDescription, $keywords, true); ?>
</head>
<body class="app-body legal-body" data-page="rules">
<?php render_site_nav('rules'); ?>
<main class="site-main legal-main">
<section class="legal-hero" data-track-section>
<div class="container">
<div class="legal-shell">
<nav aria-label="Fil d'Ariane" class="breadcrumb-wrap">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/">Accueil</a></li>
<li class="breadcrumb-item active" aria-current="page">Règlement d'utilisation</li>
</ol>
</nav>
<span class="eyebrow mt-4">Cadre d'usage</span>
<h1>Règlement d'utilisation du site</h1>
<p class="lead legal-lead">Le présent règlement encadre l'utilisation de programmetelecesoir.fr, précise l'objet du service, les responsabilités respectives, les règles de disponibilité et les bonnes pratiques applicables à la consultation du contenu.</p>
</div>
</div>
</section>
<section class="section-block" data-track-section>
<div class="container">
<div class="section-shell legal-shell">
<div class="content-grid legal-grid">
<article class="article-panel">
<h2>1. Objet du site</h2>
<p>Programmetelecesoir.fr est une page éditoriale conçue pour faciliter l'accès au programme TV ce soir et au direct en ce moment. Le site met en avant un widget externe nécessaire au service principal, complété par des contenus éditoriaux, des sections d'aide et des documents juridiques.</p>
<h2>2. Conditions d'accès</h2>
<p>L'accès au site est libre, sous réserve d'une connexion internet compatible. L'utilisateur s'engage à ne pas détourner l'usage du site, à ne pas perturber son bon fonctionnement et à respecter les lois et règlements applicables lors de sa navigation.</p>
<h2>3. Propriété intellectuelle</h2>
<p>Les textes, compositions, éléments graphiques, structures de mise en page et contenus créés pour programmetelecesoir.fr sont protégés par les règles applicables à la propriété intellectuelle. Les marques, logos, titres d'émissions, programmes ou contenus tiers restent la propriété de leurs ayants droit respectifs.</p>
<h2>4. Services tiers et sources externes</h2>
<p>Le site s'appuie sur un widget TV embarqué fourni par un service externe afin d'afficher l'information “en ce moment”. La disponibilité, l'exactitude et l'éventuelle évolution de ce service tiers relèvent également de son éditeur. Le site présente clairement la source du widget au sein de la page d'accueil.</p>
<h2>5. Disponibilité et maintenance</h2>
<p>Le responsable s'efforce de maintenir le site accessible et fonctionnel. Des interruptions temporaires peuvent intervenir pour maintenance, mise à jour, incident technique, saturation du réseau ou indisponibilité du fournisseur tiers. Aucune garantie de disponibilité continue absolue n'est donnée.</p>
<h2>6. Responsabilité</h2>
<p>Le site a une vocation d'information et d'accès rapide. L'utilisateur reste libre de vérifier le programme final auprès des diffuseurs concernés. Le responsable ne pourra être tenu responsable d'un changement de grille, d'une annulation de programme, d'une erreur provenant d'une source tierce ou d'une indisponibilité ponctuelle indépendante de sa volonté.</p>
<h2>7. Données personnelles et cookies</h2>
<p>L'utilisation du site emporte l'application des documents dédiés à la confidentialité et aux cookies. La bannière de consentement, le bouton flottant de rappel et les pages légales sont conçus pour permettre à l'utilisateur de comprendre et d'ajuster ses choix simplement.</p>
<h2>8. Contact</h2>
<p>Pour toute question relative au fonctionnement du site, à la confidentialité ou à l'exercice de vos droits, vous pouvez contacter le DPO déclaré&nbsp;: <strong><?= e($site['dpo_name']) ?></strong>, <?= e($site['dpo_address']) ?>, tél. <?= e($site['dpo_phone']) ?>.</p>
<h2>9. Hébergement</h2>
<p>L'hébergement du site est assuré par <strong><?= e($site['host_name']) ?></strong>.</p>
<h2>10. Droit applicable</h2>
<p>Le présent règlement est rédigé en français et s'interprète conformément au droit applicable au site et à son lieu d'exploitation. En cas de litige, une résolution amiable sera privilégiée avant toute démarche contentieuse.</p>
</article>
<aside class="aside-panels">
<article class="info-panel">
<h3>Usage recommandé</h3>
<p>Utilisez les ancres de navigation pour atteindre rapidement le widget, la FAQ et les documents légaux sans perdre le fil de lecture.</p>
</article>
<article class="info-panel">
<h3>Vie privée</h3>
<p>Le bouton flottant de rappel des cookies reste affiché en bas à gauche afin que le réglage soit toujours disponible.</p>
</article>
<article class="info-panel">
<h3>Transparence</h3>
<p>La source du widget TV est visible dans la page principale et les informations de contact du DPO sont répétées dans les documents juridiques.</p>
</article>
</aside>
</div>
</div>
</div>
</section>
</main>
<?php render_site_footer(); ?>
<?php render_cookie_controls(); ?>
<?php render_site_scripts(); ?>
</body>
</html>

4
robots.txt Normal file
View File

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://programmetelecesoir.fr/sitemap.xml

262
site.php Normal file
View File

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
@date_default_timezone_set('Europe/Paris');
function site_asset_version(): string
{
static $version = null;
if ($version !== null) {
return $version;
}
$paths = [
__DIR__ . '/assets/css/custom.css',
__DIR__ . '/assets/js/main.js',
];
$mtime = 0;
foreach ($paths as $path) {
if (is_file($path)) {
$mtime = max($mtime, (int) filemtime($path));
}
}
$version = (string) ($mtime ?: time());
return $version;
}
function site_settings(): array
{
static $settings = null;
if ($settings !== null) {
return $settings;
}
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'programmetelecesoir.fr';
$settings = [
'domain' => 'programmetelecesoir.fr',
'project_name' => $_SERVER['PROJECT_NAME'] ?? 'programmetelecesoir.fr',
'project_description' => $_SERVER['PROJECT_DESCRIPTION'] ?? '',
'project_image_url' => $_SERVER['PROJECT_IMAGE_URL'] ?? '',
'base_url' => $scheme . '://' . $host,
'asset_version' => site_asset_version(),
'owner_name' => 'M LORENTE CHRISTOPHE',
'dpo_name' => 'M LORENTE CHRISTOPHE',
'dpo_address' => '7 rue Lucien Deneau 28300 Mainvilliers',
'dpo_phone' => '06 58 22 59 16',
'host_name' => 'FLATLOGIC.COM',
];
return $settings;
}
function e(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function site_current_path(): string
{
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$path = strtok($requestUri, '?');
return $path !== false && $path !== '' ? $path : '/';
}
function site_asset_url(string $relativePath): string
{
return $relativePath . '?v=' . rawurlencode(site_asset_version());
}
function render_site_head(string $pageTitle, string $fallbackDescription, string $keywords = '', bool $noindex = false): void
{
$site = site_settings();
$projectDescription = $site['project_description'];
$projectImageUrl = $site['project_image_url'];
$canonical = $site['base_url'] . site_current_path();
?>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= e($pageTitle) ?></title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= e($projectDescription) ?>" />
<meta property="og:description" content="<?= e($projectDescription) ?>" />
<meta property="twitter:description" content="<?= e($projectDescription) ?>" />
<?php else: ?>
<meta name="description" content="<?= e($fallbackDescription) ?>" />
<meta property="og:description" content="<?= e($fallbackDescription) ?>" />
<meta property="twitter:description" content="<?= e($fallbackDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= e($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>" />
<?php endif; ?>
<?php if ($keywords !== ''): ?>
<meta name="keywords" content="<?= e($keywords) ?>" />
<?php endif; ?>
<meta name="author" content="<?= e($site['owner_name']) ?>" />
<meta name="theme-color" content="#111827" />
<meta property="og:title" content="<?= e($pageTitle) ?>" />
<meta property="og:type" content="website" />
<meta property="og:url" content="<?= e($canonical) ?>" />
<meta property="twitter:card" content="summary_large_image" />
<link rel="canonical" href="<?= e($canonical) ?>" />
<?php if ($noindex): ?>
<meta name="robots" content="noindex, nofollow" />
<?php else: ?>
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<?php endif; ?>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="<?= e(site_asset_url('assets/css/custom.css')) ?>">
<?php
}
function nav_link(string $label, string $href, bool $active = false, bool $sectionLink = false): string
{
$className = 'nav-link';
if ($active) {
$className .= ' active';
}
$dataAttr = $sectionLink ? ' data-section-link="1"' : '';
$current = $active ? ' aria-current="page"' : '';
return '<a class="' . e($className) . '" href="' . e($href) . '"' . $current . $dataAttr . '>' . e($label) . '</a>';
}
function render_site_nav(string $current = 'home'): void
{
?>
<header class="site-header">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container">
<a class="navbar-brand" href="/">programmetelecesoir.fr</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Afficher la navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNavbar">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><?= nav_link('En ce moment', '/#widget-section', $current === 'home', true) ?></li>
<li class="nav-item"><?= nav_link('Guide TV', '/#guide', false, true) ?></li>
<li class="nav-item"><?= nav_link('Chaînes', '/#chaines', false, true) ?></li>
<li class="nav-item"><?= nav_link('FAQ', '/#faq', false, true) ?></li>
<li class="nav-item"><?= nav_link('Cookies', '/politique-cookies.php', $current === 'cookies') ?></li>
<li class="nav-item"><?= nav_link('Confidentialité', '/politique-confidentialite.php', $current === 'privacy') ?></li>
<li class="nav-item"><?= nav_link('Règlement', '/reglement.php', $current === 'rules') ?></li>
</ul>
</div>
</div>
</nav>
</header>
<?php
}
function render_site_footer(): void
{
$site = site_settings();
?>
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div>
<div class="footer-brand">programmetelecesoir.fr</div>
<p class="footer-copy">Guide éditorial et page d'accès rapide au programme TV ce soir, au direct en ce moment et aux principales chaînes TNT.</p>
</div>
<div>
<div class="footer-title">Documents</div>
<ul class="footer-links list-unstyled mb-0">
<li><a href="/politique-cookies.php">Politique de cookies</a></li>
<li><a href="/politique-confidentialite.php">Politique de confidentialité</a></li>
<li><a href="/reglement.php">Règlement d'utilisation</a></li>
</ul>
</div>
<div>
<div class="footer-title">DPO & contact</div>
<address class="footer-copy mb-0">
<?= e($site['dpo_name']) ?><br>
<?= e($site['dpo_address']) ?><br>
Tél. <?= e($site['dpo_phone']) ?>
</address>
</div>
<div>
<div class="footer-title">Hébergement</div>
<p class="footer-copy mb-0">Hébergeur déclaré : <?= e($site['host_name']) ?>.</p>
<button type="button" class="link-button mt-2" id="footer-cookie-settings">Revoir mon choix cookies</button>
</div>
</div>
</div>
</footer>
<?php
}
function render_cookie_controls(): void
{
?>
<div class="cookie-floating">
<button type="button" class="cookie-reopen" id="cookie-reopen" aria-controls="cookie-banner" aria-expanded="false">
<span class="cookie-reopen__dot" aria-hidden="true"></span>
<span id="cookie-reopen-label">Cookies : essentiels</span>
</button>
</div>
<div class="cookie-banner" id="cookie-banner" hidden>
<div class="cookie-banner__top">
<div>
<p class="cookie-banner__eyebrow mb-2">Préférences cookies</p>
<h2 class="cookie-banner__title">Gérez votre confidentialité</h2>
</div>
</div>
<p class="cookie-banner__intro">Le widget TV nécessaire au service reste actif pour afficher le programme en temps réel. Les traceurs optionnels sont désactivés par défaut tant que vous n'avez pas choisi.</p>
<div class="cookie-banner__toggles">
<div class="cookie-switch-row">
<div>
<strong>Essentiels</strong>
<p>Conservent votre choix de consentement et la sécurité minimale du site.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-essential" checked disabled>
</div>
</div>
<div class="cookie-switch-row">
<div>
<label class="cookie-switch-label" for="cookie-personalization">Personnalisation</label>
<p>Mémorise vos préférences d'interface sur cet appareil, comme le rappel de lecture mobile.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-personalization" data-consent-control="personalization">
</div>
</div>
<div class="cookie-switch-row">
<div>
<label class="cookie-switch-label" for="cookie-audience">Mesure locale d'audience</label>
<p>Active un simple compteur local dans votre navigateur, sans service tiers ni publicité.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-audience" data-consent-control="audience">
</div>
</div>
</div>
<div class="cookie-banner__actions">
<button type="button" class="btn btn-light btn-refined" data-cookie-action="reject">Continuer sans accepter</button>
<button type="button" class="btn btn-outline-dark btn-refined" data-cookie-action="save">Enregistrer mes choix</button>
<button type="button" class="btn btn-dark btn-refined" data-cookie-action="accept">Tout accepter</button>
</div>
<div class="cookie-banner__links">
<a href="/politique-cookies.php">Voir la politique de cookies</a>
<span aria-hidden="true"></span>
<a href="/politique-confidentialite.php">Confidentialité</a>
</div>
</div>
<div class="toast-stack" id="toast-stack" aria-live="polite" aria-atomic="true"></div>
<?php
}
function render_site_scripts(): void
{
?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="<?= e(site_asset_url('assets/js/main.js')) ?>" defer></script>
<?php
}

8
sitemap.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://programmetelecesoir.fr/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>