Redesign UI to match target design images

- Flat purple-gradient nav bar with POSTERG/RÉPERTOIRE/À PROPOS links
- Full-width search bar with icon, bottom-border only, below nav
- Home: white bg, media card grid (thumbnail + author/title label below)
- Répertoire: 4-column index (Années/Catégories/Étudiantes/Mots-clés)
- TFE: 2-column layout (large text left, media right)
- À Propos: 2-column, large monospace text, new apropos.php page
- Admin: dark theme (#1a1a1a), purple gradient nav, bottom-border inputs
- New shared partials: templates/nav.php, templates/search-bar.php
- Rewrote all CSS: common, main, search, tfe, apropos, admin
This commit is contained in:
Pontoporeia
2026-02-24 23:34:16 +01:00
parent eaad740574
commit 2110d2b916
22 changed files with 2459 additions and 3043 deletions

View File

@@ -1,356 +1,254 @@
<?php
// Bootstrap application
require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/RateLimit.php';
// Rate limiting: 30 requests per minute
// Rate limiting
$rateLimit = new RateLimit(30, 60);
// Check rate limit
if (!$rateLimit->check()) {
http_response_code(429);
header('Retry-After: ' . $rateLimit->getResetTime());
$rateLimit->sendHeaders();
// Simple error page
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Rate Limit</title></head><body>';
echo '<h1>Trop de requêtes</h1>';
echo '<p>Vous avez dépassé la limite de 30 recherches par minute. Veuillez réessayer dans ' . $rateLimit->getResetTime() . ' secondes.</p>';
echo '</body></html>';
echo '<!DOCTYPE html><html><body><h1>Trop de requêtes</h1><p>Réessayez dans ' . $rateLimit->getResetTime() . ' secondes.</p></body></html>';
exit;
}
$rateLimit->sendHeaders();
if (rand(1, 100) === 1) $rateLimit->cleanup();
if (rand(1, 100) === 1) {
$rateLimit->cleanup();
}
// Pagination - adjust to grid
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$itemsPerPage = 9; // Default grid size (3 rows × 3 columns)
// Collect search parameters
// 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['finality'])) {
$searchParams['finality'] = $_GET['finality'];
}
if (!empty($_GET['keyword'])) {
$searchParams['keyword'] = $_GET['keyword'];
}
if (!empty($_GET['format'])) {
$searchParams['format'] = $_GET['format'];
}
if (!empty($_GET['language'])) {
$searchParams['language'] = $_GET['language'];
}
if (isset($_GET['is_doctoral'])) {
$searchParams['is_doctoral'] = $_GET['is_doctoral'] === '1';
}
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;
$showFilters = isset($_GET['filters']) && $_GET['filters'] === 'show';
try {
$db = Database::getInstance();
// Get search results
$offset = ($page - 1) * $itemsPerPage;
$results = $db->searchTheses($searchParams, $itemsPerPage, $offset);
$totalItems = $db->countSearchResults($searchParams);
$totalPages = ceil($totalItems / $itemsPerPage);
// Get filter options
$years = $db->getAvailableYears();
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->getOrientations();
$apPrograms = $db->getApPrograms();
$finalityTypes = $db->getFinalityTypes();
$keywords = $db->getUsedKeywords();
$formats = $db->getFormatTypes();
$languages = $db->getLanguages();
$apPrograms = $db->getApPrograms();
$keywords = $db->getUsedKeywords();
// Get all published theses for student index (multiple pages if needed)
$students = $db->searchTheses([], 100, 0); // max 100 per DB limit
} catch (InvalidArgumentException $e) {
error_log("Search validation error: " . $e->getMessage());
$validationError = $e->getMessage();
$results = [];
$totalPages = 0;
$totalItems = 0;
$years = [];
$orientations = [];
$apPrograms = [];
$finalityTypes = [];
$keywords = [];
$formats = [];
$languages = [];
$results = []; $totalItems = 0; $totalPages = 0;
$years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = [];
} catch (Exception $e) {
error_log("Error in search: " . $e->getMessage());
$validationError = "Une erreur est survenue lors de la recherche.";
$results = [];
$totalPages = 0;
$totalItems = 0;
$years = [];
$orientations = [];
$apPrograms = [];
$finalityTypes = [];
$keywords = [];
$formats = [];
$languages = [];
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'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recherche - Posterg</title>
<title>Répertoire Posterg</title>
<link rel="stylesheet" href="assets/modern-normalize.min.css">
<link rel="stylesheet" href="assets/common.css">
<link rel="stylesheet" href="assets/search.css">
<?php if (php_sapi_name() === 'cli-server'): ?>
<script>
(function poll() {
fetch('/live-reload.php').then(r=>r.json()).then(d=>{
if(d.changed) location.reload(); else setTimeout(poll,1000);
}).catch(()=>setTimeout(poll,2000));
})();
</script>
<?php endif; ?>
</head>
<body>
<header>
<div class="search-header">
<a href="index.php" class="back-button">← Retour</a>
<form method="GET" action="search.php" class="search-form">
<input
type="text"
name="query"
class="search-input"
placeholder="Rechercher..."
value="<?= htmlspecialchars($_GET['query'] ?? ''); ?>"
autofocus
>
<div class="search-actions">
<button type="submit" class="search-button">Rechercher</button>
<button type="button" class="filter-button <?= $showFilters ? 'active' : ''; ?>" onclick="toggleFilters()">
Filtres
</button>
</div>
<!-- Hidden field to maintain filter panel state -->
<input type="hidden" name="filters" id="filters-state" value="<?= $showFilters ? 'show' : 'hide'; ?>">
<!-- Preserve other filter values as hidden fields when searching -->
<?php foreach (['year', 'orientation', 'ap_program', 'finality', 'keyword', 'format', 'language', 'is_doctoral'] as $field): ?>
<?php if (!empty($_GET[$field])): ?>
<input type="hidden" name="<?= $field; ?>" value="<?= htmlspecialchars($_GET[$field]); ?>">
<?php endif; ?>
<body class="search-body">
<?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'] ?? '') ?>">
<div class="search-filter-group">
<span class="search-filter-label">Année</span>
<select class="search-filter-select" name="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; ?>
</form>
</select>
</div>
<?php if ($validationError): ?>
<div class="error-message">
⚠ <?= htmlspecialchars($validationError); ?>
</div>
<?php endif; ?>
<div class="filters-panel <?= $showFilters ? 'show' : ''; ?>" id="filters-panel">
<form method="GET" action="search.php">
<!-- Preserve query when using filters -->
<?php if (!empty($_GET['query'])): ?>
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query']); ?>">
<?php endif; ?>
<input type="hidden" name="filters" value="show">
<div class="filters-grid">
<div class="filter-group">
<label class="filter-label">Année</label>
<select name="year" class="filter-select">
<option value="">Toutes</option>
<?php foreach ($years as $year): ?>
<option value="<?= (int)$year; ?>" <?= (isset($_GET['year']) && $_GET['year'] == $year) ? 'selected' : ''; ?>>
<?= (int)$year; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Orientation</label>
<select name="orientation" class="filter-select">
<option value="">Toutes</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?= htmlspecialchars($orientation['name']); ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $orientation['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">AP</label>
<select name="ap_program" class="filter-select">
<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>
</div>
<div class="filter-group">
<label class="filter-label">Finalité</label>
<select name="finality" class="filter-select">
<option value="">Toutes</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?= htmlspecialchars($finality['name']); ?>"
<?= (isset($_GET['finality']) && $_GET['finality'] == $finality['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($finality['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Format</label>
<select name="format" class="filter-select">
<option value="">Tous</option>
<?php foreach ($formats as $format): ?>
<option value="<?= htmlspecialchars($format['name']); ?>"
<?= (isset($_GET['format']) && $_GET['format'] == $format['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($format['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Langue</label>
<select name="language" class="filter-select">
<option value="">Toutes</option>
<?php foreach ($languages as $language): ?>
<option value="<?= htmlspecialchars($language['name']); ?>"
<?= (isset($_GET['language']) && $_GET['language'] == $language['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($language['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Mot-clé</label>
<select name="keyword" class="filter-select">
<option value="">Tous</option>
<?php foreach ($keywords as $keyword): ?>
<option value="<?= htmlspecialchars($keyword['keyword']); ?>"
<?= (isset($_GET['keyword']) && $_GET['keyword'] == $keyword['keyword']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($keyword['keyword']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Type</label>
<select name="is_doctoral" class="filter-select">
<option value="">Tous</option>
<option value="0" <?= (isset($_GET['is_doctoral']) && $_GET['is_doctoral'] == '0') ? 'selected' : ''; ?>>
TFE uniquement
</option>
<option value="1" <?= (isset($_GET['is_doctoral']) && $_GET['is_doctoral'] == '1') ? 'selected' : ''; ?>>
Thèses doctorales
</option>
</select>
</div>
</div>
<div class="filter-actions">
<button type="submit" class="search-button">Appliquer</button>
<a href="search.php?filters=show" class="reset-button">Réinitialiser</a>
</div>
</form>
<div class="search-filter-group">
<span class="search-filter-label">Orientation</span>
<select class="search-filter-select" name="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>
</div>
</header>
<div class="main-wrapper">
<main>
<div class="cards-container">
<?php if (count($results) > 0): ?>
<div class="search-filter-group">
<span class="search-filter-label">AP</span>
<select class="search-filter-select" name="ap_program">
<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>
</div>
<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">
<div class="search-results-view">
<p class="search-results-header"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></p>
<?php if (!empty($results)): ?>
<div class="results-grid">
<?php foreach ($results as $item): ?>
<a href="tfe.php?id=<?= (int)$item['id']; ?>" class="card-link">
<div class="card">
<div class="card-content">
<h3 class="title"><?= htmlspecialchars($item['title']); ?></h3>
<p class="authors"><?= htmlspecialchars($item['authors'] ?? 'Auteur inconnu'); ?></p>
<p class="year"><?= htmlspecialchars($item['year']); ?></p>
<?php if (!empty($item['orientation'])): ?>
<div class="tags">
<span class="tag"><?= htmlspecialchars($item['orientation']); ?></span>
</div>
<?php endif; ?>
</div>
</div>
</a>
<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>
<span class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></span>
</a>
<?php endforeach; ?>
<?php elseif (!empty($searchParams)): ?>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; color: white; font-size: 1.2rem;">
Aucun résultat trouvé pour cette recherche.
</div>
</div>
<?php if ($totalPages > 1): ?>
<div style="display:flex;gap:.5rem;justify-content:center;align-items:center;padding:1.5rem 0;">
<a href="?<?= http_build_query(array_merge($_GET, ['page' => max(1, $page - 1)])) ?>"
style="padding:.25rem .7rem;border:1px solid #ddd;border-radius:3px;color:#111;text-decoration:none;<?= $page <= 1 ? 'opacity:.3;pointer-events:none;' : '' ?>"></a>
<span style="font-size:.9rem;color:#666;"><?= $page ?> / <?= $totalPages ?></span>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => min($totalPages, $page + 1)])) ?>"
style="padding:.25rem .7rem;border:1px solid #ddd;border-radius:3px;color:#111;text-decoration:none;<?= $page >= $totalPages ? 'opacity:.3;pointer-events:none;' : '' ?>"></a>
</div>
<?php endif; ?>
<?php else: ?>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; color: white; font-size: 1.2rem;">
Utilisez la barre de recherche pour trouver des mémoires.
</div>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<a href="?<?= http_build_query(array_merge($_GET, ['page' => max(1, $page - 1)])); ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : ''; ?>">
</a>
<div class="pagination-info">
<span class="page-current"><?= $page; ?></span>
<span class="page-separator">/</span>
<span class="page-total"><?= $totalPages; ?></span>
</div>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => min($totalPages, $page + 1)])); ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : ''; ?>">
</a>
</main>
<?php else: ?>
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
<main class="search-main">
<div class="repertoire-index">
<!-- ANNÉES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Années</h2>
<?php foreach ($years as $y): ?>
<a href="search.php?year=<?= (int)$y ?>"
class="year-index-item <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'active' : '' ?>">
<?= (int)$y ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<footer>
<div class="results-footer">
<span class="results-count"><?= (int)$totalItems; ?></span>
<span>résultat<?= $totalItems > 1 ? 's' : ''; ?></span>
<!-- CATÉGORIES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Catégories</h2>
<?php if (!empty($orientations)): ?>
<span class="cat-index-label">Orientation</span>
<?php foreach ($orientations as $o): ?>
<a href="search.php?orientation=<?= urlencode($o['name']) ?>"
class="cat-index-item <?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'active' : '' ?>">
<?= htmlspecialchars($o['name']) ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($apPrograms)): ?>
<span class="cat-index-label">Ateliers Pluridisciplinaires</span>
<?php foreach ($apPrograms as $ap): ?>
<a href="search.php?ap_program=<?= urlencode($ap['name']) ?>"
class="cat-index-item <?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'active' : '' ?>">
<?= htmlspecialchars($ap['name']) ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
</div>
</footer>
</div>
<script>
function toggleFilters() {
const panel = document.getElementById('filters-panel');
const button = document.querySelector('.filter-button');
const state = document.getElementById('filters-state');
panel.classList.toggle('show');
button.classList.toggle('active');
// Update hidden field
state.value = panel.classList.contains('show') ? 'show' : 'hide';
}
</script>
<!-- ÉTUDIANTES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">É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);
foreach ($authorMap as $name => $id): ?>
<a href="tfe.php?id=<?= (int)$id ?>" class="student-index-item">
<?= htmlspecialchars($name) ?>
</a>
<?php endforeach; ?>
</div>
<!-- MOTS-CLÉS -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Mots-clés</h2>
<?php foreach ($keywords as $kw): ?>
<a href="search.php?keyword=<?= urlencode($kw['keyword']) ?>"
class="keyword-index-item <?= (isset($_GET['keyword']) && $_GET['keyword'] == $kw['keyword']) ? 'active' : '' ?>">
<?= htmlspecialchars($kw['keyword']) ?>
</a>
<?php endforeach; ?>
</div>
</div>
</main>
<?php endif; ?>
</body>
</html>