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

155
TODO.md
View File

@@ -1,132 +1,31 @@
# Post-ERG Dependency & Refactoring Analysis
# TODO
## Summary
## Styling Redesign (matching design images)
The project has **zero external PHP library dependencies** (no Composer, no vendor/).
All PHP logic relies exclusively on standard PHP extensions: PDO/SQLite, `finfo`,
`session_*`, `password_verify`, `hash_equals`, `random_bytes`, `json_*`, SPL iterators.
There is one vendored CSS file (`modern-normalize.min.css`, 1 file, 8 lines).
- [x] Redesign shared nav bar (purple gradient top, flat, POSTERG / RÉPERTOIRE / À PROPOS)
- [x] Redesign shared search bar (full-width, icon, bottom border only, white bg)
- [x] Rewrite `common.css` (nav + search bar components)
- [x] Rewrite `main.css` (home page — white bg, media card grid, label below)
- [x] Rewrite `search.css` (répertoire index — 4-col ANNÉES/CATÉGORIES/ÉTUDIANTES/MOTS-CLÉS)
- [x] Rewrite `tfe.css` (TFE page — 2-col, large author/title left, media right)
- [x] Add `apropos.css` (À Propos — 2-col, large monospace text)
- [x] Rewrite `admin.css` (dark bg, purple gradient nav, bottom-border-only form inputs)
- [x] Update `templates/nav.php` (new shared nav partial)
- [x] Update `templates/search-bar.php` (new shared search bar partial)
- [x] Rewrite `public/index.php` (home page with new layout)
- [x] Rewrite `public/search.php` (répertoire index view + search results view)
- [x] Rewrite `public/tfe.php` (individual TFE page)
- [x] Create `public/apropos.php` (À Propos page)
- [x] Rewrite `templates/admin/head.php` (admin nav)
- [x] Rewrite `templates/admin/footer.php` (clean close)
- [x] Rewrite `public/admin/add.php` (form with row layout)
- [x] Rewrite `public/admin/index.php` (dark table)
- [x] Rewrite `public/admin/edit.php` (form with row layout)
- [x] Rewrite `public/admin/login.php` (centered dark login box)
- [x] Rewrite `public/admin/thanks.php` (dark info cards)
- [x] Rewrite `public/admin/import.php` (clean dark form)
The only real problems are **internal structural bugs** and **dead code paths**, not
third-party dependencies. The tasks below are ordered from critical to nice-to-have.
## Pending
---
## Critical Bugs (broken at runtime)
- [x] **Fix broken `lib/` require paths in all admin pages**
Admin pages (`add.php`, `edit.php`, `import.php`, `thanks.php`, `login.php`,
`logout.php`, `actions/formulaire.php`, `actions/publish.php`) all require
`../../lib/AdminAuth.php` and `../../lib/Database.php`, but the `lib/` directory
**does not exist**. The actual files live in `src/`. This means the entire admin
panel is broken. Fix: change all `lib/` references to `src/`.
- [x] **Fix missing `modern-normalize.css` (no `.min` variant)**
`templates/header.php`, `templates/head.php`, and `public/search.php` reference
`assets/modern-normalize.css` (without `.min`), but only `modern-normalize.min.css`
exists. Either rename the file or update the references to be consistent.
- [x] **Fix `admin/index.php` inconsistency**
`admin/index.php` uses `src/AdminAuth.php` (correct) but then
`../../lib/Database.php` (broken). It should load from `src/` consistently.
---
## Structural / Code-Quality Refactors
- [ ] **Unify and rename `src/` path references across the entire codebase**
After fixing the `lib/``src/` migration, normalise every admin page to load
`src/Database.php` and `src/AdminAuth.php` via `APP_ROOT` (the constant already
defined in `bootstrap.php`), removing the fragile relative-path `../../` chains.
- [x] **Eliminate the duplicate `searchTheses` / `countSearchResults` condition block**
`Database::searchTheses()` and `Database::countSearchResults()` share identical
WHERE-clause construction logic (~80 lines each). Extract a private
`buildSearchConditions(array $params): array` helper that returns `[$conditions,
$bindings]` and call it from both methods.
- [ ] **Remove `getConnection()` / `getPDO()` alias duplication**
The `Database` class exposes `getConnection()`, `getPDO()`, and direct transaction
delegation (`beginTransaction`, `commit`, `rollback`) purely because the admin code
accesses raw PDO. Consider removing `getConnection()` (alias of `getPDO()`) and
instead promoting the most-used raw queries into `Database` methods, reducing
direct PDO exposure.
- [x] **Move inline SQL in `admin/index.php` into `Database`**
`admin/index.php` builds a raw SQL query with dynamic filter conditions directly in
the page. This is the only admin page doing so. Add a `getThesesList(array
$filters): array` method to `Database` to match the pattern used everywhere else.
- [ ] **Add a `getThesisByIdAdmin(int $id): ?array` method to remove repeated raw queries in admin**
`admin/thanks.php` and `admin/edit.php` each call `$db->getThesis($id)` then
immediately issue further raw PDO queries for related data (`thesis_languages`,
`thesis_formats`, files). Consolidate into a method that returns everything needed
for the admin detail view.
---
## What Can Be Removed / Simplified
- [x] **Remove `include_template()` helper from `bootstrap.php` — it is never called**
The function `include_template($name)` in `config/bootstrap.php` is dead code;
pages use direct `include APP_ROOT . '/templates/...'` instead.
- [x] **Remove the Composer autoload stub from `bootstrap.php`**
`bootstrap.php` has `if (file_exists(APP_ROOT . '/vendor/autoload.php'))` — there
is no Composer vendor directory and no plan for one. Remove this dead branch.
- [x] **Delete `apps/admin/` directory**
`apps/admin/` contains only `data/` (empty with test data) and `error.log` and
`test.db`. It appears to be a leftover from an earlier structure. If confirmed
unused, delete it.
- [x] **Remove `apps/` directory entirely if it contains only residual artefacts**
Related to the above — verify no active code references `apps/`.
---
## What Needs External Dependencies (nothing — keep it that way)
- **Authentication**: `password_verify` + `session_*` + `random_bytes` — already
standard PHP. No dependency needed.
- **Database**: PDO + SQLite — already standard PHP. No dependency needed.
- **Rate limiting**: File-based JSON sliding window — already implemented without
deps. Could be replaced by Redis/APCu at scale, but unnecessary for current load.
- **File serving / MIME validation**: `finfo` (fileinfo extension) — standard PHP
bundled extension.
- **CSRF**: `hash_equals` + `random_bytes` — standard PHP. No dependency needed.
- **CSS reset** (`modern-normalize`): The single vendored file (8 lines, minified)
is small enough to keep vendored. No CDN link, no build step. ✓
---
## Testing Infrastructure
- [x] **Fix `SearchTest.php` — it calls `searchTheses()` with a string, not an array**
`$db->searchTheses('art')` passes a string, but `searchTheses()` expects
`array $params`. This test would throw a TypeError at runtime. Fix the call to
`$db->searchTheses(['query' => 'art'])`.
- [ ] **Add a test for the `lib/` → `src/` path fix once it is applied**
After the path fix, add a smoke test that `require`-s each admin page's
dependencies to catch future regressions.
---
## Low Priority / Nice-to-Have
- [ ] **Normalise `modern-normalize` to a single canonical filename**
Pick either `.min.css` or `.css` and use it everywhere. Prefer `.min.css` since
the file is already minified.
- [ ] **Consider extracting file-upload logic from `formulaire.php` into `Database`**
File validation, directory creation, and `insertThesisFile()` are scattered across
`formulaire.php`. Wrapping them in a `Database::attachFile()` or a dedicated
`FileUploadHandler` class would make `formulaire.php` much shorter and the upload
logic testable.
- [ ] **Unify `head.php` vs `header.php` templates**
The public site has both `templates/head.php` (shared `<head>` tag) and
`templates/header.php` (full `<head>` + `<body><header>`). `tfe.php` uses
`head.php` and renders its own `<body>`, while `index.php` uses `header.php`.
This split is confusing. Consider making `header.php` the single entry point.
- [ ] Add pagination to répertoire student index (currently capped at 100)
- [ ] Thumbnail generation / cover image support for home grid cards

View File

@@ -1,18 +1,14 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$pageTitle = "Ajout de TFE";
$pageTitle = "Ajouter un TFE";
// Load database helper
require_once __DIR__ . '/../../src/Database.php';
try {
@@ -24,239 +20,218 @@ try {
$formatTypes = $db->getAllFormatTypes();
} catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage());
die("Erreur lors du chargement du formulaire. Veuillez réessayer plus tard.");
die("Erreur lors du chargement du formulaire.");
}
// Get error message and preserved form data from session (if redirected back from error)
$error = isset($_SESSION["form_error"]) ? $_SESSION["form_error"] : null;
$formData = isset($_SESSION["form_data"]) ? $_SESSION["form_data"] : [];
$error = $_SESSION["form_error"] ?? null;
$formData = $_SESSION["form_data"] ?? [];
unset($_SESSION["form_error"], $_SESSION["form_data"]);
// Clear session data after retrieving
unset($_SESSION["form_error"]);
unset($_SESSION["form_data"]);
// Helper function to get old form value
function old($key, $default = "")
{
function old($key, $default = "") {
global $formData;
return isset($formData[$key])
? htmlspecialchars($formData[$key])
: $default;
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
}
// Helper function to check if value was previously selected
function wasSelected($key, $value)
{
function wasSelected($key, $value) {
global $formData;
if (!isset($formData[$key])) {
return false;
}
if (is_array($formData[$key])) {
return in_array($value, $formData[$key]);
}
if (!isset($formData[$key])) return false;
if (is_array($formData[$key])) return in_array($value, $formData[$key]);
return $formData[$key] == $value;
}
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main>
<main class="admin-main">
<h1 class="admin-page-title">Ajouter un TFE</h1>
<?php if ($error): ?>
<div class="error-message">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data">
<!-- CSRF Protection -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(
$_SESSION["csrf_token"],
); ?>">
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<!-- Titre -->
<div class="admin-form-row">
<label class="admin-label" for="titre">Titre :</label>
<input class="admin-input" type="text" id="titre" name="titre"
value="<?= old('titre') ?>" required>
</div>
<fieldset>
<legend>Informations de base</legend>
<label for="auteurice">Nom/Prénom/Pseudo *</label>
<input type="text" id="auteurice" name="auteurice" placeholder="Nom de l'auteur·ice" value="<?php echo old(
"auteurice",
); ?>" required>
<br>
<label for="mail">Contact (email, site web, insta, ...)</label>
<input type="text" id="mail" name="mail" placeholder="votre.email@example.com ou @instagram" value="<?php echo old(
"mail",
); ?>">
<br>
<label for="année">Année diplômante *</label>
<input type="number" id="année" name="année" min="2000" max="<?php echo date(
"Y",
) + 1; ?>" placeholder="<?php echo date(
"Y",
); ?>" value="<?php echo old("année"); ?>" required>
</fieldset>
<!-- Sous-titre -->
<div class="admin-form-row">
<label class="admin-label" for="subtitle">Sous-titre (si applicable) :</label>
<input class="admin-input" type="text" id="subtitle" name="subtitle"
value="<?= old('subtitle') ?>">
</div>
<!-- Auteur·ice -->
<div class="admin-form-row">
<label class="admin-label" for="auteurice">Auteur·ice(s) :</label>
<input class="admin-input" type="text" id="auteurice" name="auteurice"
value="<?= old('auteurice') ?>" required>
</div>
<fieldset>
<legend>Informations académiques</legend>
<label for="orientation">Orientation principale *</label>
<select id="orientation" name="orientation" required>
<option value="">-- Sélectionner une orientation --</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo htmlspecialchars(
$orientation["id"],
); ?>" <?php echo wasSelected(
"orientation",
$orientation["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars(
$orientation["name"],
); ?>
<!-- Contact -->
<div class="admin-form-row">
<label class="admin-label" for="mail">Contact(s) (optionnel) [mail/site/insta/etc.] :</label>
<input class="admin-input" type="text" id="mail" name="mail"
value="<?= old('mail') ?>">
</div>
<!-- Promoteur interne -->
<div class="admin-form-row">
<label class="admin-label" for="promoteurice">Promoteur·ice interne :</label>
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
value="<?= old('promoteurice') ?>">
</div>
<!-- Promoteur externe -->
<div class="admin-form-row">
<label class="admin-label" for="promoteurice_externe">Promoteur·ice externe :</label>
<input class="admin-input" type="text" id="promoteurice_externe" name="promoteurice_externe"
value="<?= old('promoteurice_externe') ?>">
</div>
<!-- Année -->
<div class="admin-form-row">
<label class="admin-label" for="année">Année :</label>
<input class="admin-input" type="number" id="année" name="année"
min="2000" max="<?= date('Y') + 1 ?>"
placeholder="<?= date('Y') ?>"
value="<?= old('année') ?>" required>
</div>
<!-- Orientation -->
<div class="admin-form-row">
<label class="admin-label" for="orientation">Orientation :</label>
<select class="admin-select" id="orientation" name="orientation" required>
<option value=""></option>
<?php foreach ($orientations as $o): ?>
<option value="<?= htmlspecialchars($o['id']) ?>"
<?= wasSelected('orientation', $o['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<br>
<label for="ap">Atelier Pratique (AP) *</label>
<select id="ap" name="ap" required>
<option value="">-- Sélectionner un AP --</option>
<!-- AP -->
<div class="admin-form-row">
<label class="admin-label" for="ap">Atelier pluridisciplinaire :</label>
<select class="admin-select" id="ap" name="ap" required>
<option value=""></option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo htmlspecialchars(
$ap["id"],
); ?>" <?php echo wasSelected("ap", $ap["id"])
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($ap["name"]); ?>
<?php if (
$ap["code"]
): ?> (<?php echo htmlspecialchars(
$ap["code"],
); ?>)<?php endif; ?>
<option value="<?= htmlspecialchars($ap['id']) ?>"
<?= wasSelected('ap', $ap['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?><?php if ($ap['code']): ?> (<?= htmlspecialchars($ap['code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<br>
<label for="finality">Finalité du master *</label>
<select id="finality" name="finality" required>
<option value="">-- Sélectionner une finalité --</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo htmlspecialchars(
$finality["id"],
); ?>" <?php echo wasSelected(
"finality",
$finality["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($finality["name"]); ?>
<!-- Finalité -->
<div class="admin-form-row">
<label class="admin-label" for="finality">Finalité du master :</label>
<select class="admin-select" id="finality" name="finality" required>
<option value=""></option>
<?php foreach ($finalityTypes as $f): ?>
<option value="<?= htmlspecialchars($f['id']) ?>"
<?= wasSelected('finality', $f['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($f['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<br>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" placeholder="Nom du/de la promoteur·ice (si plusieurs, séparer par des virgules)" value="<?php echo old(
"promoteurice",
); ?>">
</fieldset>
<fieldset>
<legend>À propos du TFE</legend>
<label for="titre">Titre du mémoire *</label>
<input type="text" id="titre" name="titre" placeholder="Titre de votre TFE" value="<?php echo old(
"titre",
); ?>" required>
<br>
<label for="subtitle">Sous-titre (si applicable)</label>
<input type="text" id="subtitle" name="subtitle" placeholder="Sous-titre de votre TFE" value="<?php echo old(
"subtitle",
); ?>">
<br>
<label for="synopsis">Synopsis (environ 200 mots) *</label>
<textarea id="synopsis" name="synopsis" rows="8" placeholder="Décrivez votre TFE en quelques paragraphes..." required><?php echo old(
"synopsis",
); ?></textarea>
<br>
<label for="problématique">Problématique</label>
<textarea id="problématique" name="problématique" rows="4" placeholder="La problématique principale de votre mémoire..."><?php echo old(
"problématique",
); ?></textarea>
<br>
<label>Langue(s) du TFE * (sélection multiple possible)</label>
<ul class="no-style">
<?php foreach ($languages as $language): ?>
<li>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo htmlspecialchars(
$language["id"],
); ?>" <?php echo wasSelected(
"languages",
$language["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($language["name"]); ?>
<!-- Langue(s) -->
<div class="admin-form-row">
<label class="admin-label">Langue(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($languages as $lang): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="languages[]"
value="<?= htmlspecialchars($lang['id']) ?>"
<?= wasSelected('languages', $lang['id']) ? 'checked' : '' ?>>
<?= htmlspecialchars($lang['name']) ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<br>
<label>Format(s) (sélection multiple possible)</label>
<ul class="no-style">
<?php foreach ($formatTypes as $format): ?>
<li>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo htmlspecialchars(
$format["id"],
); ?>" <?php echo wasSelected(
"formats",
$format["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($format["name"]); ?>
<!-- Format(s) -->
<div class="admin-form-row">
<label class="admin-label">Format(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($formatTypes as $fmt): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="formats[]"
value="<?= htmlspecialchars($fmt['id']) ?>"
<?= wasSelected('formats', $fmt['id']) ? 'checked' : '' ?>>
<?= htmlspecialchars($fmt['name']) ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<br>
<label for="tag">Mots-clés (max 10, séparés par des virgules)</label>
<input type="text" id="tag" name="tag" placeholder="typographie, photographie, outils libre, post-colonial..." value="<?php echo old(
"tag",
); ?>">
<br>
<small>Séparez les mots-clés par des virgules. Maximum 10 mots-clés.</small>
<label for="duration_info">Durée/Taille (si applicable)</label>
<input type="text" id="duration_info" name="duration_info" placeholder="Ex: 68 minutes, 128 pages, 78 pages + 15 minutes" value="<?php echo old(
"duration_info",
); ?>">
<br>
<small>Indiquez la durée (en minutes) ou le nombre de pages de votre TFE.</small>
<label for="lien">Lien vers un site web ou ressource en ligne</label>
<input type="url" id="lien" name="lien" placeholder="https://monmemoire.erg.be/..." value="<?php echo old(
"lien",
); ?>">
</fieldset>
<!-- Mots-clés -->
<div class="admin-form-row">
<label class="admin-label" for="tag">Mots-clés :</label>
<div>
<input class="admin-input" type="text" id="tag" name="tag"
placeholder="sociologie, anthropologie, ..."
value="<?= old('tag') ?>">
<p class="admin-hint">Séparez par des virgules. Max 10 mots-clés.</p>
</div>
</div>
<!-- Synopsis -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label" for="synopsis">Synopsis :</label>
<textarea class="admin-textarea" id="synopsis" name="synopsis"
rows="7" required><?= old('synopsis') ?></textarea>
</div>
<fieldset>
<legend>Fichiers</legend>
<label for="couverture">Importer une image de couverture</label>
<small>Formats acceptés : JPG, PNG. Taille max : 10MB.</small>
<!-- Durée/Taille -->
<div class="admin-form-row">
<label class="admin-label" for="duration_info">Durée / Taille :</label>
<div>
<input class="admin-input" type="text" id="duration_info" name="duration_info"
placeholder="Ex : 84 pages"
value="<?= old('duration_info') ?>">
<p class="admin-hint">Durée (minutes) ou nombre de pages.</p>
</div>
</div>
<!-- Lien -->
<div class="admin-form-row">
<label class="admin-label" for="lien">Lien (site / ressource) :</label>
<input class="admin-input" type="url" id="lien" name="lien"
placeholder="https://..."
value="<?= old('lien') ?>">
</div>
<!-- Image couverture -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Image de couverture :</label>
<div class="admin-file-input">
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png">
<p class="admin-hint">JPG, PNG. Taille max : 10 MB.</p>
</div>
</div>
<label for="files">Importer le TFE et les fichiers annexes</label>
<small>Formats acceptés : PDF, JPG, PNG, MP4, ZIP. Taille max par fichier : 50MB.</small>
<small>Si vous voulez importer un dossier, créez une archive ZIP.</small>
<input type="file" id="files" name="files[]" multiple accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
</fieldset>
<!-- Fichiers -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Fichiers du TFE :</label>
<div class="admin-file-input">
<input type="file" id="files" name="files[]" multiple
accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
<p class="admin-hint">PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier.</p>
</div>
</div>
<br>
<input type="submit" name="go" value="Soumettre mon TFE">
<div class="admin-submit-wrap">
<button type="submit" name="go" class="admin-btn">Soumettre</button>
</div>
</form>
</main>

View File

@@ -175,146 +175,155 @@ try {
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main>
<main class="admin-main">
<h1 class="admin-page-title">Modifier un TFE</h1>
<?php if ($error): ?>
<div class="alert-error">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert-success">
<strong>✓ <?php echo htmlspecialchars($success); ?></strong>
</div>
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<form method="post" action="edit.php?id=<?php echo $thesisId; ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<form method="post" action="edit.php?id=<?= $thesisId ?>" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<h2>Informations de base</h2>
<div class="admin-form-row">
<label class="admin-label" for="auteurice">Auteur·ice(s) :</label>
<input class="admin-input" type="text" id="auteurice" name="auteurice"
value="<?= htmlspecialchars($thesis['authors']) ?>" required>
</div>
<fieldset>
<label for="auteurice">Nom/Prénom/Pseudo *</label>
<input type="text" id="auteurice" name="auteurice" value="<?php echo htmlspecialchars($thesis['authors']); ?>" required>
<small>Si plusieurs, séparer par des virgules</small>
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="mail">Contact :</label>
<input class="admin-input" type="text" id="mail" name="mail" value="">
</div>
<fieldset>
<label for="mail">Contact</label>
<input type="text" id="mail" name="mail" value="">
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="année">Année :</label>
<input class="admin-input" type="number" id="année" name="année"
value="<?= $thesis['year'] ?>" required>
</div>
<fieldset>
<label for="année">Année *</label>
<input type="number" id="année" name="année" value="<?php echo $thesis['year']; ?>" required>
</fieldset>
<h2>Informations académiques</h2>
<fieldset>
<label for="orientation">Orientation *</label>
<select id="orientation" name="orientation" required>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo ($thesis['orientation'] == $orientation['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
<div class="admin-form-row">
<label class="admin-label" for="orientation">Orientation :</label>
<select class="admin-select" id="orientation" name="orientation" required>
<?php foreach ($orientations as $o): ?>
<option value="<?= $o['id'] ?>"
<?= ($thesis['orientation'] == $o['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
</div>
<fieldset>
<label for="ap">Atelier Pratique *</label>
<select id="ap" name="ap" required>
<div class="admin-form-row">
<label class="admin-label" for="ap">Atelier pluridisciplinaire :</label>
<select class="admin-select" id="ap" name="ap" required>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo $ap['id']; ?>" <?php echo ($thesis['ap_program'] == $ap['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($ap['name']); ?>
<option value="<?= $ap['id'] ?>"
<?= ($thesis['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
</div>
<fieldset>
<label for="finality">Finalité *</label>
<select id="finality" name="finality" required>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo $finality['id']; ?>" <?php echo ($thesis['finality_type'] == $finality['name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($finality['name']); ?>
<div class="admin-form-row">
<label class="admin-label" for="finality">Finalité du master :</label>
<select class="admin-select" id="finality" name="finality" required>
<?php foreach ($finalityTypes as $f): ?>
<option value="<?= $f['id'] ?>"
<?= ($thesis['finality_type'] == $f['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($f['name']) ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
</div>
<fieldset>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" value="<?php echo htmlspecialchars($thesis['supervisors'] ?? ''); ?>">
<small>Si plusieurs, séparer par des virgules</small>
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="promoteurice">Promoteur·ice(s) :</label>
<input class="admin-input" type="text" id="promoteurice" name="promoteurice"
value="<?= htmlspecialchars($thesis['supervisors'] ?? '') ?>">
</div>
<h2>À propos du TFE</h2>
<div class="admin-form-row">
<label class="admin-label" for="titre">Titre :</label>
<input class="admin-input" type="text" id="titre" name="titre"
value="<?= htmlspecialchars($thesis['title']) ?>" required>
</div>
<fieldset>
<label for="titre">Titre *</label>
<input type="text" id="titre" name="titre" value="<?php echo htmlspecialchars($thesis['title']); ?>" required>
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="subtitle">Sous-titre :</label>
<input class="admin-input" type="text" id="subtitle" name="subtitle"
value="<?= htmlspecialchars($thesis['subtitle'] ?? '') ?>">
</div>
<fieldset>
<label for="subtitle">Sous-titre</label>
<input type="text" id="subtitle" name="subtitle" value="<?php echo htmlspecialchars($thesis['subtitle'] ?? ''); ?>">
</fieldset>
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label" for="synopsis">Synopsis :</label>
<textarea class="admin-textarea" id="synopsis" name="synopsis" rows="7" required><?= htmlspecialchars($thesis['synopsis'] ?? '') ?></textarea>
</div>
<fieldset>
<label for="synopsis">Synopsis *</label>
<textarea id="synopsis" name="synopsis" rows="8" required><?php echo htmlspecialchars($thesis['synopsis'] ?? ''); ?></textarea>
</fieldset>
<fieldset>
<label>Langue(s) *</label>
<?php foreach ($languages as $language): ?>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo $language['id']; ?>" <?php echo in_array($language['id'], $currentLanguages) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($language['name']); ?>
<div class="admin-form-row">
<label class="admin-label">Langue(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($languages as $lang): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="languages[]" value="<?= $lang['id'] ?>"
<?= in_array($lang['id'], $currentLanguages) ? 'checked' : '' ?>>
<?= htmlspecialchars($lang['name']) ?>
</label>
<?php endforeach; ?>
</fieldset>
</div>
</div>
<fieldset>
<label>Format(s)</label>
<?php foreach ($formatTypes as $format): ?>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo $format['id']; ?>" <?php echo in_array($format['id'], $currentFormats) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($format['name']); ?>
<div class="admin-form-row">
<label class="admin-label">Format(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($formatTypes as $fmt): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="formats[]" value="<?= $fmt['id'] ?>"
<?= in_array($fmt['id'], $currentFormats) ? 'checked' : '' ?>>
<?= htmlspecialchars($fmt['name']) ?>
</label>
<?php endforeach; ?>
</fieldset>
</div>
</div>
<fieldset>
<label for="tag">Mots-clés (max 10)</label>
<input type="text" id="tag" name="tag" value="<?php echo htmlspecialchars($thesis['keywords'] ?? ''); ?>">
<small>Séparer par des virgules</small>
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="tag">Mots-clés :</label>
<div>
<input class="admin-input" type="text" id="tag" name="tag"
value="<?= htmlspecialchars($thesis['keywords'] ?? '') ?>">
<p class="admin-hint">Séparer par des virgules. Max 10.</p>
</div>
</div>
<fieldset>
<label for="duration_info">Durée/Taille</label>
<input type="text" id="duration_info" name="duration_info" value="<?php echo htmlspecialchars($thesis['file_size_info'] ?? ''); ?>">
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="duration_info">Durée / Taille :</label>
<input class="admin-input" type="text" id="duration_info" name="duration_info"
value="<?= htmlspecialchars($thesis['file_size_info'] ?? '') ?>">
</div>
<fieldset>
<label for="lien">Lien externe</label>
<input type="url" id="lien" name="lien" value="<?php echo htmlspecialchars($thesis['baiu_link'] ?? ''); ?>">
</fieldset>
<div class="admin-form-row">
<label class="admin-label" for="lien">Lien externe :</label>
<input class="admin-input" type="url" id="lien" name="lien"
value="<?= htmlspecialchars($thesis['baiu_link'] ?? '') ?>">
</div>
<h2>Publication</h2>
<fieldset>
<label class="checkbox-label">
<input type="checkbox" name="is_published" value="1" <?php echo $thesis['is_published'] ? 'checked' : ''; ?>>
<span>Publier ce TFE sur le site public</span>
<div class="admin-form-row">
<label class="admin-label">Publication :</label>
<label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1"
<?= $thesis['is_published'] ? 'checked' : '' ?>>
Publier ce TFE sur le site public
</label>
<small>Si coché, ce TFE sera visible sur le site public. Sinon, il restera en attente.</small>
</fieldset>
</div>
<button type="submit">Enregistrer les modifications</button>
<a href="/admin/thanks.php?id=<?php echo $thesisId; ?>">Annuler</a>
<div class="admin-submit-wrap">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/thanks.php?id=<?= $thesisId ?>" class="admin-btn-secondary" style="margin-left:.75rem;">Annuler</a>
</div>
</form>
</main>

View File

@@ -279,72 +279,51 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main>
<h2>Importer des TFE depuis un fichier CSV</h2>
<main class="admin-main">
<h1 class="admin-page-title">Importer une liste de TFE</h1>
<?php if (!empty($errors)): ?>
<div class="alert-error">
<strong>⚠ Erreurs:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<div class="admin-alert admin-alert--error">
<strong>⚠ Erreurs :</strong>
<ul style="margin:.5rem 0 0;padding-left:1.2rem;">
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($message): ?>
<div class="alert-success">
<strong>✓ <?php echo htmlspecialchars($message); ?></strong>
</div>
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<form action="import.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<form action="import.php" method="post" enctype="multipart/form-data" class="admin-import-area">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<fieldset>
<legend>Sélectionner un fichier CSV</legend>
<p><strong>Format attendu:</strong></p>
<ul>
<li>Colonnes: Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU</li>
<li>Les deux premières lignes seront ignorées (entête)</li>
<li>Séparateur: virgule</li>
<li>Encodage: UTF-8</li>
</ul>
<label for="csv_file">Fichier CSV:</label>
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Fichier CSV :</label>
<div class="admin-file-input">
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
<div class="admin-hint" style="margin-top:.5rem;">
Colonnes attendues : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
— Deux premières lignes ignorées (en-tête) — Séparateur : virgule — Encodage : UTF-8
</div>
</div>
</div>
<button type="submit">Importer</button>
</fieldset>
<div style="margin-top:1rem;">
<button type="submit" class="admin-btn">Importer</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<h3>Résultats de l'import</h3>
<div style="margin-top:2rem;">
<h2 style="font-size:1rem;font-weight:600;margin-bottom:.75rem;color:var(--admin-text-muted);text-transform:uppercase;letter-spacing:.06em;">Résultats de l'import</h2>
<div class="info-message">
<pre><?php
foreach ($importResults as $result) {
echo htmlspecialchars($result) . "\n";
}
?></pre>
<pre><?php foreach ($importResults as $r) echo htmlspecialchars($r) . "\n"; ?></pre>
</div>
</div>
<?php endif; ?>
<hr>
<h3>Notes importantes</h3>
<ul>
<li><strong>Codes orientation:</strong> SC (Sculpture), VI (Vidéographie), CA (Cinéma d'animation), IP (Installation-Performance), etc.</li>
<li><strong>Codes AP:</strong> DPM, LIENS, APS (comme dans la base)</li>
<li><strong>Auteurs multiples:</strong> Séparer par des virgules</li>
<li><strong>Mots-clés:</strong> Maximum 10, séparés par des virgules</li>
<li><strong>Formats:</strong> Séparer par des virgules</li>
<li>Les lignes avec erreurs seront ignorées et loggées</li>
</ul>
<h3>Exemple de fichier CSV</h3>
<p>Voir: <code>../db/Database_TFE_test.csv</code></p>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -1,38 +1,25 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$pageTitle = "Liste des TFE";
require_once __DIR__ . '/../../src/Database.php';
try {
$db = new Database();
// Get filter parameters
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
$filters = [];
if ($searchQuery) {
$filters['search'] = $searchQuery;
}
if ($yearFilter) {
$filters['year'] = $yearFilter;
}
if ($orientationFilter) {
$filters['orientation'] = $orientationFilter;
}
if ($searchQuery) $filters['search'] = $searchQuery;
if ($yearFilter) $filters['year'] = $yearFilter;
if ($orientationFilter) $filters['orientation'] = $orientationFilter;
$theses = $db->getThesesList($filters);
$years = $db->getAllYears();
@@ -42,159 +29,113 @@ try {
die("Erreur lors du chargement de la liste.");
}
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<script>
function toggleAll(source) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]');
checkboxes.forEach(checkbox => {
checkbox.checked = source.checked;
});
updateBulkActionsVisibility();
function toggleAll(src) {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.checked = src.checked);
updateBulk();
}
function updateBulkActionsVisibility() {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
if (checkboxes.length > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = checkboxes.length;
} else {
bulkActions.style.display = 'none';
function updateBulk() {
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
const bulk = document.getElementById('bulk-actions');
document.getElementById('selected-count').textContent = checked.length;
bulk.style.display = checked.length > 0 ? 'flex' : 'none';
}
}
function bulkAction(action) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
if (checkboxes.length === 0) {
alert('Veuillez sélectionner au moins un TFE.');
return false;
}
const actionText = action === 'publish' ? 'publier' : 'dépublier';
if (!confirm(`Voulez-vous vraiment ${actionText} ${checkboxes.length} TFE(s) ?`)) {
return false;
}
// Set action
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
if (!checked.length) { alert('Sélectionnez au moins un TFE.'); return; }
const word = action === 'publish' ? 'publier' : 'dépublier';
if (!confirm(`${word.charAt(0).toUpperCase()+word.slice(1)} ${checked.length} TFE(s) ?`)) return;
document.getElementById('bulk-action-input').value = action;
// Copy selected thesis IDs to hidden form
const bulkCheckboxesContainer = document.getElementById('bulk-checkboxes');
bulkCheckboxesContainer.innerHTML = '';
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_theses[]';
input.value = checkbox.value;
bulkCheckboxesContainer.appendChild(input);
const container = document.getElementById('bulk-checkboxes');
container.innerHTML = '';
checked.forEach(cb => {
const inp = document.createElement('input');
inp.type = 'hidden'; inp.name = 'selected_theses[]'; inp.value = cb.value;
container.appendChild(inp);
});
// Submit the form
document.getElementById('bulk-form').submit();
return false;
}
document.addEventListener('DOMContentLoaded', function() {
// Add change listeners to all checkboxes
document.querySelectorAll('input[name="selected_theses[]"]').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkActionsVisibility);
});
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
});
</script>
<main>
<main class="admin-main">
<h1 class="admin-page-title">Liste des TFE</h1>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert-error">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($_SESSION['error']);
unset($_SESSION['error']); ?>
</div>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?></div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert-success">
<strong>✓ <?php echo htmlspecialchars($_SESSION['success']);
unset($_SESSION['success']); ?></strong>
</div>
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?></div>
<?php endif; ?>
<div id="bulk-actions" class="bulk-actions">
<!-- Stats -->
<div class="admin-stats">
<div class="admin-stat">
<div class="admin-stat__number"><?= count($theses) ?></div>
<div class="admin-stat__label">TFE total</div>
</div>
<div class="admin-stat">
<div class="admin-stat__number"><?= count(array_filter($theses, fn($t) => $t['is_published'])) ?></div>
<div class="admin-stat__label">Publiés</div>
</div>
<div class="admin-stat">
<div class="admin-stat__number"><?= count(array_filter($theses, fn($t) => !$t['is_published'])) ?></div>
<div class="admin-stat__label">En attente</div>
</div>
</div>
<!-- Filters -->
<form class="admin-filters" method="get" action="/admin/">
<input type="text" name="search" placeholder="Titre, auteur..."
value="<?= htmlspecialchars($searchQuery) ?>">
<select name="year">
<option value="">Toutes les années</option>
<?php foreach ($years as $y): ?>
<option value="<?= $y ?>" <?= $yearFilter == $y ? 'selected' : '' ?>><?= $y ?></option>
<?php endforeach; ?>
</select>
<select name="orientation">
<option value="">Toutes les orientations</option>
<?php foreach ($orientations as $o): ?>
<option value="<?= $o['id'] ?>" <?= $orientationFilter == $o['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="admin-filters-btn">Filtrer</button>
<?php if ($searchQuery || $yearFilter || $orientationFilter): ?>
<a href="/admin/" class="admin-filters-reset">Réinitialiser</a>
<?php endif; ?>
</form>
<!-- Bulk actions bar -->
<div id="bulk-actions" class="admin-bulk-actions">
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
<div class="bulk-actions-buttons">
<button type="button" class="btn-bulk-publish" onclick="bulkAction('publish')">Publier la sélection</button>
<button type="button" class="btn-bulk-unpublish" onclick="bulkAction('unpublish')">Dépublier la sélection</button>
<div class="admin-bulk-btns">
<button type="button" class="admin-btn-sm admin-btn-publish" onclick="bulkAction('publish')">Publier</button>
<button type="button" class="admin-btn-sm admin-btn-unpublish" onclick="bulkAction('unpublish')">Dépublier</button>
</div>
</div>
<form id="bulk-form" method="post" action="actions/publish.php">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" id="bulk-action-input" name="action" value="">
<input type="hidden" name="bulk" value="1">
<div id="bulk-checkboxes"></div>
</form>
<div class="stats">
<div class="stat-card">
<div class="stat-number"><?php echo count($theses); ?></div>
<div class="stat-label">TFE total</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => $t['is_published'])); ?></div>
<div class="stat-label">Publiés</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => !$t['is_published'])); ?></div>
<div class="stat-label">En attente</div>
</div>
</div>
<div class="filters">
<form method="get" action="/admin/">
<fieldset>
<label for="search">Rechercher</label>
<input type="text" id="search" name="search" placeholder="Titre, auteur..." value="<?php echo htmlspecialchars($searchQuery); ?>">
</fieldset>
<fieldset>
<label for="year">Année</label>
<select id="year" name="year">
<option value="">Toutes</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $yearFilter == $year ? 'selected' : ''; ?>>
<?php echo $year; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="orientation">Orientation</label>
<select id="orientation" name="orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo $orientationFilter == $orientation['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<button type="submit">Filtrer</button>
<?php if ($searchQuery || $yearFilter || $orientationFilter): ?>
<a href="/admin/">Réinitialiser</a>
<?php endif; ?>
</form>
</div>
<!-- Table -->
<?php if (empty($theses)): ?>
<p>Aucun TFE trouvé.</p>
<p style="color:var(--admin-text-muted);padding:1rem 0;">Aucun TFE trouvé.</p>
<?php else: ?>
<table class="thesis-table">
<table class="admin-table">
<thead>
<tr>
<th><input type="checkbox" class="select-all-checkbox" onchange="toggleAll(this)" title="Tout sélectionner"></th>
<th><input type="checkbox" onchange="toggleAll(this)"></th>
<th>ID</th>
<th>Titre</th>
<th>Auteur(s)</th>
@@ -208,18 +149,18 @@ try {
<tbody>
<?php foreach ($theses as $thesis): ?>
<tr>
<td><input type="checkbox" class="select-checkbox" name="selected_theses[]" value="<?php echo $thesis['id']; ?>"></td>
<td><?php echo htmlspecialchars($thesis['identifier'] ?? $thesis['id']); ?></td>
<td><input type="checkbox" name="selected_theses[]" value="<?= $thesis['id'] ?>"></td>
<td style="color:var(--admin-text-muted);font-size:.8rem;"><?= htmlspecialchars($thesis['identifier'] ?? $thesis['id']) ?></td>
<td>
<div class="thesis-title"><?php echo htmlspecialchars($thesis['title']); ?></div>
<div class="thesis-title"><?= htmlspecialchars($thesis['title']) ?></div>
<?php if ($thesis['subtitle']): ?>
<div class="thesis-subtitle"><?php echo htmlspecialchars($thesis['subtitle']); ?></div>
<div class="thesis-subtitle"><?= htmlspecialchars($thesis['subtitle']) ?></div>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($thesis['authors'] ?? 'N/A'); ?></td>
<td><?php echo $thesis['year']; ?></td>
<td><?php echo htmlspecialchars($thesis['orientation'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($thesis['ap_program'] ?? 'N/A'); ?></td>
<td><?= htmlspecialchars($thesis['authors'] ?? 'N/A') ?></td>
<td><?= $thesis['year'] ?></td>
<td><?= htmlspecialchars($thesis['orientation'] ?? 'N/A') ?></td>
<td><?= htmlspecialchars($thesis['ap_program'] ?? 'N/A') ?></td>
<td>
<?php if ($thesis['is_published']): ?>
<span class="status-badge status-published">Publié</span>
@@ -228,18 +169,19 @@ try {
<?php endif; ?>
</td>
<td>
<div class="actions">
<a href="/admin/thanks.php?id=<?php echo $thesis['id']; ?>" class="btn btn-view">Voir</a>
<a href="/admin/edit.php?id=<?php echo $thesis['id']; ?>" class="btn btn-edit">Éditer</a>
<div class="admin-actions">
<a href="/admin/thanks.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm admin-btn-view">Voir</a>
<a href="/admin/edit.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm admin-btn-edit">Éditer</a>
<form method="post" action="actions/publish.php" class="publish-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="thesis_id" value="<?php echo $thesis['id']; ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="thesis_id" value="<?= $thesis['id'] ?>">
<?php if ($thesis['is_published']): ?>
<input type="hidden" name="action" value="unpublish">
<button type="submit" class="btn btn-unpublish" onclick="return confirm('Retirer ce TFE de la publication ?');">Dépublier</button>
<button type="submit" class="admin-btn-sm admin-btn-unpublish"
onclick="return confirm('Retirer de la publication ?')">Dépublier</button>
<?php else: ?>
<input type="hidden" name="action" value="publish">
<button type="submit" class="btn btn-publish">Publier</button>
<button type="submit" class="admin-btn-sm admin-btn-publish">Publier</button>
<?php endif; ?>
</form>
</div>

View File

@@ -2,13 +2,10 @@
require_once __DIR__ . '/../../config/bootstrap.php';
require_once __DIR__ . '/../../src/AdminAuth.php';
// If no password is configured, nothing to log into — go straight to admin.
if (!defined('ADMIN_PASSWORD_HASH')) {
header('Location: /admin/');
exit;
}
// Already authenticated — redirect to admin.
if (AdminAuth::isAuthenticated()) {
header('Location: /admin/');
exit;
@@ -21,7 +18,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: /admin/');
exit;
}
// Intentionally vague error — avoid user-enumeration.
$error = 'Mot de passe incorrect.';
}
@@ -31,30 +27,32 @@ $pageTitle = 'Connexion';
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle); ?> — Post-ERG Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Connexion Posterg Admin</title>
<link rel="stylesheet" href="/assets/modern-normalize.min.css">
<link rel="stylesheet" href="/assets/admin.css">
<link rel="shortcut icon" href="/assets/admin_favicon.svg" type="image/svg+xml">
</head>
<body>
<header>
<h1><?php echo htmlspecialchars($pageTitle); ?></h1>
</header>
<main>
<body class="admin-body">
<nav class="admin-nav">
<span class="admin-nav__logo">Posterg</span>
</nav>
<div class="admin-login-wrap">
<div class="admin-login-box">
<h2>Administration</h2>
<?php if ($error): ?>
<div class="alert-error">
<strong>⚠️ <?php echo htmlspecialchars($error); ?></strong>
</div>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="post" action="/admin/login.php">
<fieldset>
<legend>Authentification admin</legend>
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" required autofocus>
<button type="submit">Se connecter</button>
</fieldset>
<form method="post" action="/admin/login.php" class="admin-form">
<div class="admin-form-row" style="grid-template-columns:1fr;border:none;padding:.4rem 0;">
<label class="admin-label" style="font-size:.82rem;color:var(--admin-text-muted);" for="password">Mot de passe</label>
<input class="admin-input" type="password" id="password" name="password" required autofocus>
</div>
<div class="admin-submit-wrap" style="margin-top:1rem;padding-top:.5rem;">
<button type="submit" class="admin-btn" style="width:100%;">Se connecter</button>
</div>
</form>
</main>
</div>
</div>
</body>
</html>

View File

@@ -69,132 +69,92 @@ function formatFileSize($bytes) {
}
// Set page title for header
$pageTitle = "Merci";
$pageTitle = "Récapitulatif TFE";
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main>
<main class="admin-main">
<h1 class="admin-page-title">Récapitulatif TFE</h1>
<?php if ($error): ?>
<div class="error">
<p>⚠️ <?php echo htmlspecialchars($error); ?></p>
<p><a href="/admin/add.php">Retour au formulaire</a></p>
</div>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($error) ?></div>
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
<?php elseif ($thesis): ?>
<p>d'avoir soumis votre TFE. Les informations ont été enregistrées et sont en attente de traitement.</p>
<div class="thesis-info">
<h2>Récapitulatif de votre soumission</h2>
<h3>Informations de base</h3>
<div class="admin-thesis-info">
<h2>Informations de base</h2>
<dl>
<dt>Identifiant:</dt>
<dd><strong><?php echo htmlspecialchars($thesis['identifier']); ?></strong></dd>
<dt>Titre:</dt>
<dd><?php echo htmlspecialchars($thesis['title']); ?></dd>
<dt>Identifiant</dt><dd><?= htmlspecialchars($thesis['identifier']) ?></dd>
<dt>Titre</dt><dd><?= htmlspecialchars($thesis['title']) ?></dd>
<?php if ($thesis['subtitle']): ?>
<dt>Sous-titre:</dt>
<dd><?php echo htmlspecialchars($thesis['subtitle']); ?></dd>
<dt>Sous-titre</dt><dd><?= htmlspecialchars($thesis['subtitle']) ?></dd>
<?php endif; ?>
<dt>Auteur·ice(s):</dt>
<dd><?php echo htmlspecialchars($thesis['authors']); ?></dd>
<dt>Année:</dt>
<dd><?php echo htmlspecialchars($thesis['year']); ?></dd>
<dt>Auteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['authors']) ?></dd>
<dt>Année</dt><dd><?= htmlspecialchars($thesis['year']) ?></dd>
</dl>
</div>
<h3>Détails académiques</h3>
<div class="admin-thesis-info">
<h2>Détails académiques</h2>
<dl>
<dt>Orientation:</dt>
<dd><?php echo htmlspecialchars($thesis['orientation'] ?? 'Non spécifié'); ?></dd>
<dt>Atelier Pratique:</dt>
<dd><?php echo htmlspecialchars($thesis['ap_program'] ?? 'Non spécifié'); ?></dd>
<dt>Finalité:</dt>
<dd><?php echo htmlspecialchars($thesis['finality_type'] ?? 'Non spécifié'); ?></dd>
<dt>Orientation</dt><dd><?= htmlspecialchars($thesis['orientation'] ?? '') ?></dd>
<dt>Atelier pratique</dt><dd><?= htmlspecialchars($thesis['ap_program'] ?? '') ?></dd>
<dt>Finalité</dt><dd><?= htmlspecialchars($thesis['finality_type'] ?? '') ?></dd>
<?php if ($thesis['supervisors']): ?>
<dt>Promoteur·ice(s):</dt>
<dd><?php echo htmlspecialchars($thesis['supervisors']); ?></dd>
<dt>Promoteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['supervisors']) ?></dd>
<?php endif; ?>
</dl>
</div>
<h3>Contenu</h3>
<div class="admin-thesis-info">
<h2>Contenu</h2>
<dl>
<?php if ($thesis['synopsis']): ?>
<dt>Synopsis:</dt>
<dd><?php echo nl2br(htmlspecialchars($thesis['synopsis'])); ?></dd>
<?php endif; ?>
<?php if ($thesis['languages']): ?>
<dt>Langue(s):</dt>
<dd><?php echo htmlspecialchars($thesis['languages']); ?></dd>
<dt>Langue(s)</dt><dd><?= htmlspecialchars($thesis['languages']) ?></dd>
<?php endif; ?>
<?php if ($thesis['formats']): ?>
<dt>Format(s):</dt>
<dd><?php echo htmlspecialchars($thesis['formats']); ?></dd>
<dt>Format(s)</dt><dd><?= htmlspecialchars($thesis['formats']) ?></dd>
<?php endif; ?>
<?php if ($thesis['keywords']): ?>
<dt>Mots-clés:</dt>
<dd><?php echo htmlspecialchars($thesis['keywords']); ?></dd>
<dt>Mots-clés</dt><dd><?= htmlspecialchars($thesis['keywords']) ?></dd>
<?php endif; ?>
<?php if ($thesis['file_size_info']): ?>
<dt>Durée/Taille:</dt>
<dd><?php echo htmlspecialchars($thesis['file_size_info']); ?></dd>
<dt>Durée / Taille</dt><dd><?= htmlspecialchars($thesis['file_size_info']) ?></dd>
<?php endif; ?>
<?php if ($thesis['baiu_link']): ?>
<dt>Lien:</dt>
<dd><a href="<?php echo htmlspecialchars($thesis['baiu_link']); ?>" target="_blank" rel="noopener noreferrer">
<?php echo htmlspecialchars($thesis['baiu_link']); ?>
</a></dd>
<dt>Lien</dt><dd><a href="<?= htmlspecialchars($thesis['baiu_link']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($thesis['baiu_link']) ?></a></dd>
<?php endif; ?>
</dl>
</div>
<?php if (!empty($files)): ?>
<h3>Fichiers téléversés</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Nom du fichier</th>
<th>Taille</th>
<th>Date</th>
</tr>
</thead>
<div class="admin-thesis-info">
<h2>Fichiers</h2>
<table class="admin-table">
<thead><tr><th>Type</th><th>Fichier</th><th>Taille</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($files as $file): ?>
<?php foreach ($files as $f): ?>
<tr>
<td><?php echo htmlspecialchars($file['file_type']); ?></td>
<td><?php echo htmlspecialchars($file['file_name']); ?></td>
<td><?php echo formatFileSize($file['file_size']); ?></td>
<td><?php echo date('d/m/Y H:i', strtotime($file['uploaded_at'])); ?></td>
<td><?= htmlspecialchars($f['file_type']) ?></td>
<td><?= htmlspecialchars($f['file_name']) ?></td>
<td><?= formatFileSize($f['file_size']) ?></td>
<td><?= date('d/m/Y H:i', strtotime($f['uploaded_at'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<h3>Statut de publication</h3>
<p><strong>⏳ En attente</strong> - Votre TFE ne sera publié qu'après la soutenance et l'ajout éventuel d'une note contextuelle par le jury.</p>
<p class="submitted-date">
Soumis le <?php echo date('d/m/Y à H:i', strtotime($thesis['submitted_at'])); ?>
</p>
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap;">
<a href="/admin/edit.php?id=<?= $thesisId ?>" class="admin-btn">Modifier</a>
<a href="/admin/add.php" class="admin-btn-secondary">Ajouter un autre TFE</a>
<a href="/admin/" class="admin-btn-secondary">Retour à la liste</a>
</div>
<p><a href="/admin/add.php">Soumettre un autre TFE</a></p>
<?php else: ?>
<p>Aucune donnée à afficher.</p>
<p><a href="/admin/add.php">Retour au formulaire</a></p>
<p style="color:var(--admin-text-muted);">Aucune donnée à afficher.</p>
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
<?php endif; ?>
</main>

92
public/apropos.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
require_once __DIR__ . '/../config/bootstrap.php';
$currentNav = 'apropos';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>À Propos Posterg</title>
<link rel="stylesheet" href="assets/modern-normalize.min.css">
<link rel="stylesheet" href="assets/common.css">
<link rel="stylesheet" href="assets/apropos.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 class="apropos-body">
<?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
<main class="apropos-main">
<div class="apropos-layout">
<!-- LEFT: main text -->
<div class="apropos-left">
<div class="apropos-description">
<p>Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg École de Recherches Graphique de Bruxelles.</p>
<p>L'objectif est à la fois d'offrir une vitrine aux projets des anciens é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.</p>
</div>
<div class="apropos-licences">
<h2>Licences</h2>
<p>Les contenus de ce site sont publiés avec l'accord des auteur·ices et de l'erg. La reproduction des œuvres est soumise à l'autorisation de leurs auteur·ices respectif·ives.</p>
</div>
</div>
<!-- RIGHT: links, contacts, credits -->
<div class="apropos-right">
<div>
<h2 class="apropos-section-title">
<a href="https://erg.be" target="_blank" rel="noopener">Site de l'erg</a>
</h2>
</div>
<div>
<h2 class="apropos-section-title">Contacts</h2>
<div class="apropos-contact">
<span class="apropos-contact-name">Laurent Leprince</span>
<span class="apropos-contact-role">Bibliothèque d'architecture, d'ingénierie architecturale, d'urbanisme (BAIU) :</span>
<span class="apropos-contact-email">laurent.leprince@uclouvain.be</span>
</div>
<div class="apropos-contact">
<span class="apropos-contact-name">Xavier Gorgol</span>
<span class="apropos-contact-role">Responsable des mémoires de l'ERG :</span>
<span class="apropos-contact-email">xavier.gorgol@erg.be</span>
</div>
<div class="apropos-contact">
<span class="apropos-contact-name">Brigitte Ledune</span>
<span class="apropos-contact-role">Cours de suivi de mémoire :</span>
<span class="apropos-contact-email">brigitte.ledune@erg.be</span>
</div>
</div>
<div>
<h2 class="apropos-section-title">Crédits</h2>
<p class="apropos-credits-text">
Design &amp; développement : Olivia Marly, Théophile Gerveau-Mercie &amp; Théo Hennequin
</p>
<p class="apropos-credits-text">
Typographies : Ductus (Amélie Dumont) &amp; BBB DM Sans
</p>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -1,304 +1,473 @@
/* ============================================================
ADMIN SECTION
============================================================ */
:root {
--background-body: #3c856bff;
--background: #161f27;
--background-alt: #1a242f;
--selection: #1c76c5;
--text-main: #dbdbdb;
--text-bright: #fff;
--text-muted: #a9b1ba;
--links: #41adff;
--focus: #0096bfab;
--border: ;
--code: #ffbe85;
--animation-duration: 0.1s;
--button-base: #0c151c;
--button-hover: #040a0f;
--scrollbar-thumb: var(--button-hover);
--scrollbar-thumb-hover: color-mod(var(--scrollbar-thumb) lightness(-30%));
--form-placeholder: #a9a9a9;
--form-text: #fff;
--variable: #d941e2;
--highlight: #efdb43;
--select-arrow: svg-load("./assets/select-arrow.svg", fill: #efefef);
--admin-bg: #1a1a1a;
--admin-bg-alt: #242424;
--admin-border: #333;
--admin-text: #e8e8e8;
--admin-text-muted: #888;
--admin-purple: #9557b5;
--admin-input-bg: transparent;
}
/* @media (prefers-color-scheme: dark) { */
/* --background-body: #202b38; */
/* --background: #161f27; */
/* --background-alt: #1a242f; */
/* --selection: #1c76c5; */
/* --text-main: #dbdbdb; */
/* --text-bright: #fff; */
/* --text-muted: #a9b1ba; */
/* --links: #41adff; */
/* --focus: #0096bfab; */
/* --border: #526980; */
/* --code: #ffbe85; */
/* --animation-duration: 0.1s; */
/* --button-base: #0c151c; */
/* --button-hover: #040a0f; */
/* --scrollbar-thumb: var(--button-hover); */
/* --scrollbar-thumb-hover: color-mod(var(--scrollbar-thumb) lightness(-30%)); */
/* --form-placeholder: #a9a9a9; */
/* --form-text: #fff; */
/* --variable: #d941e2; */
/* --highlight: #efdb43; */
/* --select-arrow: svg-load("./assets/select-arrow.svg", fill: #efefef); */
/* } */
/* Base Styles */
body {
margin: auto;
padding: 0;
background-color: var(--background-body);
color: var(--text-main);
font-size: 1.2em;
}
header, main, footer {
margin: auto 2ch;
}
header, footer {
text-align: center;
}
main {
padding: 2ch;
max-width: 80vw;
margin: auto 1.2rem;
max-width: 800px;
margin: 20px auto;
padding: 0 10px;
position: relative;
}
table {
table-layout: auto;
margin: auto;
width: 95vw;
margin-left: calc(50% - 45vw);
/* margin: auto; */
}
table {
width: max-content;
min-width: 95vw; /* optional fallback */
table-layout: auto;
}
/* https://stackoverflow.com/questions/3084261/alternate-table-row-color-using-css */
tr:nth-child(even) {
background-color: #204639;
}
tr:nth-child(odd) {
background-color: #2f6a55;
}
nav {
margin-top: 1rem;
}
fieldset {
border: 1px solid;
border-radius: 6px;
html, body {
margin: 0;
margin-bottom: 12px;
padding: 1em;
padding: 0;
height: 100%;
}
legend {
font-size: 0.9em;
.admin-body {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--admin-bg);
color: var(--admin-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
/* Admin nav (dark version of site-nav) */
.admin-nav {
background: linear-gradient(to bottom, var(--admin-purple) 0%, rgba(149, 87, 181, 0.0) 100%);
padding: 0.55rem 1.5rem;
display: flex;
align-items: center;
gap: 2.5rem;
flex-shrink: 0;
}
.admin-nav__logo {
font-size: 0.88rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-weight: 400;
}
.admin-nav__link {
font-size: 0.85rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
font-weight: 400;
transition: opacity 0.15s;
}
.admin-nav__link:hover,
.admin-nav__link.active {
opacity: 1;
color: var(--white);
}
/* Main content area */
.admin-main {
flex: 1;
padding: 2.5rem 2rem 4rem;
max-width: 1100px;
width: 100%;
}
.admin-page-title {
font-size: 1.8rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--admin-text);
margin: 0 0 2.5rem 0;
}
label {
vertical-align: middle;
margin-bottom: 1em;
display: inline-block;
/* ---- Forms ---- */
.admin-form {
display: flex;
flex-direction: column;
gap: 0;
}
button,
select,
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="checkbox"],
input[type="range"],
input[type="radio"] {
.admin-form-row {
display: grid;
grid-template-columns: 260px 1fr;
align-items: start;
border-top: 1px solid var(--admin-border);
padding: 0.75rem 0;
gap: 1rem;
}
.admin-form-row:last-of-type {
border-bottom: 1px solid var(--admin-border);
}
.admin-label {
font-size: 0.92rem;
color: var(--admin-text);
padding-top: 0.5rem;
font-weight: 400;
}
.admin-input,
.admin-select,
.admin-textarea {
width: 100%;
background: var(--admin-input-bg);
border: none;
border-bottom: 1px solid var(--admin-border);
color: var(--admin-text);
font-size: 0.92rem;
font-family: inherit;
padding: 0.4rem 0;
outline: none;
border-radius: 0;
transition: border-color 0.15s;
-webkit-appearance: none;
appearance: none;
}
.admin-input:focus,
.admin-select:focus,
.admin-textarea:focus {
border-bottom-color: var(--admin-purple);
}
.admin-input::placeholder,
.admin-textarea::placeholder {
color: var(--admin-text-muted);
font-size: 0.88rem;
}
.admin-textarea {
resize: vertical;
min-height: 100px;
line-height: 1.5;
}
/* Select custom arrow */
.admin-select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0 center;
padding-right: 1.2rem;
}
.admin-select option {
background: var(--admin-bg);
color: var(--admin-text);
}
/* File inputs */
.admin-file-input {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.admin-file-input input[type="file"] {
font-size: 0.85rem;
color: var(--admin-text-muted);
background: transparent;
border: 1px dashed var(--admin-border);
padding: 0.4rem 0.6rem;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
}
.admin-file-input input[type="file"]:hover {
border-color: var(--admin-purple);
}
.admin-hint {
font-size: 0.78rem;
color: var(--admin-text-muted);
margin-top: 0.15rem;
}
/* Checkboxes & radios */
.admin-checkbox-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding-top: 0.3rem;
}
.admin-checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: var(--admin-text);
cursor: pointer;
}
input,
select {
display: block;
.admin-checkbox-label input[type="checkbox"] {
accent-color: var(--admin-purple);
width: 14px;
height: 14px;
cursor: pointer;
}
input,
button,
textarea,
select {
color: var(--form-text);
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
/* Submit button */
.admin-submit-wrap {
margin-top: 2rem;
padding-top: 1.5rem;
}
.admin-btn {
padding: 0.65rem 2.5rem;
background: var(--admin-purple);
color: #fff;
border: none;
border-radius: 6px;
outline: none;
border-radius: 3px;
font-size: 0.92rem;
font-family: inherit;
cursor: pointer;
letter-spacing: 0.04em;
transition: background 0.15s;
}
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
.admin-btn:hover {
background: #7b3fa0;
}
/* Alert Messages */
.error-message,
.alert-error {
background: #fee;
border: 2px solid #c00;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
color: #c00;
.admin-btn-secondary {
padding: 0.5rem 1.5rem;
background: transparent;
color: var(--admin-text-muted);
border: 1px solid var(--admin-border);
border-radius: 3px;
font-size: 0.88rem;
font-family: inherit;
cursor: pointer;
letter-spacing: 0.04em;
text-decoration: none;
display: inline-block;
transition: all 0.15s;
}
.success-message,
.alert-success {
background: #efe;
border: 2px solid #0a0;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
color: #0a0;
.admin-btn-secondary:hover {
border-color: var(--admin-text-muted);
color: var(--admin-text);
}
.info-message {
background: #f5f5f5;
padding: 1rem;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
/* ---- Alert Messages ---- */
.admin-alert {
padding: 0.75rem 1rem;
border-radius: 3px;
font-size: 0.9rem;
margin-bottom: 1.5rem;
border-left: 3px solid;
}
.info-message pre {
margin: 0;
font-size: 0.9em;
.admin-alert--error {
background: rgba(200, 0, 0, 0.1);
border-color: #c00;
color: #ff6b6b;
}
/* Lists */
/* ul.no-style { */
/* list-style: none; */
/* } */
/* Filters */
.filters {
padding: 1rem;
margin-bottom: 2rem;
border-radius: 4px;
.admin-alert--success {
background: rgba(0, 150, 80, 0.1);
border-color: #0a0;
color: #4caf50;
}
.filters form {
/* ---- Stats cards ---- */
.admin-stats {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
align-items: end;
}
.filters fieldset {
margin: 0;
padding: 0;
.admin-stat {
background: var(--admin-bg-alt);
border: 1px solid var(--admin-border);
border-radius: 4px;
padding: 1rem 1.5rem;
min-width: 140px;
}
.admin-stat__number {
font-size: 2rem;
font-weight: 700;
color: var(--admin-purple);
line-height: 1;
}
.admin-stat__label {
font-size: 0.82rem;
color: var(--admin-text-muted);
margin-top: 0.25rem;
}
/* ---- Filters bar ---- */
.admin-filters {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
align-items: flex-end;
}
.admin-filters input[type="text"],
.admin-filters select {
background: var(--admin-bg-alt);
border: 1px solid var(--admin-border);
border-radius: 3px;
color: var(--admin-text);
font-size: 0.88rem;
font-family: inherit;
padding: 0.45rem 0.75rem;
outline: none;
cursor: pointer;
}
.admin-filters input[type="text"]:focus,
.admin-filters select:focus {
border-color: var(--admin-purple);
}
.admin-filters-btn {
padding: 0.45rem 1rem;
background: var(--admin-purple);
color: #fff;
border: none;
min-width: 200px;
border-radius: 3px;
font-size: 0.88rem;
font-family: inherit;
cursor: pointer;
}
/* Tables */
.thesis-table {
.admin-filters-reset {
font-size: 0.88rem;
color: var(--admin-text-muted);
text-decoration: underline;
cursor: pointer;
}
/* ---- Table ---- */
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.thesis-table th,
.thesis-table td {
padding: 0.75rem;
.admin-table th {
text-align: left;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--admin-text-muted);
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--admin-border);
font-weight: 400;
white-space: nowrap;
}
.thesis-table th {
font-weight: bold;
.admin-table td {
padding: 0.65rem 0.75rem;
border-bottom: 1px solid var(--admin-border);
color: var(--admin-text);
vertical-align: top;
}
.thesis-title {
font-weight: bold;
.admin-table tr:hover td {
background: var(--admin-bg-alt);
}
.thesis-subtitle {
.admin-table .thesis-title {
font-weight: 500;
color: var(--admin-text);
}
.admin-table .thesis-subtitle {
font-size: 0.82rem;
color: var(--admin-text-muted);
font-style: italic;
color: #666;
font-size: 0.9em;
}
/* Status Badges */
/* Status badges */
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.85em;
}
.status-pending {
background: #ffd700;
color: #000;
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.04em;
}
.status-published {
background: #90ee90;
color: #000;
background: rgba(0, 150, 80, 0.15);
color: #4caf50;
}
/* Buttons */
.actions {
.status-pending {
background: rgba(255, 200, 0, 0.12);
color: #ffc107;
}
/* Action buttons in table */
.admin-actions {
display: flex;
gap: 0.5rem;
gap: 0.4rem;
flex-wrap: wrap;
}
.btn {
padding: 0.35rem 0.75rem;
.admin-btn-sm {
padding: 0.25rem 0.6rem;
border-radius: 3px;
font-size: 0.78rem;
font-family: inherit;
cursor: pointer;
text-decoration: none;
font-size: 0.9em;
display: inline-block;
border: 1px solid transparent;
transition: all 0.15s;
white-space: nowrap;
}
.btn-view {
background: #4a90e2;
color: white;
.admin-btn-view {
background: rgba(65, 173, 255, 0.15);
color: #41adff;
border-color: rgba(65, 173, 255, 0.3);
}
.btn-edit {
background: #f39c12;
color: white;
.admin-btn-view:hover {
background: rgba(65, 173, 255, 0.25);
}
.btn-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
.admin-btn-edit {
background: rgba(243, 156, 18, 0.15);
color: #f39c12;
border-color: rgba(243, 156, 18, 0.3);
}
.btn-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
.admin-btn-edit:hover {
background: rgba(243, 156, 18, 0.25);
}
.admin-btn-publish {
background: rgba(0, 150, 80, 0.15);
color: #4caf50;
border-color: rgba(0, 150, 80, 0.3);
border: 1px solid rgba(0, 150, 80, 0.3);
}
.admin-btn-publish:hover {
background: rgba(0, 150, 80, 0.25);
}
.admin-btn-unpublish {
background: rgba(149, 165, 166, 0.15);
color: #95a5a6;
border-color: rgba(149, 165, 166, 0.3);
border: 1px solid rgba(149, 165, 166, 0.3);
}
.admin-btn-unpublish:hover {
background: rgba(149, 165, 166, 0.25);
}
.publish-form {
@@ -306,162 +475,112 @@ input[type="button"] {
margin: 0;
}
/* Statistics */
.stats {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.stat-card {
background: darkslateblue;
padding: 1rem;
border-radius: 4px;
min-width: 150px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
}
.stat-label {
font-size: 0.9em;
}
/* Bulk Actions */
.bulk-actions {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
display: flex;
gap: 1rem;
/* Bulk actions */
.admin-bulk-actions {
display: none;
align-items: center;
gap: 1rem;
padding: 0.6rem 1rem;
background: var(--admin-bg-alt);
border: 1px solid var(--admin-border);
border-radius: 4px;
margin-bottom: 1rem;
font-size: 0.88rem;
}
.bulk-actions-buttons {
.admin-bulk-btns {
display: flex;
gap: 0.5rem;
}
.btn-bulk-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.btn-bulk-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.select-checkbox {
cursor: pointer;
}
.select-all-checkbox {
cursor: pointer;
}
#bulk-actions {
display: none;
}
#bulk-form {
display: none;
}
/* Thesis Info (Thanks page) */
.thesis-info {
/* background: #f5f5f5; */
border: 1px white solid;
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.thesis-info h2 {
margin-top: 0;
border-bottom: 2px solid #333;
padding-bottom: 0.5rem;
}
.thesis-info h3 {
margin-top: 2rem;
margin-bottom: 1rem;
/* color: #555; */
}
.thesis-info dl {
display: grid;
grid-template-columns: 200px 1fr;
gap: 0.5rem 1rem;
/* Thesis info (thanks page) */
.admin-thesis-info {
border: 1px solid var(--admin-border);
border-radius: 6px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.thesis-info dt {
font-weight: bold;
/* color: #666; */
.admin-thesis-info h2 {
margin: 0 0 1rem;
font-size: 1.2rem;
border-bottom: 1px solid var(--admin-border);
padding-bottom: 0.5rem;
}
.thesis-info dd {
.admin-thesis-info dl {
display: grid;
grid-template-columns: 180px 1fr;
gap: 0.4rem 1rem;
}
.admin-thesis-info dt {
font-weight: 600;
font-size: 0.88rem;
color: var(--admin-text-muted);
}
.admin-thesis-info dd {
margin: 0;
font-size: 0.9rem;
}
.thesis-info table {
width: 100%;
margin-top: 1rem;
/* Info/error messages */
.info-message {
background: var(--admin-bg-alt);
border: 1px solid var(--admin-border);
border-radius: 4px;
padding: 1rem;
font-size: 0.9rem;
}
.thesis-info table th {
text-align: left;
background: #ddd;
padding: 0.5rem;
.info-message pre {
margin: 0;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
}
.thesis-info table td {
padding: 0.5rem;
border-bottom: 1px solid #ddd;
}
.submitted-date {
margin-top: 2rem;
font-style: italic;
color: #666;
}
.error {
background: #fee;
border: 2px solid #c00;
padding: 1.5rem;
border-radius: 8px;
color: #c00;
}
/* Form Elements */
label.checkbox-label {
/* Login page */
.admin-login-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: center;
min-height: calc(100vh - 60px);
}
/* Responsive */
@media (max-width: 768px) {
.thesis-info dl {
.admin-login-box {
background: var(--admin-bg-alt);
border: 1px solid var(--admin-border);
border-radius: 6px;
padding: 2rem;
width: 100%;
max-width: 380px;
}
.admin-login-box h2 {
margin: 0 0 1.5rem;
font-size: 1.1rem;
font-weight: 500;
text-align: center;
}
.admin-login-box .admin-form-row {
grid-template-columns: 1fr;
gap: 0.25rem;
border: none;
padding: 0.4rem 0;
}
.thesis-info dt {
margin-top: 1rem;
.admin-login-box .admin-label {
font-size: 0.82rem;
color: var(--admin-text-muted);
padding: 0;
margin-bottom: 0.2rem;
}
/* Import page */
.admin-import-area {
display: flex;
flex-direction: column;
gap: 1.5rem;
}

139
public/assets/apropos.css Normal file
View File

@@ -0,0 +1,139 @@
/* ============================================================
À PROPOS PAGE (apropos.php)
============================================================ */
html, body {
margin: 0;
padding: 0;
height: 100%;
}
.apropos-body {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--white);
}
.apropos-main {
flex: 1;
overflow-y: auto;
padding: 2.5rem 1.5rem 4rem;
}
/* Two-column layout */
.apropos-layout {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 4rem;
max-width: 1200px;
}
/* Left col — main description text in big monospace */
.apropos-left {}
.apropos-description {
font-family: "Courier New", Courier, monospace;
font-size: 1.55rem;
line-height: 1.45;
color: var(--black);
font-weight: 400;
margin: 0 0 2rem 0;
}
.apropos-description p {
margin: 0 0 1.2em 0;
}
/* Right col — links, contacts, credits */
.apropos-right {
display: flex;
flex-direction: column;
gap: 2rem;
}
.apropos-section-title {
font-family: "Courier New", Courier, monospace;
font-size: 1.55rem;
font-weight: 400;
color: var(--black);
margin: 0 0 0.5rem 0;
line-height: 1.2;
}
.apropos-section-title a {
color: inherit;
text-decoration: underline;
text-underline-offset: 3px;
}
.apropos-contact {
margin-bottom: 1rem;
}
.apropos-contact-name {
font-weight: 700;
font-size: 0.95rem;
color: var(--black);
display: block;
margin-bottom: 0.15rem;
}
.apropos-contact-role,
.apropos-contact-email {
font-size: 0.9rem;
color: var(--black);
line-height: 1.4;
display: block;
}
.apropos-credits-text {
font-size: 0.9rem;
color: var(--black);
line-height: 1.6;
}
/* Licences section */
.apropos-licences {
margin-top: 2rem;
}
.apropos-licences h2 {
font-family: "Courier New", Courier, monospace;
font-size: 1.55rem;
font-weight: 400;
margin: 0 0 0.75rem 0;
}
.apropos-licences p {
font-size: 0.9rem;
color: var(--black);
line-height: 1.6;
margin: 0 0 0.75rem 0;
}
/* Responsive */
@media (max-width: 900px) {
.apropos-layout {
grid-template-columns: 1fr;
gap: 2rem;
}
.apropos-description {
font-size: 1.2rem;
}
.apropos-section-title {
font-size: 1.2rem;
}
}
@media (max-width: 600px) {
.apropos-main {
padding: 1.5rem 1rem 3rem;
}
.apropos-description {
font-size: 1rem;
}
}

View File

@@ -1,104 +1,135 @@
@font-face {
font-family: police1;
font-family: "police1";
src: url("./fonts/Combinedd.otf");
}
/* Dark theme */
/* UTILE POUR FORCER UN MODE LIGHT */
/* @media (prefers-color-scheme: dark) { */
/* :root, */
/* ::backdrop { */
/* --bg: #fff; */
/* --accent-bg: #f5f7ff; */
/* --text: #212121; */
/* --text-light: #585858; */
/* --border: #898ea4; */
/* --accent: #0d47a1; */
/* --code: #d81b60; */
/* --preformatted: #444; */
/* --marked: #ffdd33; */
/* --disabled: #efefef; */
/* } */
/* } */
/* ============================================================
SHARED VARIABLES & RESET
============================================================ */
:root {
--purple: #9557b5;
--purple-dark: #7b3fa0;
--purple-light: rgba(149, 87, 181, 0.12);
--black: #111;
--white: #fff;
--grey-light: #f5f5f5;
--border-color: #ddd;
--text-muted: #666;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
}
/* FORMULAIRE */
form label {
font-family: police1;
font-size: 1rem;
}
form input,
select,
textarea {
border-color: #c104fc;
overflow: visible;
outline: none;
background-color: white;
}
form input:focus,
select:focus {
border: 3px solid rgba(77, 168, 112, 1);
}
label {
margin-top: 2rem;
}
input {
/* font-family: police1; */
/* font-weight: bold; */
background-color: none;
color: rgb(193, 4, 252);
border: 1px solid rgb(193, 4, 252);
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background: var(--white);
color: var(--black);
}
a {
color: rgb(193, 4, 252);
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: none;
}
a, a:visited {
color: rgb(193, 4, 252);
/* ============================================================
NAV BAR (shared across all public pages)
============================================================ */
.site-nav {
background: linear-gradient(to bottom, var(--purple) 0%, rgba(149, 87, 181, 0.0) 100%);
padding: 0.55rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
input:active {
border-color: rgba(77, 168, 112, 1);
}
button,
[role="button"],
input[type="submit"],
input[type="reset"],
input[type="button"],
label[type="button"] {
background-color: white;
margin-top: 2rem;
font-size: 16px;
border-radius: 10px;
padding: 1rem;
margin: 1rem;
a {
color: black;
.site-nav__logo {
font-family: "police1", sans-serif;
font-size: 0.95rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--white);
text-decoration: none;
}
font-weight: 400;
}
button {}
/* For Google Chrome, Safari, and newer versions of Opera */
::placeholder {
/* color: rgb(213, 73, 255); */
font-size: 0.8rem;
.site-nav__links {
display: flex;
gap: 3rem;
align-items: center;
}
/* For Mozilla Firefox */
::-moz-placeholder {
/* color: rgb(213, 73, 255); */
font-size: 0.8rem;
.site-nav__link {
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--white);
text-decoration: none;
font-weight: 400;
opacity: 0.92;
transition: opacity 0.15s;
}
.site-nav__link:hover {
opacity: 1;
}
.site-nav__right {
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--white);
text-decoration: none;
font-weight: 400;
opacity: 0.92;
transition: opacity 0.15s;
}
.site-nav__right:hover {
opacity: 1;
}
/* ============================================================
SEARCH BAR (shared)
============================================================ */
.site-search {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: var(--white);
flex-shrink: 0;
}
.site-search__icon {
color: var(--text-muted);
flex-shrink: 0;
width: 16px;
height: 16px;
}
.site-search__input {
flex: 1;
border: none;
outline: none;
font-size: 0.95rem;
color: var(--black);
background: transparent;
padding: 0.15rem 0;
font-family: inherit;
}
.site-search__input::placeholder {
color: #aaa;
}

View File

@@ -1,350 +1,152 @@
body {
/* ============================================================
HOME PAGE (index.php)
============================================================ */
html, body {
margin: 0;
padding: 0;
height: 100%;
}
.home-body {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
background: var(--white);
}
/* Layout ratios: header 2/5, main 3/5, footer 1/5 */
header {
flex: 2;
min-height: 0;
}
main {
flex: 7;
min-height: 0;
}
footer {
/* Cards grid — scrollable main area */
.home-main {
flex: 1;
min-height: 0;
}
header, main, footer {
padding: 1rem;
margin: 0;
border-radius: 40px;
}
header {
font-family: "police1", sans-serif;
background: #9557b5ff;
color: white;
display: flex;
gap: 4%;
padding: 1.5rem 3rem;
align-items: center;
overflow-y: auto;
box-sizing: border-box;
}
header .title {
color: white;
font-size: 1.75rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
}
header section {
flex: 1;
font-size: 0.9rem;
line-height: 1.4;
}
header section p {
margin: 0.5rem 0;
}
header section p:first-child {
font-size: 1rem;
font-weight: 600;
}
header section p:not(:first-child) {
font-size: 0.85rem;
opacity: 0.95;
}
header nav {
display: flex;
gap: 0.75rem;
}
header nav button {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
font-family: inherit;
}
header nav button:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
header nav a {
color: white;
text-decoration: none;
}
/* Filter info banner */
.filter-info {
background: rgba(149, 87, 181, 0.9);
color: white;
padding: 0.5rem 1rem;
text-align: center;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.clear-filter {
color: white;
text-decoration: none;
padding: 0.25rem 0.75rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
font-size: 0.85rem;
transition: background 0.2s;
}
.clear-filter:hover {
background: rgba(0, 0, 0, 0.4);
}
main {
background: #3c856bff;
position: relative;
padding: 1rem;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
overflow-x: hidden;
padding: 0;
}
.cards-container {
display: grid;
gap: 0.75rem;
flex: 1;
min-height: 0;
padding: 0.5rem;
overflow: hidden;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0;
}
/* Default: 3 rows × 4 columns = 12 items */
@media (min-width: 1400px) {
.cards-container {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
grid-auto-flow: row;
}
/* Small screens: 2 rows × 3 columns = 6 items */
@media (max-width: 1200px) {
.cards-container {
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
}
/* Medium screens: 3 rows × 4 columns = 12 items */
@media (min-width: 1201px) and (max-width: 1500px) {
.cards-container {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
}
}
/* Large screens: 3 rows × 6 columns = 18 items */
@media (min-width: 1701px) {
.cards-container {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(6, 1fr);
}
}
/* Extra large screens: 4 rows × 6 columns = 24 items */
@media (min-width: 2000px) {
.cards-container {
grid-template-rows: repeat(4, 1fr);
grid-template-columns: repeat(6, 1fr);
}
}
/* Mobile placeholder (will adjust later) */
@media (max-width: 768px) {
.cards-container {
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(2, 1fr);
}
}
/* Each card = media thumbnail + text below */
.card-link {
text-decoration: none;
color: inherit;
display: block;
height: 100%;
}
.card {
background: #eee;
border-radius: 8px;
padding: 0.5rem;
height: 100%;
display: flex;
flex-direction: column;
cursor: pointer;
border: none;
background: var(--white);
overflow: hidden;
}
.card__media {
width: 100%;
box-sizing: border-box;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 4/3;
overflow: hidden;
min-width: 0;
min-height: 0;
background: #e8e8e8;
position: relative;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-content {
display: flex;
flex-direction: column;
gap: 0.3rem;
.card__media img,
.card__media video {
width: 100%;
height: 100%;
overflow: hidden;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.card .title {
font-size: 0.75rem;
font-weight: 600;
.card:hover .card__media img,
.card:hover .card__media video {
transform: scale(1.02);
}
.card__media--placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #e8e8e8, #d0d0d0);
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
font-size: 2rem;
}
.card__info {
padding: 0.55rem 0.5rem 0.65rem;
font-size: 0.88rem;
line-height: 1.35;
color: var(--black);
}
.card__info .authors {
margin: 0;
line-height: 1.15;
color: #333;
font-weight: 400;
}
.card__info .title {
margin: 0;
font-weight: 400;
color: var(--text-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card .authors {
font-size: 0.65rem;
margin: 0;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card .year {
font-size: 0.65rem;
margin: 0;
color: #9557b5;
font-weight: 600;
}
.card .tags {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
margin-top: auto;
padding-top: 0.2rem;
}
.card .tag {
font-size: 0.55rem;
padding: 0.15rem 0.25rem;
background: rgba(149, 87, 181, 0.15);
color: #7a3d95;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
font-weight: 500;
}
footer {
background: #222222ff;
overflow: hidden;
box-sizing: border-box;
}
/* Years navigation in footer */
.years-nav {
height: 100%;
/* Filter info */
.filter-info {
background: var(--purple-light);
color: var(--purple);
padding: 0.4rem 1.5rem;
font-size: 0.85rem;
display: flex;
align-items: center;
padding: 0 1rem;
gap: 1rem;
flex-shrink: 0;
}
.years-scroll {
display: flex;
gap: 0.75rem;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
padding: 0.75rem 0;
width: 100%;
}
/* Hide scrollbar but keep functionality */
.years-scroll::-webkit-scrollbar {
height: 4px;
}
.years-scroll::-webkit-scrollbar-track {
background: transparent;
}
.years-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.year-link {
color: white;
.clear-filter {
color: var(--purple);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 20px;
white-space: nowrap;
transition: background 0.2s;
font-size: 1rem;
padding: 0.15rem 0.6rem;
background: rgba(149, 87, 181, 0.12);
border-radius: 3px;
font-size: 0.82rem;
}
.year-link:hover {
background: rgba(255, 255, 255, 0.1);
.clear-filter:hover {
background: rgba(149, 87, 181, 0.22);
}
.year-link.active {
background: #9557b5ff;
font-weight: bold;
}
/* Pagination controls */
.pagination {
/* Pagination */
.pagination-wrap {
display: flex;
align-items: center;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin-top: 1rem;
width: fit-content;
align-self: center;
padding: 1rem;
border-top: 1px solid var(--border-color);
background: var(--white);
flex-shrink: 0;
}
@@ -352,24 +154,20 @@ footer {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
height: 2.5rem;
padding: 0 0.75rem;
background: rgba(255, 255, 255, 0.9);
color: #3c856b;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--black);
font-size: 0.9rem;
text-decoration: none;
border-radius: 6px;
font-size: 1.2rem;
font-weight: 600;
transition: all 0.2s;
border: 2px solid transparent;
transition: all 0.15s;
}
.pagination-btn:hover:not(.disabled) {
background: white;
border-color: #9557b5;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: var(--purple);
color: var(--purple);
}
.pagination-btn.disabled {
@@ -379,32 +177,12 @@ footer {
}
.pagination-info {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 1rem;
color: white;
font-size: 1rem;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-muted);
padding: 0 0.5rem;
}
.page-current {
font-size: 1.25rem;
font-weight: 700;
color: #9557b5;
background: white;
padding: 0.25rem 0.75rem;
border-radius: 6px;
min-width: 2rem;
text-align: center;
}
.page-separator {
font-weight: 300;
opacity: 0.6;
padding: 0 0.25rem;
}
.page-total {
opacity: 0.8;
font-weight: 600;
color: var(--black);
}

View File

@@ -1,452 +1,289 @@
/* Search page - horizontal layout */
body {
/* ============================================================
RÉPERTOIRE / SEARCH PAGE (search.php)
============================================================ */
html, body {
margin: 0;
padding: 0;
height: 100%;
}
.search-body {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: row;
background: var(--white);
}
.search-main {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
/* ---- 4-column index layout ---- */
.repertoire-index {
display: grid;
grid-template-columns: 1fr 2fr 2fr 1.5fr;
gap: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
padding: 0 1.5rem;
min-height: 100%;
}
/* Layout: header sidebar, main+footer wrapper takes rest */
header {
flex: 0 0 250px;
min-width: 0;
max-width: 250px;
height: 100vh;
font-family: "police1", sans-serif;
background: #9557b5ff;
color: white;
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0.75rem 0.6rem;
margin: 0;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
border-radius: 40px;
.repertoire-col {
padding: 0.75rem 0.5rem 2rem;
border-right: 1px solid var(--border-color);
}
.main-wrapper {
flex: 1;
min-width: 0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
.repertoire-col:last-child {
border-right: none;
}
main {
flex: 1;
min-width: 0;
background: #3c856bff;
position: relative;
padding: 1rem;
margin: 0;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 40px;
.repertoire-col__header {
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 400;
margin: 0 0 0.5rem 0;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--border-color);
}
footer {
background: #222222ff;
padding: 0.75rem 1rem;
margin: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 40px;
}
/* Search header */
.search-header {
display: flex;
flex-direction: column;
gap: 0.4rem;
width: 100%;
flex-shrink: 0;
}
.back-button {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.35rem 0.6rem;
border-radius: 10px;
cursor: pointer;
text-decoration: none;
font-size: 0.8rem;
transition: all 0.2s;
white-space: nowrap;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.back-button:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.search-form {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.search-input {
width: 100%;
padding: 0.35rem 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 10px;
font-size: 0.8rem;
background: rgba(255, 255, 255, 0.9);
color: #333;
box-sizing: border-box;
}
.search-input::placeholder {
color: #999;
font-size: 0.75rem;
}
.search-actions {
display: flex;
gap: 0.35rem;
}
.search-button,
.filter-button {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.35rem 0.5rem;
border-radius: 10px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
white-space: nowrap;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.search-button:hover,
.filter-button:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.filter-button.active {
background: rgba(255, 255, 255, 0.3);
font-weight: 600;
}
/* Filters panel */
.filters-panel {
display: none;
background: rgba(255, 255, 255, 0.08);
padding: 0.4rem;
border-radius: 8px;
flex-shrink: 0;
overflow: visible;
}
.filters-panel.show {
/* Years column - big bold numbers */
.year-index-item {
display: block;
}
.filters-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.3rem;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.filter-label {
color: white;
font-weight: 600;
font-size: 0.65rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.filter-select {
padding: 0.3rem 0.35rem;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.9);
font-size: 0.7rem;
min-width: 0;
width: 100%;
box-sizing: border-box;
}
.filter-actions {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-top: 0.3rem;
flex-shrink: 0;
}
.reset-button {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.3rem 0.5rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.7rem;
text-decoration: none;
display: inline-block;
white-space: nowrap;
text-align: center;
}
.reset-button:hover {
background: rgba(255, 255, 255, 0.25);
}
.error-message {
background: rgba(255, 100, 100, 0.2);
color: white;
padding: 0.35rem 0.5rem;
border-radius: 8px;
font-size: 0.75rem;
flex-shrink: 0;
}
/* Main content - results grid */
.cards-container {
display: grid;
gap: 1rem;
flex: 1;
min-height: 0;
padding: 0.5rem;
overflow-y: auto;
overflow-x: hidden;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(3, 1fr);
grid-auto-flow: row;
}
@media (min-width: 1400px) and (max-width: 1700px) {
.cards-container {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
}
}
@media (min-width: 1701px) {
.cards-container {
grid-template-rows: repeat(4, 1fr);
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 1399px) {
.cards-container {
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
}
}
.card-link {
text-decoration: none;
color: inherit;
display: block;
height: 100%;
}
.card {
background: #eee;
border-radius: 12px;
padding: 0.75rem;
height: 100%;
width: 100%;
box-sizing: border-box;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
height: 100%;
overflow: hidden;
}
.card .title {
font-size: 0.85rem;
font-weight: 600;
margin: 0;
line-height: 1.2;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card .authors {
font-size: 0.75rem;
margin: 0;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card .year {
font-size: 0.75rem;
margin: 0;
color: #9557b5;
font-weight: 600;
}
.card .tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: auto;
padding-top: 0.3rem;
}
.card .tag {
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
background: rgba(149, 87, 181, 0.15);
color: #7a3d95;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
font-weight: 500;
}
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
margin-top: 0.75rem;
width: fit-content;
align-self: center;
flex-shrink: 0;
}
.pagination-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
background: rgba(255, 255, 255, 0.9);
color: #3c856b;
text-decoration: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
border: 2px solid transparent;
}
.pagination-btn:hover:not(.disabled) {
background: white;
border-color: #9557b5;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.pagination-btn.disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
}
.pagination-info {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.5rem;
color: white;
font-size: 0.9rem;
font-weight: 500;
}
.page-current {
font-size: 1.1rem;
font-size: 2.2rem;
font-weight: 700;
color: #9557b5;
background: white;
padding: 0.2rem 0.6rem;
border-radius: 6px;
min-width: 1.5rem;
text-align: center;
}
.page-separator {
font-weight: 300;
opacity: 0.6;
padding: 0 0.15rem;
}
.page-total {
opacity: 0.8;
}
/* Footer - results count */
.results-footer {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1rem;
gap: 0.5rem;
text-align: center;
}
.results-count {
font-weight: 700;
font-size: 1.5rem;
line-height: 1.1;
color: var(--black);
text-decoration: none;
padding: 0.1rem 0;
transition: color 0.15s;
letter-spacing: -0.02em;
}
.year-index-item:hover,
.year-index-item.active {
color: var(--purple);
}
/* Categories column */
.cat-index-group {
margin-bottom: 0.6rem;
}
.cat-index-label {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 400;
display: block;
margin-bottom: 0.15rem;
margin-top: 0.75rem;
}
.cat-index-item {
display: block;
font-size: 0.95rem;
color: var(--black);
text-decoration: none;
padding: 0.1rem 0;
line-height: 1.4;
transition: color 0.15s;
}
.cat-index-item:hover,
.cat-index-item.active {
color: var(--purple);
}
/* Students column */
.student-index-item {
display: block;
font-size: 0.95rem;
color: var(--black);
text-decoration: none;
padding: 0.1rem 0;
line-height: 1.4;
transition: color 0.15s;
}
.student-index-item:hover {
color: var(--purple);
}
/* Keywords column */
.keyword-index-item {
display: block;
font-size: 0.95rem;
color: var(--black);
text-decoration: none;
padding: 0.1rem 0;
line-height: 1.4;
transition: color 0.15s;
}
.keyword-index-item:hover,
.keyword-index-item.active {
color: var(--purple);
}
/* ---- Search results view (grid) ---- */
.search-results-view {
padding: 1rem 1.5rem;
}
.search-results-header {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.result-card {
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.result-card__authors {
font-size: 0.9rem;
font-weight: 500;
color: var(--black);
}
.result-card__title {
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.result-card__meta {
font-size: 0.78rem;
color: var(--purple);
}
/* Toggle button (index/results) */
.view-toggle {
display: flex;
gap: 0;
border: 1px solid var(--border-color);
border-radius: 3px;
overflow: hidden;
flex-shrink: 0;
}
.view-toggle__btn {
padding: 0.25rem 0.75rem;
font-size: 0.78rem;
background: var(--white);
color: var(--text-muted);
border: none;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
transition: all 0.15s;
}
.view-toggle__btn.active,
.view-toggle__btn:hover {
background: var(--purple);
color: var(--white);
}
/* Search controls bar */
.search-controls {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.4rem 1.5rem;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
flex-wrap: wrap;
}
.search-filter-group {
display: flex;
align-items: center;
gap: 0.4rem;
}
.search-filter-label {
font-size: 0.78rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
white-space: nowrap;
}
.search-filter-select {
font-size: 0.82rem;
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 0.2rem 0.5rem;
background: var(--white);
color: var(--black);
outline: none;
font-family: inherit;
cursor: pointer;
}
.search-filter-select:focus {
border-color: var(--purple);
}
.search-apply-btn {
font-size: 0.82rem;
padding: 0.2rem 0.8rem;
background: var(--purple);
color: var(--white);
border: none;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s;
}
.search-apply-btn:hover {
background: var(--purple-dark);
}
.search-reset-link {
font-size: 0.82rem;
color: var(--text-muted);
text-decoration: underline;
cursor: pointer;
}
.search-empty {
padding: 3rem 1.5rem;
color: var(--text-muted);
font-size: 1rem;
text-align: center;
}
/* Error message */
.search-error {
background: #fff0f0;
border-left: 3px solid #c00;
color: #c00;
padding: 0.5rem 1rem;
font-size: 0.88rem;
margin: 0.5rem 1.5rem;
flex-shrink: 0;
}

View File

@@ -1,389 +1,172 @@
/* TFE (Thesis) Page Styling */
/* ============================================================
TFE INDIVIDUAL PAGE (tfe.php)
============================================================ */
html, body {
margin: 0;
padding: 0;
height: 100%;
}
.tfe-body {
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--white);
}
/* Header */
.tfe-header {
flex: 2;
min-height: 0;
background: #9557b5;
color: white;
padding: 1.5rem 2rem;
margin: 0;
border-radius: 40px;
display: flex;
align-items: center;
box-sizing: border-box;
overflow-y: auto;
}
.header-content {
width: 100%;
}
.tfe-title {
font-family: "police1", sans-serif;
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
line-height: 1.2;
color: white;
}
.tfe-subtitle {
font-size: 1.15rem;
margin: 0 0 1rem 0;
opacity: 0.9;
font-style: italic;
}
.header-metadata {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.meta-group {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.95rem;
opacity: 0.95;
}
.meta-group.keywords {
margin-top: 0.25rem;
}
.meta-group .label {
font-weight: 600;
opacity: 0.85;
}
.meta-group .author {
font-weight: 500;
}
.meta-group .separator {
opacity: 0.6;
}
.meta-group .year {
font-weight: 600;
}
/* Main Content */
.tfe-main {
flex: 7;
min-height: 0;
background: #3c856b;
padding: 2rem;
margin: 0;
border-radius: 40px;
box-sizing: border-box;
overflow-y: auto;
}
.tfe-container {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Metadata Section */
.tfe-metadata {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.subtitle {
font-size: 1.25rem;
color: #666;
margin: 0 0 1.5rem 0;
font-style: italic;
}
.metadata-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem 3rem;
margin-bottom: 1rem;
}
.metadata-column {
display: flex;
flex-direction: column;
gap: 1rem;
}
.meta-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.meta-item strong {
color: #9557b5;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.meta-item span {
color: #333;
font-size: 1rem;
line-height: 1.5;
}
.context-note {
margin-top: 1.5rem;
padding: 1rem;
background: #f8f8f8;
border-left: 4px solid #9557b5;
border-radius: 4px;
}
.context-note em {
color: #555;
font-size: 0.95rem;
line-height: 1.6;
}
/* Synopsis Section */
.tfe-synopsis {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tfe-synopsis h3 {
font-size: 1.5rem;
color: #9557b5;
margin: 0 0 1rem 0;
font-weight: 700;
}
.synopsis-content {
color: #333;
font-size: 1rem;
line-height: 1.7;
}
/* Files Section */
.tfe-files {
display: flex;
flex-direction: column;
gap: 2rem;
}
.file-block {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.file-block embed,
.file-block video {
border-radius: 8px;
display: block;
}
.image-figure {
margin: 0;
text-align: center;
}
.image-figure img {
max-width: 100%;
height: auto;
border-radius: 8px;
display: block;
margin: 0 auto;
}
.file-description {
margin: 1rem 0 0 0;
color: #666;
font-size: 0.9rem;
font-style: italic;
text-align: center;
}
.no-files {
background: #fff3cd;
color: #856404;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
border: 1px solid #ffeaa7;
}
/* Footer */
.tfe-footer {
flex: 1;
min-height: 0;
background: #222;
color: white;
padding: 1rem 2rem;
overflow-y: auto;
padding: 2rem 1.5rem 3rem;
}
/* Two-column layout */
.tfe-layout {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 3rem;
max-width: 1200px;
}
/* Left column */
.tfe-left {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Student name — large */
.tfe-author {
font-size: 1.9rem;
font-weight: 400;
color: var(--black);
margin: 0;
border-radius: 40px;
box-sizing: border-box;
overflow: hidden;
line-height: 1.15;
letter-spacing: -0.01em;
}
.footer-content {
height: 100%;
/* Title — very large, slightly spaced */
.tfe-title {
font-size: 2.2rem;
font-weight: 400;
color: var(--black);
margin: 0;
line-height: 1.15;
letter-spacing: -0.01em;
}
/* Metadata list */
.tfe-meta-list {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
gap: 0.45rem;
font-size: 0.95rem;
line-height: 1.4;
}
.tfe-meta-item {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
gap: 1rem;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
color: white;
text-decoration: none;
transition: all 0.2s ease;
.tfe-meta-item .label {
color: var(--black);
font-weight: 400;
flex-shrink: 0;
}
.back-button:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateX(-2px);
.tfe-meta-item .value {
color: var(--black);
font-weight: 700;
}
.back-button svg {
.tfe-meta-item .value a {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
/* Synopsis text */
.tfe-synopsis-text {
font-size: 0.95rem;
line-height: 1.7;
color: var(--black);
margin-top: 0.5rem;
}
/* Right column — media */
.tfe-right {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.tfe-media-block {
overflow: hidden;
}
.tfe-media-block img {
width: 100%;
height: auto;
display: block;
}
.footer-meta {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9rem;
opacity: 0.9;
.tfe-media-block embed,
.tfe-media-block video {
width: 100%;
display: block;
border: none;
}
.footer-meta .separator {
opacity: 0.6;
.tfe-media-block video {
max-height: 500px;
}
/* Responsive Design */
@media (max-width: 768px) {
.tfe-header {
padding: 1rem;
.tfe-media-block embed {
height: 700px;
}
.tfe-title {
font-size: 1.35rem;
}
.tfe-subtitle {
font-size: 1rem;
}
.meta-group {
font-size: 0.85rem;
}
.back-button {
width: 40px;
height: 40px;
}
.back-button svg {
width: 20px;
height: 20px;
}
.tfe-main {
padding: 1.5rem;
}
.tfe-container {
gap: 1.5rem;
}
.tfe-metadata,
.tfe-synopsis,
.file-block {
padding: 1.5rem;
}
.metadata-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.footer-content {
flex-direction: column;
text-align: center;
justify-content: center;
}
.tfe-footer {
padding: 1rem;
}
}
@media (max-width: 480px) {
.tfe-title {
font-size: 1.15rem;
}
.tfe-subtitle {
font-size: 0.9rem;
}
.meta-group {
.tfe-file-caption {
font-size: 0.8rem;
gap: 0.4rem;
color: var(--text-muted);
margin: 0.3rem 0 0;
font-style: italic;
}
.back-button {
width: 36px;
height: 36px;
.tfe-no-files {
font-size: 0.95rem;
color: var(--text-muted);
padding: 1rem 0;
}
.back-button svg {
width: 18px;
height: 18px;
/* Responsive */
@media (max-width: 900px) {
.tfe-layout {
grid-template-columns: 1fr;
gap: 2rem;
}
.tfe-synopsis h3 {
.tfe-author {
font-size: 1.5rem;
}
.tfe-title {
font-size: 1.7rem;
}
}
@media (max-width: 600px) {
.tfe-main {
padding: 1.5rem 1rem 2rem;
}
.tfe-author {
font-size: 1.25rem;
}
.file-block embed {
height: 500px;
}
.tfe-main {
padding: 1rem;
}
.tfe-container {
gap: 1rem;
.tfe-title {
font-size: 1.4rem;
}
}

View File

@@ -3,20 +3,15 @@
require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
$pageTitle = "Liste des TFE";
$page = isset($_GET["page"]) ? intval($_GET["page"]) : 1;
$year = isset($_GET["year"]) ? intval($_GET["year"]) : null;
// Default to 12 items (4 cols × 3 rows)
$itemsPerPage = 12;
$itemsPerPage = 24; // bigger grid
try {
$db = Database::getInstance();
$offset = ($page - 1) * $itemsPerPage;
// Get available years for footer
$availableYears = $db->getAvailableYears();
// Filter by year if specified
if ($year) {
$itemsToLoad = $db->searchTheses(['year' => $year], $itemsPerPage, $offset);
$totalItems = $db->countSearchResults(['year' => $year]);
@@ -31,10 +26,34 @@ try {
$itemsToLoad = [];
$totalPages = 0;
$availableYears = [];
$totalItems = 0;
}
include APP_ROOT . '/templates/header.php';
$currentNav = '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Posterg</title>
<link rel="stylesheet" href="assets/modern-normalize.min.css">
<link rel="stylesheet" href="assets/common.css">
<link rel="stylesheet" href="assets/main.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 class="home-body">
<?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
<?php if ($year): ?>
<div class="filter-info">
@@ -43,72 +62,66 @@ include APP_ROOT . '/templates/header.php';
</div>
<?php endif; ?>
<main>
<main class="home-main">
<div class="cards-container">
<?php foreach ($itemsToLoad as $item): ?>
<a href="tfe.php?id=<?= (int)$item["id"] ?>" class="card-link">
<div class="card">
<div class="card-content">
<h2 class="title"><?= htmlspecialchars($item["title"]) ?></h2>
<p class="authors"><?= htmlspecialchars($item["authors"]) ?></p>
<p class="year"><?= htmlspecialchars($item["year"]) ?></p>
<?php if (!empty($item["keywords"])): ?>
<div class="tags">
<div class="card__media">
<?php
$keywords = explode(',', $item["keywords"]);
foreach (array_slice($keywords, 0, 3) as $keyword):
// Use first image/video file as thumbnail
$thumb = null;
if (!empty($item['files'])) {
foreach ($item['files'] as $f) {
$ext = strtolower(pathinfo($f['file_path'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','webp'])) {
$thumb = $f['file_path'];
break;
}
}
}
// Also check cover image
if (!$thumb && !empty($item['cover_image'])) {
$thumb = $item['cover_image'];
}
?>
<span class="tag"><?= htmlspecialchars(trim($keyword)) ?></span>
<?php endforeach; ?>
</div>
<?php if ($thumb): ?>
<img src="/media.php?path=<?= urlencode($thumb) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
loading="lazy">
<?php else: ?>
<div class="card__media--placeholder">◻</div>
<?php endif; ?>
</div>
<div class="card__info">
<p class="authors"><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> <?php endif; ?><?= htmlspecialchars($item["title"]) ?></p>
</div>
</div>
</a>
<?php endforeach; ?>
<?php if (empty($itemsToLoad)): ?>
<p>Aucun mémoire trouvé.</p>
<p style="padding:2rem;color:#666;">Aucun mémoire trouvé.</p>
<?php endif; ?>
</div>
<?php if ($totalPages > 1): ?>
<nav class="pagination">
<?php
$yearParam = $year ? '&year=' . (int)$year : '';
?>
<div class="pagination-wrap">
<?php $yearParam = $year ? '&year=' . (int)$year : ''; ?>
<a href="?page=1<?= $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true"' : '' ?>>
</a>
<a href="?page=<?= max(1, (int)($page - 1)) . $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true"' : '' ?>>
</a>
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>">«</a>
<a href="?page=<?= max(1, $page - 1) . $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"></a>
<span class="pagination-info">
<span class="page-current"><?= (int)$page ?></span>
<span class="page-separator">/</span>
<span class="page-total"><?= (int)$totalPages ?></span>
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
</span>
<a href="?page=<?= min($totalPages, (int)($page + 1)) . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true"' : '' ?>>
</a>
<a href="?page=<?= min($totalPages, $page + 1) . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"></a>
<a href="?page=<?= $totalPages . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true"' : '' ?>>
</a>
</nav>
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>">»</a>
</div>
<?php endif; ?>
</main>
<?php include APP_ROOT . '/templates/footer.php'; ?>
</body>
</html>

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;
if ($hasSearch) {
$results = $db->searchTheses($searchParams, $itemsPerPage, $offset);
$totalItems = $db->countSearchResults($searchParams);
$totalPages = ceil($totalItems / $itemsPerPage);
} else {
$results = [];
$totalItems = 0;
$totalPages = 0;
}
// Get filter options
$years = $db->getAvailableYears();
$orientations = $db->getOrientations();
$apPrograms = $db->getApPrograms();
$finalityTypes = $db->getFinalityTypes();
$keywords = $db->getUsedKeywords();
$formats = $db->getFormatTypes();
$languages = $db->getLanguages();
// 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">
</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 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; ?>
<?php endforeach; ?>
</form>
</div>
</head>
<body class="search-body">
<?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
<?php if ($validationError): ?>
<div class="error-message">
⚠ <?= htmlspecialchars($validationError); ?>
</div>
<div class="search-error">⚠ <?= 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">
<?php if ($hasSearch): ?>
<!-- ── RESULTS VIEW ─────────────────────────────────── -->
<div class="filters-grid">
<div class="filter-group">
<label class="filter-label">Année</label>
<select name="year" class="filter-select">
<!-- 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 $year): ?>
<option value="<?= (int)$year; ?>" <?= (isset($_GET['year']) && $_GET['year'] == $year) ? 'selected' : ''; ?>>
<?= (int)$year; ?>
<?php foreach ($years as $y): ?>
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
<?= (int)$y ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Orientation</label>
<select name="orientation" class="filter-select">
<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 $orientation): ?>
<option value="<?= htmlspecialchars($orientation['name']); ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $orientation['name']) ? 'selected' : ''; ?>>
<?= htmlspecialchars($orientation['name']); ?>
<?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>
<div class="filter-group">
<label class="filter-label">AP</label>
<select name="ap_program" class="filter-select">
<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 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>
<button type="submit" class="search-apply-btn">Filtrer</button>
<a href="search.php" class="search-reset-link">Réinitialiser</a>
</form>
</div>
</header>
<div class="main-wrapper">
<main>
<div class="cards-container">
<?php if (count($results) > 0): ?>
<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 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>
<?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>
<?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>
<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: ?>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</div>
</main>
<footer>
<div class="results-footer">
<span class="results-count"><?= (int)$totalItems; ?></span>
<span>résultat<?= $totalItems > 1 ? 's' : ''; ?></span>
</div>
</footer>
<?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>
<script>
function toggleFilters() {
const panel = document.getElementById('filters-panel');
const button = document.querySelector('.filter-button');
const state = document.getElementById('filters-state');
<!-- CATÉGORIES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Catégories</h2>
panel.classList.toggle('show');
button.classList.toggle('active');
<?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; ?>
// Update hidden field
state.value = panel.classList.contains('show') ? 'show' : 'hide';
<?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>
<!-- É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'];
}
</script>
}
}
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>

View File

@@ -1,202 +1,172 @@
<?php
// Bootstrap application
require_once __DIR__ . '/../config/bootstrap.php';
// Load required libraries and classes
require_once APP_ROOT . '/src/Database.php';
// Check if an id parameter is provided in the URL
if (isset($_GET['id'])) {
$thesisId = intval($_GET['id']);
try {
$db = Database::getInstance();
$data = $db->getThesisById($thesisId);
if (!$data) {
// Thesis not found or not published
header('Location: index.php');
exit;
}
if (!$data) { header('Location: index.php'); exit; }
} catch (Exception $e) {
error_log("Error loading thesis: " . $e->getMessage());
header('Location: index.php');
exit;
header('Location: index.php'); exit;
}
} else {
// Redirect to the index page if no id parameter is provided
header('Location: index.php');
exit;
header('Location: index.php'); exit;
}
// Set page title and additional CSS
$pageTitle = $data['title'];
$additionalCSS = ['assets/tfe.css'];
// Include shared head template
include APP_ROOT . '/templates/head.php';
$currentNav = '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($data['title']) ?> Posterg</title>
<link rel="stylesheet" href="assets/modern-normalize.min.css">
<link rel="stylesheet" href="assets/common.css">
<link rel="stylesheet" href="assets/tfe.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 class="tfe-body">
<header class="tfe-header">
<div class="header-content">
<h1 class="tfe-title"><?= htmlspecialchars($data['title']); ?></h1>
<?php if (!empty($data['subtitle'])): ?>
<p class="tfe-subtitle"><?= htmlspecialchars($data['subtitle']); ?></p>
<?php endif; ?>
<div class="header-metadata">
<div class="meta-group">
<span class="author"><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu'); ?></span>
<span class="separator">•</span>
<span class="year"><?= htmlspecialchars($data['year']); ?></span>
</div>
<?php if (!empty($data['orientation']) || !empty($data['ap_program'])): ?>
<div class="meta-group">
<?php if (!empty($data['orientation'])): ?>
<span><?= htmlspecialchars($data['orientation']); ?></span>
<?php endif; ?>
<?php if (!empty($data['orientation']) && !empty($data['ap_program'])): ?>
<span class="separator">•</span>
<?php endif; ?>
<?php if (!empty($data['ap_program'])): ?>
<span><?= htmlspecialchars($data['ap_program']); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($data['finality_type'])): ?>
<div class="meta-group">
<span><?= htmlspecialchars($data['finality_type']); ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['keywords'])): ?>
<div class="meta-group keywords">
<span class="label">Mots-clés:</span>
<span><?= htmlspecialchars($data['keywords']); ?></span>
</div>
<?php endif; ?>
</div>
</div>
</header>
<?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
<main class="tfe-main">
<div class="tfe-container">
<div class="tfe-layout">
<!-- Metadata Section -->
<section class="tfe-metadata">
<!-- LEFT: info -->
<div class="tfe-left">
<h1 class="tfe-author"><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu') ?></h1>
<h2 class="tfe-title">
<?= htmlspecialchars($data['title']) ?>
<?php if (!empty($data['subtitle'])): ?>
<h2 class="subtitle"><?= htmlspecialchars($data['subtitle']); ?></h2>
<?= htmlspecialchars($data['subtitle']) ?>
<?php endif; ?>
</h2>
<div class="metadata-grid">
<div class="metadata-column">
<?php if (!empty($data['orientation']) || !empty($data['ap_program'])): ?>
<div class="meta-item">
<strong>Programme:</strong>
<span>
<div class="tfe-meta-list">
<?php if (!empty($data['orientation'])): ?>
<?= htmlspecialchars($data['orientation']); ?>
<?php endif; ?>
<?php if (!empty($data['orientation']) && !empty($data['ap_program'])): ?>
et
<div class="tfe-meta-item">
<span class="label">Orientation :</span>
<span class="value"><?= htmlspecialchars($data['orientation']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['ap_program'])): ?>
<?= htmlspecialchars($data['ap_program']); ?>
<?php endif; ?>
</span>
<div class="tfe-meta-item">
<span class="label">Atelier pluridisciplinaire :</span>
<span class="value"><?= htmlspecialchars($data['ap_program']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['finality_type'])): ?>
<div class="meta-item">
<strong>Finalité:</strong>
<span><?= htmlspecialchars($data['finality_type']); ?></span>
<?php if (!empty($data['year'])): ?>
<div class="tfe-meta-item">
<span class="label">Date :</span>
<span class="value"><?= htmlspecialchars($data['year']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['supervisors'])): ?>
<div class="meta-item">
<strong>Promoteur·ice·s:</strong>
<span><?= htmlspecialchars($data['supervisors']); ?></span>
</div>
<?php endif; ?>
</div>
<div class="metadata-column">
<?php if (!empty($data['languages'])): ?>
<div class="meta-item">
<strong>Langue(s):</strong>
<span><?= htmlspecialchars($data['languages']); ?></span>
<div class="tfe-meta-item">
<span class="label">Langue :</span>
<span class="value"><?= htmlspecialchars($data['languages']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['formats'])): ?>
<div class="meta-item">
<strong>Format(s):</strong>
<span><?= htmlspecialchars($data['formats']); ?></span>
<div class="tfe-meta-item">
<span class="label">Format :</span>
<span class="value"><?= htmlspecialchars($data['formats']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['file_size_info'])): ?>
<div class="tfe-meta-item">
<span class="label">Durée :</span>
<span class="value"><?= htmlspecialchars($data['file_size_info']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['keywords'])): ?>
<div class="tfe-meta-item">
<span class="label">Mots-clés :</span>
<span class="value"><?= htmlspecialchars($data['keywords']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['supervisors'])): ?>
<div class="tfe-meta-item">
<span class="label">Promoteur·ice interne :</span>
<span class="value"><?= htmlspecialchars($data['supervisors']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($data['baiu_link'])): ?>
<div class="tfe-meta-item">
<span class="label">Contact :</span>
<span class="value">
<a href="<?= htmlspecialchars($data['baiu_link']) ?>" target="_blank" rel="noopener">
<?= htmlspecialchars($data['baiu_link']) ?>
</a>
</span>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!empty($data['context_note'])): ?>
<div class="context-note">
<em><?= htmlspecialchars($data['context_note']); ?></em>
</div>
<?php endif; ?>
</section>
<!-- Synopsis Section -->
<?php if (!empty($data['synopsis'])): ?>
<section class="tfe-synopsis">
<h3>Synopsis</h3>
<div class="synopsis-content">
<?= nl2br(htmlspecialchars($data['synopsis'])); ?>
<div class="tfe-synopsis-text">
<?= nl2br(htmlspecialchars($data['synopsis'])) ?>
</div>
</section>
<?php endif; ?>
<!-- Files Section -->
<section class="tfe-files">
<?php if (isset($data['files']) && count($data['files']) > 0): ?>
<div style="margin-top:1.5rem;">
<a href="index.php" style="font-size:.88rem;color:#666;text-decoration:underline;text-underline-offset:2px;">
← Retour
</a>
</div>
</div>
<!-- RIGHT: media -->
<div class="tfe-right">
<?php if (!empty($data['files'])): ?>
<?php foreach ($data['files'] as $file): ?>
<?php $ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION)); ?>
<div class="file-block">
<div class="tfe-media-block">
<?php if ($ext === 'pdf'): ?>
<embed src="/media.php?path=<?= urlencode($file['file_path']); ?>" type="application/pdf" width="100%" height="800px" />
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
type="application/pdf" width="100%" height="700px">
<?php elseif (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp'])): ?>
<figure class="image-figure">
<img src="/media.php?path=<?= urlencode($file['file_path']); ?>" alt="<?= htmlspecialchars($file['file_name']); ?>">
</figure>
<img src="/media.php?path=<?= urlencode($file['file_path']) ?>"
alt="<?= htmlspecialchars($file['file_name']) ?>">
<?php elseif ($ext === 'mp4'): ?>
<video width="100%" controls>
<source src="/media.php?path=<?= urlencode($file['file_path']); ?>" type="video/mp4">
Votre navigateur ne supporte pas la lecture de vidéos.
<source src="/media.php?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
</video>
<?php endif; ?>
<?php if (!empty($file['description'])): ?>
<p class="file-description"><?= htmlspecialchars($file['description']); ?></p>
<p class="tfe-file-caption"><?= htmlspecialchars($file['description']) ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="no-files">Aucun fichier disponible pour ce TFE.</p>
<p class="tfe-no-files">Aucun fichier disponible pour ce TFE.</p>
<?php endif; ?>
</section>
</div>
</div>
</main>
<footer class="tfe-footer">
<div class="footer-content">
<a href="index.php" class="back-button" aria-label="Retour à l'accueil" title="Retour à l'accueil">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
</a>
<div class="footer-meta">
<span><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu'); ?></span>
<span class="separator">•</span>
<span><?= htmlspecialchars($data['year']); ?></span>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -1 +1 @@
{"10":1770899252,"11":1770899274,"12":1770899301}
[1771972436,1771972448]

View File

@@ -1,6 +1,2 @@
<footer>
<p>Formulaire fait avec en PHP et <a href="https://watercss.kognise.dev/">Water.css</a>.</p>
</footer>
</body>
</html>

View File

@@ -1,71 +1,35 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle ?? 'Admin'); ?> - Post-ERG</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($pageTitle ?? 'Admin') ?> Posterg</title>
<link rel="stylesheet" href="/assets/modern-normalize.min.css">
<link rel="stylesheet" href="/assets/admin.css">
<link rel="shortcut icon" href="/assets/admin_favicon.svg" type="image/svg+xml">
<?php if (php_sapi_name() === 'cli-server'): ?>
<!-- Live reload for development -->
<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));
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>
<h1><?php echo htmlspecialchars($pageTitle ?? 'Admin'); ?></h1>
<nav>
<body class="admin-body">
<nav class="admin-nav">
<a href="/admin/" class="admin-nav__logo">Posterg</a>
<?php
// Detect current page
$currentPage = basename($_SERVER['PHP_SELF']);
$thesisId = $_GET['id'] ?? null;
// Build navigation based on context
$navLinks = [];
// Always show list
if ($currentPage !== 'index.php') {
$navLinks[] = '<a href="/admin/"><button>📋 Liste des TFE</button></a>';
}
// Show add thesis if not on add page
if ($currentPage !== 'add.php') {
$navLinks[] = '<a href="/admin/add.php"><button> Ajouter un TFE</button></a>';
}
// Show import if not on import page
if ($currentPage !== 'import.php') {
$navLinks[] = '<a href="/admin/import.php"><button>📥 Importer CSV</button></a>';
}
// If on edit or thanks page with thesis ID, show edit and view links
if ($thesisId && in_array($currentPage, ['edit.php', 'thanks.php'])) {
if ($currentPage !== 'edit.php') {
$navLinks[] = '<a href="/admin/edit.php?id=' . intval($thesisId) . '"><button>✏️ Modifier</button></a>';
}
if ($currentPage !== 'thanks.php') {
$navLinks[] = '<a href="/admin/thanks.php?id=' . intval($thesisId) . '"><button>👁️ Voir</button></a>';
}
}
echo implode(' ', $navLinks);
?>
<a href="/admin/" class="admin-nav__link <?= $currentPage === 'index.php' ? 'active' : '' ?>">Liste des TFE</a>
<a href="/admin/add.php" class="admin-nav__link <?= $currentPage === 'add.php' ? 'active' : '' ?>">Ajouter un TFE</a>
<a href="/admin/import.php" class="admin-nav__link <?= $currentPage === 'import.php' ? 'active' : '' ?>">Importer une liste de TFE</a>
<?php if ($thesisId && in_array($currentPage, ['edit.php', 'thanks.php'])): ?>
<a href="/admin/edit.php?id=<?= intval($thesisId) ?>" class="admin-nav__link <?= $currentPage === 'edit.php' ? 'active' : '' ?>">Modifier</a>
<?php endif; ?>
<?php if (defined('ADMIN_PASSWORD_HASH')): ?>
<a href="/admin/logout.php"><button>🔐 Déconnexion</button></a>
<a href="/admin/logout.php" class="admin-nav__link" style="margin-left:auto;opacity:.6;">Déconnexion</a>
<?php endif; ?>
</nav>
</header>

15
templates/nav.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
// nav.php — shared public navigation bar
// Usage: include this partial from any public page
// Provide $currentPage variable to mark active links (optional)
$_navCurrent = $currentNav ?? '';
?>
<nav class="site-nav">
<a class="site-nav__logo" href="/index.php">Posterg</a>
<div class="site-nav__links">
<a class="site-nav__link <?= ($_navCurrent === 'repertoire') ? 'site-nav__link--active' : '' ?>"
href="/search.php">Répertoire</a>
</div>
<a class="site-nav__right <?= ($_navCurrent === 'apropos') ? 'site-nav__link--active' : '' ?>"
href="/apropos.php">À Propos</a>
</nav>

19
templates/search-bar.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
// search-bar.php — shared search bar partial
// $searchValue: current search query (optional)
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
?>
<form class="site-search" method="GET" action="/search.php">
<svg class="site-search__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
class="site-search__input"
type="text"
name="query"
placeholder="Recherche..."
value="<?= htmlspecialchars($_sbValue) ?>"
autocomplete="off"
>
</form>