mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
All third-party assets are now self-hosted — zero external requests at runtime.
CSS (assets/css/):
- modern-normalize.min.css (was assets/)
- common.css, admin.css, main.css, search.css, tfe.css, apropos.css (was assets/)
- easymde.min.css 2.20.0 (was cdn.jsdelivr.net)
- font-awesome.min.css 4.7.0 (was maxcdn.bootstrapcdn.com; injected at runtime by EasyMDE)
JS (assets/js/):
- easymde.min.js 2.20.0 (was cdn.jsdelivr.net)
Fonts (assets/fonts/fontawesome/):
- fontawesome-webfont.{eot,woff2,woff,ttf,svg}, FontAwesome.otf 4.7.0
Path fixes:
- common.css @font-face: ./fonts/ -> ../fonts/ (one level deeper)
- font-awesome.min.css @font-face: ../fonts/ -> ../fonts/fontawesome/ (dedicated subdir)
- pages-edit.php: autoDownloadFontAwesome:false added to EasyMDE init to
suppress the runtime CDN injection that was still present inside easymde.min.js
Reference updates (all now absolute /assets/css/* or /assets/js/*):
- templates/public/head.php: modern-normalize + common
- templates/admin/head.php: modern-normalize + admin
- public/admin/login.php: modern-normalize + admin (standalone head)
- public/index.php, tfe.php, search.php, apropos.php, licence.php: extraCss paths
- public/admin/pages-edit.php: extraCss + extraJs (font-awesome, easymde CSS/JS)
Nginx static-file location already covers .css/.js/.woff/.woff2/.ttf/.otf with
30-day cache headers — no nginx config change needed.
298 lines
12 KiB
PHP
298 lines
12 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../config/bootstrap.php';
|
||
require_once APP_ROOT . '/src/Database.php';
|
||
require_once APP_ROOT . '/src/RateLimit.php';
|
||
|
||
// Rate limiting
|
||
$rateLimit = new RateLimit(30, 60);
|
||
if (!$rateLimit->check()) {
|
||
http_response_code(429);
|
||
header('Retry-After: ' . $rateLimit->getResetTime());
|
||
$retrySeconds = (int)$rateLimit->getResetTime();
|
||
echo <<<HTML
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Trop de requêtes – Posterg</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
background: #0d0d0d;
|
||
color: #e0e0e0;
|
||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
}
|
||
.box { max-width: 520px; text-align: center; }
|
||
.box__logo {
|
||
font-size: 1.1rem; font-weight: 700;
|
||
letter-spacing: .12em; text-transform: uppercase;
|
||
color: #fff; margin-bottom: 2.5rem;
|
||
}
|
||
.box__title { font-size: 1.6rem; font-weight: 300; margin-bottom: 1rem; }
|
||
.box__text { font-size: .95rem; color: #999; line-height: 1.7; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="box">
|
||
<div class="box__logo">POSTERG</div>
|
||
<h1 class="box__title">Trop de requêtes</h1>
|
||
<p class="box__text">Vous avez effectué trop de recherches en peu de temps.<br>
|
||
Réessayez dans {$retrySeconds} secondes.</p>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
HTML;
|
||
exit;
|
||
}
|
||
$rateLimit->sendHeaders();
|
||
if (rand(1, 100) === 1) $rateLimit->cleanup();
|
||
|
||
// Collect search/filter params
|
||
$searchParams = [];
|
||
if (!empty($_GET['query'])) $searchParams['query'] = trim($_GET['query']);
|
||
if (!empty($_GET['year'])) $searchParams['year'] = intval($_GET['year']);
|
||
if (!empty($_GET['orientation'])) $searchParams['orientation'] = $_GET['orientation'];
|
||
if (!empty($_GET['ap_program'])) $searchParams['ap_program'] = $_GET['ap_program'];
|
||
if (!empty($_GET['keyword'])) $searchParams['keyword'] = $_GET['keyword'];
|
||
|
||
$hasSearch = !empty($searchParams);
|
||
|
||
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
|
||
$itemsPerPage = 30;
|
||
$validationError = null;
|
||
|
||
try {
|
||
$db = Database::getInstance();
|
||
$offset = ($page - 1) * $itemsPerPage;
|
||
|
||
if ($hasSearch) {
|
||
$results = $db->searchTheses($searchParams, $itemsPerPage, $offset);
|
||
$totalItems = $db->countSearchResults($searchParams);
|
||
$totalPages = ceil($totalItems / $itemsPerPage);
|
||
} else {
|
||
$results = [];
|
||
$totalItems = 0;
|
||
$totalPages = 0;
|
||
}
|
||
|
||
$years = $db->getAvailableYears();
|
||
$orientations = $db->getAllOrientations();
|
||
$apPrograms = $db->getAllAPPrograms();
|
||
$keywords = $db->getUsedTags();
|
||
// Fetch id+authors only — lean query bypassing the fat v_theses_public view
|
||
$students = $db->getPublishedAuthors();
|
||
} catch (InvalidArgumentException $e) {
|
||
$validationError = $e->getMessage();
|
||
$results = []; $totalItems = 0; $totalPages = 0;
|
||
$years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = [];
|
||
} catch (Exception $e) {
|
||
error_log("Search error: " . $e->getMessage());
|
||
$validationError = "Une erreur est survenue.";
|
||
$results = []; $totalItems = 0; $totalPages = 0;
|
||
$years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = [];
|
||
}
|
||
|
||
$currentNav = 'repertoire';
|
||
$searchBarValue = $_GET['query'] ?? '';
|
||
$pageTitle = 'Répertoire – Posterg';
|
||
$metaDescription = 'Parcourez le répertoire des mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles. Recherche par année, orientation, atelier et mots-clés.';
|
||
$ogTags = [
|
||
'type' => 'website',
|
||
'title' => $pageTitle,
|
||
'description' => $metaDescription,
|
||
'url' => 'https://posterg.erg.be/search.php',
|
||
'site_name' => 'Posterg – ERG',
|
||
];
|
||
$extraCss = ['/assets/css/search.css'];
|
||
?>
|
||
<?php include APP_ROOT . '/templates/public/head.php'; ?>
|
||
<body class="search-body">
|
||
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
|
||
|
||
<?php include APP_ROOT . '/templates/nav.php'; ?>
|
||
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
|
||
|
||
<?php if ($validationError): ?>
|
||
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($hasSearch): ?>
|
||
<!-- ── RESULTS VIEW ─────────────────────────────────── -->
|
||
|
||
<!-- 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" 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 if ($totalPages > 1): ?>
|
||
<nav class="pagination-wrap" aria-label="Pagination">
|
||
<a href="?<?= http_build_query(array_merge($_GET, ['page' => max(1, $page - 1)])) ?>"
|
||
class="pagination-btn<?= $page <= 1 ? ' disabled' : '' ?>"
|
||
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||
aria-label="Page précédente">‹</a>
|
||
<span class="pagination-info" aria-current="page"><?= $page ?> / <?= $totalPages ?></span>
|
||
<a href="?<?= http_build_query(array_merge($_GET, ['page' => min($totalPages, $page + 1)])) ?>"
|
||
class="pagination-btn<?= $page >= $totalPages ? ' disabled' : '' ?>"
|
||
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||
aria-label="Page suivante">›</a>
|
||
</nav>
|
||
<?php endif; ?>
|
||
|
||
<?php else: ?>
|
||
<p class="search-empty">Aucun résultat pour cette recherche.</p>
|
||
<?php endif; ?>
|
||
</main>
|
||
|
||
<?php else: ?>
|
||
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
|
||
<main class="search-main" id="main-content">
|
||
<h1 class="sr-only">Répertoire</h1>
|
||
<div class="repertoire-index">
|
||
|
||
<!-- ANNÉES -->
|
||
<section class="repertoire-col">
|
||
<h2>Années</h2>
|
||
<ul>
|
||
<?php foreach ($years as $y): ?>
|
||
<li><a href="search.php?year=<?= (int)$y ?>"
|
||
<?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'aria-current="page"' : '' ?>>
|
||
<?= (int)$y ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- CATÉGORIES -->
|
||
<section class="repertoire-col">
|
||
<h2>Catégories</h2>
|
||
|
||
<?php if (!empty($orientations)): ?>
|
||
<span class="cat-index-label">Orientation</span>
|
||
<ul>
|
||
<?php foreach ($orientations as $o): ?>
|
||
<li><a href="search.php?orientation=<?= urlencode($o['name']) ?>"
|
||
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'aria-current="page"' : '' ?>>
|
||
<?= htmlspecialchars($o['name']) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!empty($apPrograms)): ?>
|
||
<span class="cat-index-label">Ateliers Pluridisciplinaires</span>
|
||
<ul>
|
||
<?php foreach ($apPrograms as $ap): ?>
|
||
<li><a href="search.php?ap_program=<?= urlencode($ap['name']) ?>"
|
||
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'aria-current="page"' : '' ?>>
|
||
<?= htmlspecialchars($ap['name']) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- ÉTUDIANTES -->
|
||
<section class="repertoire-col">
|
||
<h2>Étudiantes</h2>
|
||
<?php
|
||
// Build unique author → thesis list
|
||
$authorMap = [];
|
||
foreach ($students as $s) {
|
||
if (empty($s['authors'])) continue;
|
||
$names = explode(',', $s['authors']);
|
||
foreach ($names as $name) {
|
||
$name = trim($name);
|
||
if ($name && !isset($authorMap[$name])) {
|
||
$authorMap[$name] = $s['id'];
|
||
}
|
||
}
|
||
}
|
||
ksort($authorMap);
|
||
?>
|
||
<ul>
|
||
<?php foreach ($authorMap as $name => $id): ?>
|
||
<li><a href="tfe.php?id=<?= (int)$id ?>">
|
||
<?= htmlspecialchars($name) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- MOTS-CLÉS -->
|
||
<section class="repertoire-col">
|
||
<h2>Mots-clés</h2>
|
||
<ul>
|
||
<?php foreach ($keywords as $kw): ?>
|
||
<li><a href="search.php?keyword=<?= urlencode($kw['name']) ?>"
|
||
<?= (isset($_GET['keyword']) && $_GET['keyword'] == $kw['name']) ? 'aria-current="page"' : '' ?>>
|
||
<?= htmlspecialchars($kw['name']) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</section>
|
||
|
||
</div>
|
||
</main>
|
||
<?php endif; ?>
|
||
|
||
</body>
|
||
</html>
|