mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
feat: single entry point routing — convert to front controller pattern
- Create app/public/index.php as front controller (bootstrap + Dispatcher) - Rewrite app/router.php for PHP dev server → all non-asset requests to index.php - Update Dispatcher to render full page layouts (head+header+view+footer) - Move public view templates into templates/public/ (home, search, tfe, about, repertoire) - Delete dead direct-access public/*.php files (apropos, search, tfe, licence, repertoire) - Add clean URL routes to Dispatcher (/search, /tfe, /repertoire, /apropos, /licence, /media) - Remove .php extensions from all internal links (header, views, templates, URLs) - Update OG tags in controllers to use clean URLs - Update nginx posterg.conf → front-controller try_files pattern, block direct .php access - Update header.php and search-bar.php form actions to clean URLs - Switch AboutController nav key from 'nav' to 'currentNav' for consistency
This commit is contained in:
34
TODO.md
34
TODO.md
@@ -38,16 +38,34 @@
|
||||
- [ ] Update Dispatcher to handle all routes directly (no require APP_ROOT/public/*.php)
|
||||
|
||||
### Phase 2: Single entry point
|
||||
- [ ] Create app/public/index.php as front controller
|
||||
- [ ] Bootstrap + Dispatcher invocation
|
||||
- [ ] Remove direct-access public/*.php (index.php, search.php, tfe.php, apropos.php, licence.php)
|
||||
- [ ] Rename old entry points so they can't be hit directly (e.g., prefix with underscore or delete)
|
||||
- [x] Create app/public/index.php as front controller
|
||||
- [x] Move bootstrap logic into entry point (bootstrap.php stays for admin)
|
||||
- [x] Load and invoke Dispatcher
|
||||
- [x] Move old public/*.php views into templates/public/
|
||||
- [x] search.php → templates/public/search.php
|
||||
- [x] tfe.php → templates/public/tfe.php
|
||||
- [x] apropos.php → templates/public/about.php
|
||||
- [x] repertoire.php → templates/public/repertoire.php
|
||||
- [x] Delete old direct-access public/*.php files
|
||||
- [x] Delete public/index.php (replaced by front controller)
|
||||
- [x] Delete public/search.php
|
||||
- [x] Delete public/tfe.php
|
||||
- [x] Delete public/apropos.php
|
||||
- [x] Delete public/licence.php
|
||||
- [x] Delete public/repertoire.php
|
||||
- [x] Update Dispatcher.render to use templates/public/ views
|
||||
- [x] Update Dispatcher to render full pages (head + header + view + footer) instead of requiring bootstrap
|
||||
- [x] Ensure admin/index.php bootstraps its own path (not affected by front controller)
|
||||
|
||||
### Phase 3: Server config
|
||||
- [ ] Update router.php — route all PHP requests to Dispatcher
|
||||
- [ ] Update nginx config — point all public routes to index.php via try_files
|
||||
- [ ] Replace per-file `location ~ \.php$` with front-controller pattern
|
||||
|
||||
### Phase 4: Cleanup
|
||||
- [ ] Delete app/public/live-reload.php (already handled by LiveReloadController)
|
||||
- [ ] Test all routes (/, search.php, tfe, repertoire, apropos, licence, media, live-reload)
|
||||
- [x] Clean URL updates
|
||||
- [x] Remove .php from all internal links (header, views, controllers)
|
||||
- [x] Add clean routes to Dispatcher (/search, /tfe, /media)
|
||||
- [x] Update og:url tags in controllers to use clean URLs
|
||||
- [x] Update TfeController redirect to /
|
||||
- [x] Update header.php action URLs
|
||||
- [x] Commit current state
|
||||
- [ ] Test all routes (/, /search, /tfe, /repertoire, /apropos, /licence, /media, /live-reload)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
|
||||
$currentNav = 'apropos';
|
||||
define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg – École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciennes étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans.");
|
||||
|
||||
/**
|
||||
* Render a comma-separated list of entries with links.
|
||||
* Entries joined with comma, last two joined with " & ".
|
||||
*/
|
||||
function renderEntries(array $entries): string {
|
||||
if (empty($entries)) return '';
|
||||
$parts = [];
|
||||
foreach ($entries as $e) {
|
||||
$text = htmlspecialchars($e['text'] ?? '');
|
||||
$url = $e['url'] ?? '';
|
||||
if (!empty($url)) {
|
||||
$parts[] = '<span class="apropos-entry"><a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener">' . $text . '</a></span>';
|
||||
} else {
|
||||
$parts[] = '<span class="apropos-entry">' . $text . '</span>';
|
||||
}
|
||||
}
|
||||
$count = count($parts);
|
||||
if ($count === 1) return $parts[0];
|
||||
// Join all but last two with ", ", join last two with " & "
|
||||
$prefix = implode(', ', array_slice($parts, 0, $count - 2));
|
||||
$suffix = implode(' & ', array_slice($parts, -2));
|
||||
return $prefix !== '' ? $prefix . ', ' . $suffix : $suffix;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Intro text from pages table
|
||||
$aboutPage = $db->getPage('about');
|
||||
$rawContent = $aboutPage ? $aboutPage['content'] : '';
|
||||
if (empty(trim($rawContent)) || trim($rawContent) === 'Contenu à venir') {
|
||||
$rawContent = APROPOS_STATIC_CONTENT;
|
||||
}
|
||||
|
||||
// Contacts and credits from apropos_contents table
|
||||
$contacts = $db->getAproposContent('contacts');
|
||||
$credits = $db->getAproposContent('credits');
|
||||
|
||||
// Apply defaults if DB returns empty
|
||||
$contacts = is_array($contacts) && !empty($contacts) ? $contacts : null;
|
||||
$credits = is_array($credits) && !empty($credits) ? $credits : null;
|
||||
} catch (Exception $e) {
|
||||
error_log("Error loading about page: " . $e->getMessage());
|
||||
$rawContent = APROPOS_STATIC_CONTENT;
|
||||
$contacts = null;
|
||||
$credits = null;
|
||||
}
|
||||
|
||||
$pd = new Parsedown();
|
||||
$pd->setSafeMode(true);
|
||||
$aboutHtml = $pd->text($rawContent);
|
||||
|
||||
$pageTitle = 'À Propos – Posterg';
|
||||
$metaDescription = 'À propos de Posterg, le répertoire des mémoires de fin d\'études de l\'erg – École de Recherches Graphiques de Bruxelles.';
|
||||
$ogTags = [
|
||||
'type' => 'website',
|
||||
'title' => $pageTitle,
|
||||
'description' => $metaDescription,
|
||||
'url' => 'https://posterg.erg.be/apropos.php',
|
||||
'site_name' => 'Posterg – ERG',
|
||||
];
|
||||
$extraCss = ['/assets/css/apropos.css'];
|
||||
$bodyClass = 'apropos-body';
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main class="apropos-main" id="main-content">
|
||||
<div class="apropos-layout">
|
||||
|
||||
<!-- LEFT: sticky table of contents -->
|
||||
<nav class="apropos-toc" aria-label="Sections de la page">
|
||||
<p class="apropos-toc-label">Parties</p>
|
||||
<ul>
|
||||
<li><a href="#apropos-intro">À propos</a></li>
|
||||
<?php if (!empty($contacts)): ?>
|
||||
<li><a href="#apropos-contacts">Contacts</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($credits)): ?>
|
||||
<li><a href="#apropos-credits">Crédits</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<div class="apropos-toc-erg">
|
||||
<a href="https://erg.be" target="_blank" rel="noopener">
|
||||
Site de l'erg ↗
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MIDDLE: main prose + sections -->
|
||||
<div class="apropos-content">
|
||||
|
||||
<!-- Intro text from DB -->
|
||||
<section class="apropos-section" id="apropos-intro">
|
||||
<div class="prose">
|
||||
<?= $aboutHtml ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($contacts)): ?>
|
||||
<!-- Contacts section -->
|
||||
<section class="apropos-section" id="apropos-contacts">
|
||||
<h2 class="apropos-section-title">Contacts</h2>
|
||||
<div class="apropos-contacts-grid">
|
||||
<?php foreach ($contacts as $group): ?>
|
||||
<address class="apropos-contact-card">
|
||||
<?= renderEntries($group['entries'] ?? []) ?>
|
||||
<?php if (!empty($group['role'])): ?>
|
||||
<span><?= htmlspecialchars($group['role']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Show the email from the first entry (or any that has one on separate line)
|
||||
$emails = array_filter(array_column($group['entries'] ?? [], 'email'), fn($e) => !empty($e));
|
||||
foreach ($emails as $email):
|
||||
?>
|
||||
<a href="mailto:<?= htmlspecialchars($email) ?>"><?= htmlspecialchars($email) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</address>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($credits)): ?>
|
||||
<!-- Credits section -->
|
||||
<section class="apropos-section" id="apropos-credits">
|
||||
<h2 class="apropos-section-title">Crédits</h2>
|
||||
<dl class="apropos-credits-list">
|
||||
<?php foreach ($credits as $group): ?>
|
||||
<div class="apropos-credit-row">
|
||||
<dt><?= htmlspecialchars($group['label']) ?></dt>
|
||||
<dd><?= renderEntries($group['entries'] ?? []) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</dl>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
@@ -1,70 +1,12 @@
|
||||
<?php
|
||||
// Load configuration
|
||||
/**
|
||||
* Front Controller
|
||||
*
|
||||
* Single entry point for all public-facing page requests.
|
||||
* Bootstraps the application and delegates routing to the Dispatcher.
|
||||
*/
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Controllers/HomeController.php';
|
||||
require_once APP_ROOT . '/src/Dispatcher.php';
|
||||
|
||||
$controller = HomeController::create();
|
||||
$vars = $controller->handle();
|
||||
extract($vars);
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<?php if ($year): ?>
|
||||
<p class="filter-info" role="status">
|
||||
Année : <?= htmlspecialchars($year) ?>
|
||||
<a href="index.php" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
|
||||
</p>
|
||||
<?php elseif ($isDefaultView): ?>
|
||||
<p class="home-section-label" role="status">Publication récente</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="home-main" id="main-content">
|
||||
<h1 class="sr-only">Mémoires de l'ERG</h1>
|
||||
<ul class="cards-container">
|
||||
<?php foreach ($itemsToLoad as $item): ?>
|
||||
<li class="card">
|
||||
<a href="tfe.php?id=<?= (int)$item["id"] ?>">
|
||||
<?php
|
||||
// Resolve thumbnail: banner_path → cover file → gradient placeholder
|
||||
$thumb = null;
|
||||
|
||||
// 1. Banner path (dedicated home thumbnail)
|
||||
if (!empty($item['banner_path'])) {
|
||||
$thumb = $item['banner_path'];
|
||||
}
|
||||
|
||||
// 2. Cover image from thesis_files (batch-loaded above)
|
||||
if (!$thumb && isset($coverMap[$item['id']])) {
|
||||
$thumb = $coverMap[$item['id']];
|
||||
}
|
||||
|
||||
// 3. Fall through to gradient
|
||||
?>
|
||||
<?php if ($thumb): ?>
|
||||
<figure>
|
||||
<img src="/media.php?path=<?= urlencode($thumb) ?>"
|
||||
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
|
||||
loading="lazy">
|
||||
</figure>
|
||||
<?php else: ?>
|
||||
<div class="card__media--gradient"
|
||||
aria-hidden="true">
|
||||
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
||||
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<p><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> – <?php endif; ?><?= htmlspecialchars($item["title"]) ?><?php if (!empty($item['year'])): ?><span class="sr-only">, <?= (int)$item['year'] ?></span><?php endif; ?></p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (empty($itemsToLoad)): ?>
|
||||
<li class="cards-empty">Aucun mémoire trouvé.</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
|
||||
</main>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
$dispatcher = new Dispatcher();
|
||||
$dispatcher->dispatch();
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
|
||||
$currentNav = 'licence';
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$dbPage = $db->getPage('licenses');
|
||||
$content = $dbPage ? $dbPage['content'] : '';
|
||||
$licencePageTitle = $dbPage ? $dbPage['title'] : 'Licences';
|
||||
} catch (Exception $e) {
|
||||
error_log("Error loading licence page: " . $e->getMessage());
|
||||
$content = '';
|
||||
$licencePageTitle = 'Licences';
|
||||
}
|
||||
|
||||
$pd = new Parsedown();
|
||||
$pd->setSafeMode(true);
|
||||
$html = $pd->text($content);
|
||||
|
||||
$pageTitle = $licencePageTitle . ' – Posterg';
|
||||
$metaDescription = 'Informations sur les licences d\'utilisation des mémoires publiés sur Posterg, le répertoire des TFE de l\'erg.';
|
||||
$ogTags = [
|
||||
'type' => 'website',
|
||||
'title' => $pageTitle,
|
||||
'description' => $metaDescription,
|
||||
'url' => 'https://posterg.erg.be/licence.php',
|
||||
'site_name' => 'Posterg – ERG',
|
||||
];
|
||||
$extraCss = ['/assets/css/apropos.css'];
|
||||
$bodyClass = 'apropos-body';
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main class="apropos-main" id="main-content">
|
||||
<div class="prose apropos-single">
|
||||
<?php if (!empty(trim($content))): ?>
|
||||
<?= $html ?>
|
||||
<?php else: ?>
|
||||
<p>Contenu à venir.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Controllers/SearchController.php';
|
||||
|
||||
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
|
||||
$ctrl = SearchController::create();
|
||||
|
||||
// Collect all view variables for the répertoire index page
|
||||
extract($ctrl->handleRepertoire());
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main class="search-main" id="main-content">
|
||||
<h1 class="sr-only">Répertoire</h1>
|
||||
<span id="rep-indicator" class="rep-indicator htmx-indicator" aria-hidden="true"></span>
|
||||
<?php include APP_ROOT . '/templates/partials/repertoire-index.php'; ?>
|
||||
</main>
|
||||
<script src="/assets/js/htmx.min.js"></script>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Controllers/SearchController.php';
|
||||
|
||||
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
|
||||
$ctrl = SearchController::create();
|
||||
|
||||
// Collect all view variables for the search results page
|
||||
extract($ctrl->handleSearch());
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<?php if ($validationError): ?>
|
||||
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter controls -->
|
||||
<form class="search-controls" method="GET" action="search.php">
|
||||
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
|
||||
|
||||
<label class="search-filter-label" for="filter-year">Année
|
||||
<select class="search-filter-select" name="year" id="filter-year">
|
||||
<option value="">Toutes</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
|
||||
<?= (int)$y ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="search-filter-label" for="filter-orientation">Orientation
|
||||
<select class="search-filter-select" name="orientation" id="filter-orientation">
|
||||
<option value="">Toutes</option>
|
||||
<?php foreach ($orientations as $o): ?>
|
||||
<option value="<?= htmlspecialchars($o['name']) ?>"
|
||||
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($o['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="search-filter-label" for="filter-ap">AP
|
||||
<select class="search-filter-select" name="ap_program" id="filter-ap">
|
||||
<option value="">Tous</option>
|
||||
<?php foreach ($apPrograms as $ap): ?>
|
||||
<option value="<?= htmlspecialchars($ap['name']) ?>"
|
||||
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($ap['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="search-apply-btn">Filtrer</button>
|
||||
<a href="search.php?query=<?= urlencode($_GET['query'] ?? '') ?>" class="search-reset-link">Réinitialiser</a>
|
||||
</form>
|
||||
|
||||
<main class="search-main" id="main-content">
|
||||
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<ul class="results-grid">
|
||||
<?php foreach ($results as $item): ?>
|
||||
<li><a href="tfe.php?id=<?= (int)$item['id'] ?>" class="result-card">
|
||||
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
||||
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
|
||||
</a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="search-empty">Aucun résultat pour cette recherche.</p>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
@@ -1,263 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Controllers/TfeController.php';
|
||||
|
||||
// Build controller (loads thesis, enforces visibility, builds OG tags; redirects on 404)
|
||||
$ctrl = TfeController::create();
|
||||
|
||||
// Collect all view variables
|
||||
extract($ctrl->handle());
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main class="tfe-main" id="main-content">
|
||||
<article class="tfe-layout">
|
||||
|
||||
<!-- LEFT: info — article header -->
|
||||
<header class="tfe-left">
|
||||
<!-- Author above title -->
|
||||
<p class="tfe-author"><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu') ?></p>
|
||||
|
||||
<h1 class="tfe-title">
|
||||
<?= htmlspecialchars($data['title']) ?>
|
||||
<?php if (!empty($data['subtitle'])): ?>
|
||||
– <?= htmlspecialchars($data['subtitle']) ?>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
|
||||
<dl>
|
||||
<?php if (!empty($data['orientation'])): ?>
|
||||
<div>
|
||||
<dt>Orientation :</dt>
|
||||
<dd><a href="/repertoire.php?or[]=<?= urlencode($data['orientation']) ?>"><?= htmlspecialchars($data['orientation']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['ap_program'])): ?>
|
||||
<div>
|
||||
<dt>Atelier pluridisciplinaire :</dt>
|
||||
<dd><a href="/repertoire.php?ap[]=<?= urlencode($data['ap_program']) ?>"><?= htmlspecialchars($data['ap_program']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['year'])): ?>
|
||||
<div>
|
||||
<dt>Date :</dt>
|
||||
<dd><a href="/repertoire.php?fy[]=<?= urlencode($data['year']) ?>"><?= htmlspecialchars($data['year']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['languages'])): ?>
|
||||
<div>
|
||||
<dt>Langue :</dt>
|
||||
<dd><?php
|
||||
$langs = array_map('trim', explode(',', $data['languages']));
|
||||
$langLinks = array_map(fn($l) => '<a href="/search.php?query=' . urlencode($l) . '">' . htmlspecialchars($l) . '</a>', $langs);
|
||||
echo implode(', ', $langLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['formats'])): ?>
|
||||
<div>
|
||||
<dt>Format :</dt>
|
||||
<dd><?php
|
||||
$fmts = array_map('trim', explode(',', $data['formats']));
|
||||
$fmtLinks = array_map(fn($f) => '<a href="/search.php?query=' . urlencode($f) . '">' . htmlspecialchars($f) . '</a>', $fmts);
|
||||
echo implode(', ', $fmtLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['file_size_info'])): ?>
|
||||
<div>
|
||||
<dt>Durée :</dt>
|
||||
<dd><?= htmlspecialchars($data['file_size_info']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['keywords'])): ?>
|
||||
<div>
|
||||
<dt>Mots-clés :</dt>
|
||||
<dd><?php
|
||||
$kws = array_map('trim', explode(',', $data['keywords']));
|
||||
$kwLinks = array_map(fn($k) => '<a href="/repertoire.php?kw[]=' . urlencode($k) . '">' . htmlspecialchars($k) . '</a>', $kws);
|
||||
echo implode(', ', $kwLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($promoteursInternes)): ?>
|
||||
<div>
|
||||
<dt>Promoteur·ice interne :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursInternes);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($promoteursExternes)): ?>
|
||||
<div>
|
||||
<dt>Promoteur·ice externe :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursExternes);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($juryPresidents)): ?>
|
||||
<div>
|
||||
<dt>Président·e du jury :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryPresidents);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($juryLecteurs)): ?>
|
||||
<div>
|
||||
<dt>Lecteur·ices :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryLecteurs);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['access_type'])): ?>
|
||||
<div>
|
||||
<dt>Accès :</dt>
|
||||
<dd><?= htmlspecialchars($data['access_type']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['license_type'])): ?>
|
||||
<div>
|
||||
<dt>Licence :</dt>
|
||||
<dd><?= htmlspecialchars($data['license_type']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['context_note'])): ?>
|
||||
<div class="tfe-meta-note">
|
||||
<dt>Note :</dt>
|
||||
<dd class="tfe-note-value"><?= nl2br(htmlspecialchars($data['context_note'])) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['author_email']) && !empty($data['author_show_contact'])): ?>
|
||||
<div>
|
||||
<dt>Contact :</dt>
|
||||
<dd>
|
||||
<?php
|
||||
$_contact = $data['author_email'];
|
||||
$_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false;
|
||||
$_isEmail = !$_isUrl && str_contains($_contact, '@');
|
||||
if ($_isUrl):
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($_contact) ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars(preg_replace('#^https?://#i', '', rtrim($_contact, '/'))) ?>
|
||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||
</a>
|
||||
<?php elseif ($_isEmail): ?>
|
||||
<a href="mailto:<?= htmlspecialchars($_contact) ?>"><?= htmlspecialchars($_contact) ?></a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($_contact) ?>
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['baiu_link'])): ?>
|
||||
<?php
|
||||
$_baiuHref = htmlspecialchars($data['baiu_link']);
|
||||
$_baiuLabel = preg_replace('#^https?://#i', '', rtrim($data['baiu_link'], '/'));
|
||||
?>
|
||||
<div>
|
||||
<dt>Lien :</dt>
|
||||
<dd>
|
||||
<a href="<?= $_baiuHref ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($_baiuLabel) ?>
|
||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||
</a>
|
||||
|
||||
</dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
|
||||
<?php if (!empty($data['synopsis'])): ?>
|
||||
<p class="tfe-synopsis-text">
|
||||
<?= nl2br(htmlspecialchars($data['synopsis'])) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<!-- RIGHT: media — supplementary aside -->
|
||||
<aside class="tfe-right">
|
||||
<?php
|
||||
// $isInterdit and $captionFiles are resolved by TfeController::handle()
|
||||
$_videoIndex = 0;
|
||||
?>
|
||||
<?php if ($isInterdit): ?>
|
||||
<p class="tfe-restricted">
|
||||
Ce TFE n'est pas disponible en ligne.
|
||||
</p>
|
||||
<?php elseif (!empty($data['files'])): ?>
|
||||
<?php foreach ($data['files'] as $file): ?>
|
||||
<?php
|
||||
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
|
||||
// VTT caption files are consumed inline by <video>; skip standalone rendering.
|
||||
if ($ext === 'vtt') continue;
|
||||
?>
|
||||
<figure>
|
||||
<?php if ($ext === 'pdf'): ?>
|
||||
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
|
||||
type="application/pdf" width="100%" height="700px">
|
||||
<p class="tfe-pdf-fallback">
|
||||
<a href="/media.php?path=<?= urlencode($file['file_path']) ?>&download=1">
|
||||
Télécharger le PDF
|
||||
</a>
|
||||
</p>
|
||||
<?php elseif (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp'])): ?>
|
||||
<img src="/media.php?path=<?= urlencode($file['file_path']) ?>"
|
||||
alt="<?= htmlspecialchars(
|
||||
!empty($file['description'])
|
||||
? $file['description']
|
||||
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
|
||||
) ?>">
|
||||
<?php elseif ($ext === 'mp4'): ?>
|
||||
<?php
|
||||
// Pair this video with the N-th VTT file (if one was uploaded).
|
||||
$_vttPath = $captionFiles[$_videoIndex] ?? null;
|
||||
$_videoIndex++;
|
||||
?>
|
||||
<video width="100%" controls>
|
||||
<source src="/media.php?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
|
||||
<?php if ($_vttPath): ?>
|
||||
<track kind="captions"
|
||||
src="/media.php?path=<?= urlencode($_vttPath) ?>"
|
||||
srclang="fr"
|
||||
label="Sous-titres"
|
||||
default>
|
||||
<?php endif; ?>
|
||||
</video>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($file['description'])): ?>
|
||||
<figcaption><?= htmlspecialchars($file['description']) ?></figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="tfe-no-files">Aucun fichier disponible pour ce TFE.</p>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||
@@ -2,25 +2,19 @@
|
||||
/**
|
||||
* Router script for PHP built-in development server (php -S).
|
||||
*
|
||||
* Routes /partage/<slug> to public/partage/index.php, since the built-in
|
||||
* server has no URL rewriting like nginx's try_files.
|
||||
* All non-asset requests go through the front controller (public/index.php).
|
||||
* Static files (CSS, JS, images, fonts) are served directly.
|
||||
*/
|
||||
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
|
||||
// Route /partage/<slug> and /partage/<slug>/<action> to the partage entry
|
||||
if (preg_match('#^/partage(/.*)?$#', $uri)) {
|
||||
$_SERVER['SCRIPT_NAME'] = '/partage/index.php';
|
||||
require __DIR__ . '/public/partage/index.php';
|
||||
return true;
|
||||
// Static assets: let the dev server handle them directly
|
||||
$ext = pathinfo($uri, PATHINFO_EXTENSION);
|
||||
if (in_array($ext, ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'map'], true)) {
|
||||
return false; // serve directly
|
||||
}
|
||||
|
||||
// Route /tfe/<...> to tfe.php
|
||||
if (preg_match('#^/tfe(/.*)?$#', $uri)) {
|
||||
$_SERVER['SCRIPT_NAME'] = '/tfe.php';
|
||||
require __DIR__ . '/public/tfe.php';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default: serve static files if they exist
|
||||
return false;
|
||||
// Send everything else through the front controller
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
require __DIR__ . '/public/index.php';
|
||||
return true;
|
||||
|
||||
@@ -30,7 +30,7 @@ class AboutController {
|
||||
$pd->setSafeMode(true);
|
||||
|
||||
return [
|
||||
'nav' => 'apropos',
|
||||
'currentNav' => 'apropos',
|
||||
'aboutHtml' => $pd->text($rawContent),
|
||||
'contacts' => $contacts,
|
||||
'credits' => $credits,
|
||||
|
||||
@@ -124,7 +124,7 @@ class SearchController
|
||||
'type' => 'website',
|
||||
'title' => 'Recherche – Posterg',
|
||||
'description' => "Résultats de recherche dans le répertoire des TFE de l'erg.",
|
||||
'url' => 'https://posterg.erg.be/search.php',
|
||||
'url' => 'https://posterg.erg.be/search',
|
||||
'site_name' => 'Posterg – ERG',
|
||||
],
|
||||
'currentNav' => 'repertoire',
|
||||
@@ -174,7 +174,7 @@ class SearchController
|
||||
'type' => 'website',
|
||||
'title' => 'Répertoire – Posterg',
|
||||
'description' => "Parcourez le répertoire des mémoires de fin d'études (TFE) de l'erg – École de Recherches Graphiques de Bruxelles.",
|
||||
'url' => 'https://posterg.erg.be/repertoire.php',
|
||||
'url' => 'https://posterg.erg.be/repertoire',
|
||||
'site_name' => 'Posterg – ERG',
|
||||
],
|
||||
'currentNav' => 'repertoire',
|
||||
|
||||
@@ -168,7 +168,7 @@ class TfeController
|
||||
'type' => 'article',
|
||||
'title' => $title,
|
||||
'description' => $metaDescription,
|
||||
'url' => self::BASE_URL . '/tfe.php?id=' . $thesisId,
|
||||
'url' => self::BASE_URL . '/tfe?id=' . $thesisId,
|
||||
'image' => $ogImage,
|
||||
'image_alt' => $imageAlt,
|
||||
'site_name' => 'Posterg – ERG',
|
||||
@@ -241,7 +241,7 @@ class TfeController
|
||||
*/
|
||||
private function redirectHome(): never
|
||||
{
|
||||
header('Location: index.php');
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ class Dispatcher {
|
||||
'' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
|
||||
'/' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
|
||||
'/index.php' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
|
||||
'/search.php' => ['controller' => 'SearchController', 'action' => 'handle', 'view' => 'public/search'],
|
||||
'/search' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'],
|
||||
'/search.php' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'],
|
||||
'/repertoire' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'],
|
||||
'/repertoire.php' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'],
|
||||
'/tfe' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'],
|
||||
'/tfe.php' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'],
|
||||
'/apropos' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'],
|
||||
'/apropos.php' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'],
|
||||
@@ -127,16 +129,17 @@ class Dispatcher {
|
||||
return self::ROUTES[$path];
|
||||
}
|
||||
|
||||
// /tfe?id= pattern (TFeController handles the id param internally)
|
||||
// /tfe?id= pattern (TfeController handles the id param internally)
|
||||
if (preg_match('#^/tfe$#', $path) && isset($_GET['id'])) {
|
||||
return self::ROUTES['/tfe.php'];
|
||||
return self::ROUTES['/tfe'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view template, passing controller data through extract().
|
||||
* Render a view template wrapped in the full page layout.
|
||||
* Includes head.php, header.php, the view, and footer.php.
|
||||
*/
|
||||
private function render(string $view, array $vars): void {
|
||||
$viewPath = APP_ROOT . '/templates/' . $view . '.php';
|
||||
@@ -146,6 +149,9 @@ class Dispatcher {
|
||||
return;
|
||||
}
|
||||
extract($vars);
|
||||
include APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include $viewPath;
|
||||
include APP_ROOT . '/templates/footer.php';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<script>
|
||||
(function poll(){
|
||||
fetch('/live-reload.php').then(r=>r.json()).then(d=>{
|
||||
fetch('/live-reload').then(r=>r.json()).then(d=>{
|
||||
if(d.changed) location.reload(); else setTimeout(poll,1000);
|
||||
}).catch(()=>setTimeout(poll,2000));
|
||||
})();
|
||||
|
||||
@@ -35,21 +35,21 @@ $_thesisId = $_GET['id'] ?? null;
|
||||
|
||||
<nav aria-label="Navigation principale">
|
||||
<div class="nav-left">
|
||||
<a href="/index.php" class="nav-logo">Xamxam</a>
|
||||
<a href="/" class="nav-logo">Xamxam</a>
|
||||
<ul class="nav-left-links">
|
||||
<li>
|
||||
<a href="/repertoire.php"
|
||||
<a href="/repertoire"
|
||||
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="nav-right-links">
|
||||
<li>
|
||||
<a href="/licence.php"
|
||||
<a href="/licence"
|
||||
<?= ($_navCurrent === 'licence') ? 'aria-current="page"' : '' ?>>Licences</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/apropos.php"
|
||||
<a href="/apropos"
|
||||
<?= ($_navCurrent === 'apropos') ? 'aria-current="page"' : '' ?>>À Propos</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -65,7 +65,7 @@ $_thesisId = $_GET['id'] ?? null;
|
||||
$searchBarValue = $searchBarValue ?? $_GET['query'] ?? '';
|
||||
?>
|
||||
<div class="header-search-wrap">
|
||||
<form method="GET" action="/search.php"
|
||||
<form method="GET" action="/search"
|
||||
role="search" aria-label="Recherche">
|
||||
<label for="site-search-input" class="sr-only">Recherche</label>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
|
||||
@@ -46,7 +46,7 @@ function repToggleUrl(array $sets, string $dim, string $value): string {
|
||||
foreach ($sets['fi'] as $v) $params[] = 'fi[]=' . urlencode($v);
|
||||
foreach ($sets['kw'] as $v) $params[] = 'kw[]=' . urlencode($v);
|
||||
$qs = implode('&', $params);
|
||||
return '/repertoire.php' . ($qs ? '?' . $qs : '');
|
||||
return '/repertoire' . ($qs ? '?' . $qs : '');
|
||||
}
|
||||
|
||||
$anyActive = !empty($activeSets['years']) || !empty($activeSets['ap'])
|
||||
@@ -163,7 +163,7 @@ $hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-i
|
||||
<?php else: ?>
|
||||
<?php foreach ($studentMap as $name => $id): ?>
|
||||
<li>
|
||||
<a href="tfe.php?id=<?= (int)$id ?>" class="rep-entry rep-entry--link">
|
||||
<a href="/tfe?id=<?= (int)$id ?>" class="rep-entry rep-entry--link">
|
||||
<?= htmlspecialchars($name) ?>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
98
app/templates/public/about.php
Normal file
98
app/templates/public/about.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Render a comma-separated list of entries with links.
|
||||
* Entries joined with comma, last two joined with " & ".
|
||||
*/
|
||||
function renderEntries(array $entries): string {
|
||||
if (empty($entries)) return '';
|
||||
$parts = [];
|
||||
foreach ($entries as $e) {
|
||||
$text = htmlspecialchars($e['text'] ?? '');
|
||||
$url = $e['url'] ?? '';
|
||||
if (!empty($url)) {
|
||||
$parts[] = '<span class="apropos-entry"><a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener">' . $text . '</a></span>';
|
||||
} else {
|
||||
$parts[] = '<span class="apropos-entry">' . $text . '</span>';
|
||||
}
|
||||
}
|
||||
$count = count($parts);
|
||||
if ($count === 1) return $parts[0];
|
||||
$prefix = implode(', ', array_slice($parts, 0, $count - 2));
|
||||
$suffix = implode(' & ', array_slice($parts, -2));
|
||||
return $prefix !== '' ? $prefix . ', ' . $suffix : $suffix;
|
||||
}
|
||||
?>
|
||||
<main class="apropos-main" id="main-content">
|
||||
<div class="apropos-layout">
|
||||
|
||||
<!-- LEFT: sticky table of contents -->
|
||||
<nav class="apropos-toc" aria-label="Sections de la page">
|
||||
<p class="apropos-toc-label">Parties</p>
|
||||
<ul>
|
||||
<li><a href="#apropos-intro">À propos</a></li>
|
||||
<?php if (!empty($contacts)): ?>
|
||||
<li><a href="#apropos-contacts">Contacts</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($credits)): ?>
|
||||
<li><a href="#apropos-credits">Crédits</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<div class="apropos-toc-erg">
|
||||
<a href="https://erg.be" target="_blank" rel="noopener">
|
||||
Site de l'erg ↗
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MIDDLE: main prose + sections -->
|
||||
<div class="apropos-content">
|
||||
|
||||
<!-- Intro text from DB -->
|
||||
<section class="apropos-section" id="apropos-intro">
|
||||
<div class="prose">
|
||||
<?= $aboutHtml ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($contacts)): ?>
|
||||
<!-- Contacts section -->
|
||||
<section class="apropos-section" id="apropos-contacts">
|
||||
<h2 class="apropos-section-title">Contacts</h2>
|
||||
<div class="apropos-contacts-grid">
|
||||
<?php foreach ($contacts as $group): ?>
|
||||
<address class="apropos-contact-card">
|
||||
<?= renderEntries($group['entries'] ?? []) ?>
|
||||
<?php if (!empty($group['role'])): ?>
|
||||
<span><?= htmlspecialchars($group['role']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$emails = array_filter(array_column($group['entries'] ?? [], 'email'), fn($e) => !empty($e));
|
||||
foreach ($emails as $email):
|
||||
?>
|
||||
<a href="mailto:<?= htmlspecialchars($email) ?>"><?= htmlspecialchars($email) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</address>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($credits)): ?>
|
||||
<!-- Credits section -->
|
||||
<section class="apropos-section" id="apropos-credits">
|
||||
<h2 class="apropos-section-title">Crédits</h2>
|
||||
<dl class="apropos-credits-list">
|
||||
<?php foreach ($credits as $group): ?>
|
||||
<div class="apropos-credit-row">
|
||||
<dt><?= htmlspecialchars($group['label']) ?></dt>
|
||||
<dd><?= renderEntries($group['entries'] ?? []) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</dl>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php if ($year): ?>
|
||||
<p class="filter-info" role="status">
|
||||
Année : <?= htmlspecialchars($year) ?>
|
||||
<a href="?<?= http_build_query(array_diff_key($vars ?? [], ['page' => 1, 'year' => 1])) ?>" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
|
||||
<a href="/" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
|
||||
</p>
|
||||
<?php elseif ($isDefaultView): ?>
|
||||
<p class="home-section-label" role="status">Publication récente</p>
|
||||
@@ -24,7 +24,7 @@
|
||||
?>
|
||||
<?php if ($thumb): ?>
|
||||
<figure>
|
||||
<img src="/media.php?path=<?= urlencode($thumb) ?>"
|
||||
<img src="/media?path=<?= urlencode($thumb) ?>"
|
||||
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
|
||||
loading="lazy">
|
||||
</figure>
|
||||
|
||||
6
app/templates/public/repertoire.php
Normal file
6
app/templates/public/repertoire.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<main class="search-main" id="main-content">
|
||||
<h1 class="sr-only">Répertoire</h1>
|
||||
<span id="rep-indicator" class="rep-indicator htmx-indicator" aria-hidden="true"></span>
|
||||
<?php include APP_ROOT . '/templates/partials/repertoire-index.php'; ?>
|
||||
</main>
|
||||
<script src="/assets/js/htmx.min.js"></script>
|
||||
67
app/templates/public/search.php
Normal file
67
app/templates/public/search.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php if ($validationError): ?>
|
||||
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter controls -->
|
||||
<form class="search-controls" method="GET" action="/search">
|
||||
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
|
||||
|
||||
<label class="search-filter-label" for="filter-year">Année
|
||||
<select class="search-filter-select" name="year" id="filter-year">
|
||||
<option value="">Toutes</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
|
||||
<?= (int)$y ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="search-filter-label" for="filter-orientation">Orientation
|
||||
<select class="search-filter-select" name="orientation" id="filter-orientation">
|
||||
<option value="">Toutes</option>
|
||||
<?php foreach ($orientations as $o): ?>
|
||||
<option value="<?= htmlspecialchars($o['name']) ?>"
|
||||
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($o['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="search-filter-label" for="filter-ap">AP
|
||||
<select class="search-filter-select" name="ap_program" id="filter-ap">
|
||||
<option value="">Tous</option>
|
||||
<?php foreach ($apPrograms as $ap): ?>
|
||||
<option value="<?= htmlspecialchars($ap['name']) ?>"
|
||||
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($ap['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="search-apply-btn">Filtrer</button>
|
||||
<a href="/search?query=<?= urlencode($_GET['query'] ?? '') ?>" class="search-reset-link">Réinitialiser</a>
|
||||
</form>
|
||||
|
||||
<main class="search-main" id="main-content">
|
||||
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<ul class="results-grid">
|
||||
<?php foreach ($results as $item): ?>
|
||||
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card">
|
||||
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
||||
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
|
||||
</a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="search-empty">Aucun résultat pour cette recherche.</p>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
245
app/templates/public/tfe.php
Normal file
245
app/templates/public/tfe.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<main class="tfe-main" id="main-content">
|
||||
<article class="tfe-layout">
|
||||
|
||||
<!-- LEFT: info — article header -->
|
||||
<header class="tfe-left">
|
||||
<!-- Author above title -->
|
||||
<p class="tfe-author"><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu') ?></p>
|
||||
|
||||
<h1 class="tfe-title">
|
||||
<?= htmlspecialchars($data['title']) ?>
|
||||
<?php if (!empty($data['subtitle'])): ?>
|
||||
– <?= htmlspecialchars($data['subtitle']) ?>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
|
||||
<dl>
|
||||
<?php if (!empty($data['orientation'])): ?>
|
||||
<div>
|
||||
<dt>Orientation :</dt>
|
||||
<dd><a href="/repertoire?or[]=<?= urlencode($data['orientation']) ?>"><?= htmlspecialchars($data['orientation']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['ap_program'])): ?>
|
||||
<div>
|
||||
<dt>Atelier pluridisciplinaire :</dt>
|
||||
<dd><a href="/repertoire?ap[]=<?= urlencode($data['ap_program']) ?>"><?= htmlspecialchars($data['ap_program']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['year'])): ?>
|
||||
<div>
|
||||
<dt>Date :</dt>
|
||||
<dd><a href="/repertoire?fy[]=<?= urlencode($data['year']) ?>"><?= htmlspecialchars($data['year']) ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['languages'])): ?>
|
||||
<div>
|
||||
<dt>Langue :</dt>
|
||||
<dd><?php
|
||||
$langs = array_map('trim', explode(',', $data['languages']));
|
||||
$langLinks = array_map(fn($l) => '<a href="/search?query=' . urlencode($l) . '">' . htmlspecialchars($l) . '</a>', $langs);
|
||||
echo implode(', ', $langLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['formats'])): ?>
|
||||
<div>
|
||||
<dt>Format :</dt>
|
||||
<dd><?php
|
||||
$fmts = array_map('trim', explode(',', $data['formats']));
|
||||
$fmtLinks = array_map(fn($f) => '<a href="/search?query=' . urlencode($f) . '">' . htmlspecialchars($f) . '</a>', $fmts);
|
||||
echo implode(', ', $fmtLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['file_size_info'])): ?>
|
||||
<div>
|
||||
<dt>Durée :</dt>
|
||||
<dd><?= htmlspecialchars($data['file_size_info']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['keywords'])): ?>
|
||||
<div>
|
||||
<dt>Mots-clés :</dt>
|
||||
<dd><?php
|
||||
$kws = array_map('trim', explode(',', $data['keywords']));
|
||||
$kwLinks = array_map(fn($k) => '<a href="/repertoire?kw[]=' . urlencode($k) . '">' . htmlspecialchars($k) . '</a>', $kws);
|
||||
echo implode(', ', $kwLinks);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($promoteursInternes)): ?>
|
||||
<div>
|
||||
<dt>Promoteur·ice interne :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursInternes);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($promoteursExternes)): ?>
|
||||
<div>
|
||||
<dt>Promoteur·ice externe :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursExternes);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($juryPresidents)): ?>
|
||||
<div>
|
||||
<dt>Président·e du jury :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryPresidents);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($juryLecteurs)): ?>
|
||||
<div>
|
||||
<dt>Lecteur·ices :</dt>
|
||||
<dd><?php
|
||||
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryLecteurs);
|
||||
echo implode(', ', $links);
|
||||
?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['access_type'])): ?>
|
||||
<div>
|
||||
<dt>Accès :</dt>
|
||||
<dd><?= htmlspecialchars($data['access_type']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['license_type'])): ?>
|
||||
<div>
|
||||
<dt>Licence :</dt>
|
||||
<dd><?= htmlspecialchars($data['license_type']) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['context_note'])): ?>
|
||||
<div class="tfe-meta-note">
|
||||
<dt>Note :</dt>
|
||||
<dd class="tfe-note-value"><?= nl2br(htmlspecialchars($data['context_note'])) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['author_email']) && !empty($data['author_show_contact'])): ?>
|
||||
<div>
|
||||
<dt>Contact :</dt>
|
||||
<dd>
|
||||
<?php
|
||||
$_contact = $data['author_email'];
|
||||
$_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false;
|
||||
$_isEmail = !$_isUrl && str_contains($_contact, '@');
|
||||
if ($_isUrl):
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($_contact) ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars(preg_replace('#^https?://#i', '', rtrim($_contact, '/'))) ?>
|
||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||
</a>
|
||||
<?php elseif ($_isEmail): ?>
|
||||
<a href="mailto:<?= htmlspecialchars($_contact) ?>"><?= htmlspecialchars($_contact) ?></a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($_contact) ?>
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['baiu_link'])): ?>
|
||||
<?php
|
||||
$_baiuHref = htmlspecialchars($data['baiu_link']);
|
||||
$_baiuLabel = preg_replace('#^https?://#i', '', rtrim($data['baiu_link'], '/'));
|
||||
?>
|
||||
<div>
|
||||
<dt>Lien :</dt>
|
||||
<dd>
|
||||
<a href="<?= $_baiuHref ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($_baiuLabel) ?>
|
||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||
</a>
|
||||
|
||||
</dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
|
||||
<?php if (!empty($data['synopsis'])): ?>
|
||||
<p class="tfe-synopsis-text">
|
||||
<?= nl2br(htmlspecialchars($data['synopsis'])) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<!-- RIGHT: media — supplementary aside -->
|
||||
<aside class="tfe-right">
|
||||
<?php
|
||||
$_videoIndex = 0;
|
||||
?>
|
||||
<?php if ($isInterdit): ?>
|
||||
<p class="tfe-restricted">
|
||||
Ce TFE n'est pas disponible en ligne.
|
||||
</p>
|
||||
<?php elseif (!empty($data['files'])): ?>
|
||||
<?php foreach ($data['files'] as $file): ?>
|
||||
<?php
|
||||
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
|
||||
if ($ext === 'vtt') continue;
|
||||
?>
|
||||
<figure>
|
||||
<?php if ($ext === 'pdf'): ?>
|
||||
<embed src="/media?path=<?= urlencode($file['file_path']) ?>"
|
||||
type="application/pdf" width="100%" height="700px">
|
||||
<p class="tfe-pdf-fallback">
|
||||
<a href="/media?path=<?= urlencode($file['file_path']) ?>&download=1">
|
||||
Télécharger le PDF
|
||||
</a>
|
||||
</p>
|
||||
<?php elseif (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp'])): ?>
|
||||
<img src="/media?path=<?= urlencode($file['file_path']) ?>"
|
||||
alt="<?= htmlspecialchars(
|
||||
!empty($file['description'])
|
||||
? $file['description']
|
||||
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
|
||||
) ?>">
|
||||
<?php elseif ($ext === 'mp4'): ?>
|
||||
<?php
|
||||
$_vttPath = $captionFiles[$_videoIndex] ?? null;
|
||||
$_videoIndex++;
|
||||
?>
|
||||
<video width="100%" controls>
|
||||
<source src="/media?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
|
||||
<?php if ($_vttPath): ?>
|
||||
<track kind="captions"
|
||||
src="/media?path=<?= urlencode($_vttPath) ?>"
|
||||
srclang="fr"
|
||||
label="Sous-titres"
|
||||
default>
|
||||
<?php endif; ?>
|
||||
</video>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($file['description'])): ?>
|
||||
<figcaption><?= htmlspecialchars($file['description']) ?></figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="tfe-no-files">Aucun fichier disponible pour ce TFE.</p>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
@@ -3,7 +3,7 @@
|
||||
// $searchValue: current search query (optional)
|
||||
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
|
||||
?>
|
||||
<form method="GET" action="/search.php"
|
||||
<form method="GET" action="/search"
|
||||
role="search" aria-label="Recherche">
|
||||
<label for="site-search-input" class="sr-only">Recherche</label>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
|
||||
@@ -148,23 +148,28 @@ server {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Share-link (partage) — rewrite pretty URLs to index.php
|
||||
# Share-link (partage) — handled by front controller
|
||||
location /partage/ {
|
||||
try_files $uri /partage/index.php$is_args$args;
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
# Search endpoint - rate limiting
|
||||
location = /search.php {
|
||||
limit_req zone=search burst=10 nodelay;
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
|
||||
# /media — served by front controller (MediaController validates + streams)
|
||||
location = /media {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
# PHP files handler (public pages)
|
||||
location ~ \.php$ {
|
||||
# Rate limiting for general PHP requests
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
# /live-reload — served by front controller
|
||||
location = /live-reload {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
# Maintenance page
|
||||
location = /maintenance {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
# Front controller — all PHP requests routed through index.php
|
||||
location = /index.php {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
|
||||
|
||||
@@ -176,6 +181,16 @@ server {
|
||||
fastcgi_send_timeout 120;
|
||||
}
|
||||
|
||||
# All other clean URLs — fall through to front controller
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
# Block all other direct PHP access (security)
|
||||
location ~ \.php$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Static files caching
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|otf)$ {
|
||||
expires 30d;
|
||||
@@ -190,18 +205,13 @@ server {
|
||||
add_header Content-Disposition "inline";
|
||||
}
|
||||
|
||||
# Silence favicon.ico 404s — browsers that ignore <link rel="icon"> still request this
|
||||
# Silence favicon.ico 404s
|
||||
location = /favicon.ico {
|
||||
return 204;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Root location - try files or 404
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Deny access to .htaccess files
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
|
||||
Reference in New Issue
Block a user