Split form.css into form-base.css and form-admin.css, drop dead upload-progress code

Also introduces $extraCssAdmin support in head.php for admin-only
stylesheets (form-admin.css, filepond CSS, system.css). Admin pages
now use $extraCssAdmin for admin-only assets and $extraCss for
shared stylesheets like form-base.css.
This commit is contained in:
Pontoporeia
2026-06-11 11:04:01 +02:00
parent 99125cc8e3
commit cbd369bc72
15 changed files with 250 additions and 682 deletions

View File

@@ -32,12 +32,7 @@ require_once APP_ROOT . '/src/ErrorHandler.php';
try { try {
$ctrl = ThesisEditController::create(); $ctrl = ThesisEditController::create();
$progressToken = $_POST['progress_token'] ?? bin2hex(random_bytes(8)); $ctrl->save($thesisId, $_POST, $_FILES);
$ctrl->save($thesisId, $_POST, $_FILES, $progressToken);
// Clean up progress file
require_once APP_ROOT . '/src/PeerTubeService.php';
PeerTubeService::clearProgress($progressToken);
// Regenerate CSRF token after successful save // Regenerate CSRF token after successful save
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));

View File

@@ -1,45 +0,0 @@
<?php
/**
* upload-progress.php
*
* Returns the current upload/processing progress for a given token.
* Called by the client-side upload-progress.js while the form XHR is in flight.
*
* GET /admin/actions/upload-progress.php?token=<token>
*
* Response: JSON
* { "stage": "upload"|"processing"|"done", "pct": 45, "file": "video.mp4" }
*
* Progress data is written by ThesisEditController / ThesisCreateController
* to a temp file during processing.
*/
require_once __DIR__ . '/../../../bootstrap.php';
// No AdminAuth check here — this endpoint is called by client-side JS during
// both admin and partage (student) form uploads. Access is guarded by the
// progress token (64 bits of entropy, fresh per form render) which must match
// a temp file that only exists during an active upload.
header('Content-Type: application/json');
$token = $_GET['token'] ?? '';
if (!preg_match('/^[a-f0-9]{16}$/', $token)) {
echo json_encode(['stage' => 'error', 'error' => 'Invalid token']);
exit;
}
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
if (!file_exists($progressFile)) {
// No progress file yet — still in upload phase (or token invalid)
echo json_encode(['stage' => 'upload', 'pct' => 0, 'file' => '']);
exit;
}
$data = json_decode(file_get_contents($progressFile), true);
if (!$data) {
echo json_encode(['stage' => 'upload', 'pct' => 0, 'file' => '']);
exit;
}
echo json_encode($data);

View File

@@ -54,7 +54,8 @@ function wasSelected($key, $value) {
$isAdmin = true; $isAdmin = true;
$bodyClass = 'admin-body'; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraCss = ['/assets/css/form-base.css'];
$extraCssAdmin = ['/assets/css/form-admin.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
$extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';

View File

@@ -39,7 +39,8 @@ try {
} }
$isAdmin = true; $bodyClass = 'admin-body'; $isAdmin = true; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraCss = ['/assets/css/form-base.css'];
$extraCssAdmin = ['/assets/css/form-admin.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
$extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';

View File

@@ -515,7 +515,7 @@ if ($isHtmx) {
include APP_ROOT . '/templates/admin/index-table.php'; include APP_ROOT . '/templates/admin/index-table.php';
} }
} else { } else {
$extraCss = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraCssAdmin = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
$extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';

View File

@@ -74,7 +74,7 @@ if (empty($_SESSION['csrf_token'])) {
} }
$isAdmin = true; $bodyClass = 'admin-body'; $isAdmin = true; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/system.css']; $extraCssAdmin = ['/assets/css/system.css'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/parametres.php'; include APP_ROOT . '/templates/admin/parametres.php';

View File

@@ -115,7 +115,7 @@
fill: currentColor; fill: currentColor;
} }
/* ── Form styles → see form.css ─────────────────────────────────────────── */ /* ── Form styles → see form-base.css + form-admin.css ───────────────────── */
/* ── Buttons ────────────────────────────────────────────────────────────── */ /* ── Buttons ────────────────────────────────────────────────────────────── */
.admin-form-footer { .admin-form-footer {
@@ -763,7 +763,7 @@ th.admin-ap-col {
padding: 0; padding: 0;
} }
/* ── Jury fieldset → see form.css ───────────────────────────────────────── */ /* ── Jury fieldset → see form-base.css ──────────────────────────────────── */
/* ── Inline form (tags page) ────────────────────────────────────────────── */ /* ── Inline form (tags page) ────────────────────────────────────────────── */
.admin-inline-form { .admin-inline-form {
@@ -1679,7 +1679,7 @@ th.admin-ap-col {
} }
/* ── Form group, student mode, thanks page → see form.css ───────────────── */ /* ── Form group, student mode, thanks page → see form-base.css ──────────── */
/* ── Utility ─────────────────────────────────────────────────────────────── */ /* ── Utility ─────────────────────────────────────────────────────────────── */

View File

@@ -0,0 +1,234 @@
/* ============================================================
FORM — Admin-only extensions
Styles used exclusively by admin pages (not the student partage form).
Loaded after form-base.css.
============================================================ */
/* ── Recap files table ─────────────────────────────────────────── */
.recap-files-table {
width: 100%;
border-collapse: collapse;
font-size: var(--step--1);
}
.recap-files-table thead th {
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: var(--step--2);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--space-xs) var(--space-s);
border-bottom: 2px solid var(--border-primary);
}
.recap-files-table tbody td {
padding: var(--space-xs) var(--space-s);
border-bottom: 1px solid var(--border-secondary);
vertical-align: middle;
}
.recap-files-table tbody tr:last-child td {
border-bottom: none;
}
.recap-files-icon {
width: 1px;
white-space: nowrap;
}
.recap-files-icon-emoji {
font-size: 1.3rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2rem;
height: 2.2rem;
background: var(--accent-muted);
border-radius: var(--radius);
}
.recap-files-thumb {
max-width: 80px;
max-height: 60px;
object-fit: cover;
border-radius: var(--radius);
}
.recap-files-name {
font-weight: 500;
}
.recap-files-name a {
color: var(--text-primary);
text-decoration: none;
}
.recap-files-name a:hover {
color: var(--accent-primary);
}
.recap-files-peertube-id {
display: block;
font-size: var(--step--2);
color: var(--text-tertiary);
font-family: monospace;
}
.recap-files-label {
display: block;
font-size: var(--step--2);
color: var(--text-secondary);
font-style: italic;
}
.recap-files-type {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
}
.recap-files-size {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.recap-files-date {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
}
/* ── File browser (relink) ──────────────────────────────────────── */
.file-browser-trigger {
margin-top: var(--space-2xs);
font-size: var(--step--2);
}
.relink-modal {
width: min(90vw, 700px);
max-height: 80vh;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-primary);
padding: 0;
overflow: hidden;
color: var(--text-primary);
}
.relink-modal::backdrop {
background: rgba(0,0,0,0.5);
}
.relink-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-s) var(--space-m);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.relink-modal-header h3 {
margin: 0;
font-size: var(--step-0);
}
.relink-modal-footer {
padding: var(--space-xs) var(--space-m);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
#relink-modal-body {
padding: var(--space-s) var(--space-m);
overflow-y: auto;
max-height: 60vh;
}
/* ── File browser tree ─────────────────────────────────────────── */
.file-browser {
font-size: var(--step--1);
}
.file-browser-hint,
.file-browser-empty,
.file-browser-loading,
.file-browser-error {
color: var(--text-secondary);
text-align: center;
padding: var(--space-m);
}
.file-browser-error {
color: var(--error);
}
.file-browser-breadcrumb {
display: flex;
align-items: center;
gap: var(--space-2xs);
flex-wrap: wrap;
padding-bottom: var(--space-s);
margin-bottom: var(--space-s);
border-bottom: 1px solid var(--border);
font-size: var(--step--2);
}
.file-browser-breadcrumb a {
color: var(--accent-blue, var(--link));
text-decoration: none;
}
.file-browser-breadcrumb a:hover {
color: var(--accent-primary);
}
.file-browser-sep {
color: var(--text-tertiary);
}
.file-browser-list {
list-style: none;
margin: 0;
padding: 0;
}
.file-browser-entry {
border-bottom: 1px solid var(--border-subtle);
}
.file-browser-entry a,
.file-browser-select-btn {
display: flex;
align-items: center;
gap: var(--space-xs);
width: 100%;
padding: var(--space-xs) var(--space-2xs);
text-decoration: none;
color: var(--text-primary);
background: none;
border: none;
cursor: pointer;
font-size: var(--step--1);
text-align: left;
transition: background 0.15s;
}
.file-browser-entry a:hover,
.file-browser-select-btn:hover {
background: var(--bg-secondary);
}
.file-browser-icon {
flex-shrink: 0;
font-size: var(--step-0);
}
.file-browser-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-browser-size {
flex-shrink: 0;
color: var(--text-tertiary);
font-size: var(--step--2);
font-variant-numeric: tabular-nums;
}
.file-browser-file .file-browser-select-btn {
color: var(--accent-green, var(--success));
}
.file-browser-file .file-browser-select-btn:hover {
background: var(--bg-secondary);
}

View File

@@ -1,6 +1,7 @@
/* ============================================================ /* ============================================================
FORM TFE submission form FORM TFE submission form (base styles)
Shared between admin add/edit pages and the student partage form. Shared between admin add/edit pages and the student partage form.
Admin-only extensions live in form-admin.css.
Variables loaded via style.css (colors.css + typography.css). Variables loaded via style.css (colors.css + typography.css).
============================================================ */ ============================================================ */
@@ -1156,61 +1157,7 @@
color: var(--text-tertiary); color: var(--text-tertiary);
} }
/* ── Upload progress bar ─────────────────────────────────── */
#upload-progress-wrap {
border: 1px solid var(--accent-muted);
border-radius: var(--radius);
padding: var(--space-s);
margin-bottom: var(--space-s);
}
#upload-progress-wrap legend {
font-weight: 600;
font-size: var(--step--1);
color: var(--text-primary);
padding: 0 var(--space-2xs);
}
#upload-progress-bar {
display: block;
width: 100%;
height: 0.85rem;
border-radius: var(--radius);
overflow: hidden;
background: var(--accent-muted);
border: none;
}
#upload-progress-bar::-webkit-progress-bar {
background: var(--accent-muted);
border-radius: var(--radius);
}
#upload-progress-bar::-webkit-progress-value {
background: var(--accent-primary);
border-radius: var(--radius);
transition: width 0.3s ease;
}
#upload-progress-bar::-moz-progress-bar {
background: var(--accent-primary);
border-radius: var(--radius);
}
/* Completion state */
#upload-progress-bar[data-complete] {
box-shadow: 0 0 12px rgba(149, 87, 181, 0.4);
}
#upload-progress-bar[data-complete]::-webkit-progress-value {
background: var(--accent-green);
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
}
#upload-progress-bar[data-complete]::-moz-progress-bar {
background: var(--accent-green);
}
/* ── Sticky formats fieldset ──────────────────────────────── */ /* ── Sticky formats fieldset ──────────────────────────────── */
@@ -1266,324 +1213,6 @@ legend {
padding: 0 var(--space-2xs); padding: 0 var(--space-2xs);
} }
/* ── File figures (recap + edit existing files) ──────────── */
.admin-file-list,
.recap-files-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-s);
}
.admin-file-list-item,
.recap-files-list-item {
list-style: none;
display: flex;
align-items: center;
gap: var(--space-s);
}
.admin-file-figure {
display: flex;
align-items: flex-start;
gap: var(--space-s);
background: var(--bg-secondary);
border-radius: var(--radius);
padding: var(--space-s);
margin: 0;
flex: 1;
}
.admin-file-icon {
font-size: 1.5rem;
flex-shrink: 0;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-muted);
border-radius: var(--radius);
}
.admin-file-thumb {
max-width: 120px;
max-height: 90px;
object-fit: cover;
border-radius: var(--radius);
flex-shrink: 0;
}
.admin-file-caption {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-3xs);
}
.admin-file-name-row {
display: flex;
align-items: center;
gap: var(--space-2xs);
}
.admin-file-name {
font-weight: 600;
font-size: var(--step--1);
color: var(--text-primary);
}
.admin-file-peertube-id {
font-size: var(--step--2);
color: var(--text-tertiary);
font-family: monospace;
}
.admin-file-meta-row {
display: flex;
gap: var(--space-xs);
align-items: center;
font-size: var(--step--2);
color: var(--text-tertiary);
}
.admin-file-label {
font-size: var(--step--1);
color: var(--text-secondary);
font-style: italic;
}
/* ── Recap files table ─────────────────────────────────────────── */
.recap-files-table {
width: 100%;
border-collapse: collapse;
font-size: var(--step--1);
}
.recap-files-table thead th {
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: var(--step--2);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--space-xs) var(--space-s);
border-bottom: 2px solid var(--border-primary);
}
.recap-files-table tbody td {
padding: var(--space-xs) var(--space-s);
border-bottom: 1px solid var(--border-secondary);
vertical-align: middle;
}
.recap-files-table tbody tr:last-child td {
border-bottom: none;
}
.recap-files-icon {
width: 1px;
white-space: nowrap;
}
.recap-files-icon-emoji {
font-size: 1.3rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2rem;
height: 2.2rem;
background: var(--accent-muted);
border-radius: var(--radius);
}
.recap-files-thumb {
max-width: 80px;
max-height: 60px;
object-fit: cover;
border-radius: var(--radius);
}
.recap-files-name {
font-weight: 500;
}
.recap-files-name a {
color: var(--text-primary);
text-decoration: none;
}
.recap-files-name a:hover {
color: var(--accent-primary);
}
.recap-files-peertube-id {
display: block;
font-size: var(--step--2);
color: var(--text-tertiary);
font-family: monospace;
}
.recap-files-label {
display: block;
font-size: var(--step--2);
color: var(--text-secondary);
font-style: italic;
}
.recap-files-type {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
}
.recap-files-size {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.recap-files-date {
font-size: var(--step--2);
color: var(--text-tertiary);
white-space: nowrap;
}
/* ── File browser (relink) ──────────────────────────────────────── */
.file-browser-trigger {
margin-top: var(--space-2xs);
font-size: var(--step--2);
}
.relink-modal {
width: min(90vw, 700px);
max-height: 80vh;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-primary);
padding: 0;
overflow: hidden;
color: var(--text-primary);
}
.relink-modal::backdrop {
background: rgba(0,0,0,0.5);
}
.relink-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-s) var(--space-m);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.relink-modal-header h3 {
margin: 0;
font-size: var(--step-0);
}
.relink-modal-footer {
padding: var(--space-xs) var(--space-m);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
#relink-modal-body {
padding: var(--space-s) var(--space-m);
overflow-y: auto;
max-height: 60vh;
}
/* ── File browser tree ─────────────────────────────────────────── */
.file-browser {
font-size: var(--step--1);
}
.file-browser-hint,
.file-browser-empty,
.file-browser-loading,
.file-browser-error {
color: var(--text-secondary);
text-align: center;
padding: var(--space-m);
}
.file-browser-error {
color: var(--error);
}
.file-browser-breadcrumb {
display: flex;
align-items: center;
gap: var(--space-2xs);
flex-wrap: wrap;
padding-bottom: var(--space-s);
margin-bottom: var(--space-s);
border-bottom: 1px solid var(--border);
font-size: var(--step--2);
}
.file-browser-breadcrumb a {
color: var(--accent-blue, var(--link));
text-decoration: none;
}
.file-browser-breadcrumb a:hover {
color: var(--accent-primary);
}
.file-browser-sep {
color: var(--text-tertiary);
}
.file-browser-list {
list-style: none;
margin: 0;
padding: 0;
}
.file-browser-entry {
border-bottom: 1px solid var(--border-subtle);
}
.file-browser-entry a,
.file-browser-select-btn {
display: flex;
align-items: center;
gap: var(--space-xs);
width: 100%;
padding: var(--space-xs) var(--space-2xs);
text-decoration: none;
color: var(--text-primary);
background: none;
border: none;
cursor: pointer;
font-size: var(--step--1);
text-align: left;
transition: background 0.15s;
}
.file-browser-entry a:hover,
.file-browser-select-btn:hover {
background: var(--bg-secondary);
}
.file-browser-icon {
flex-shrink: 0;
font-size: var(--step-0);
}
.file-browser-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-browser-size {
flex-shrink: 0;
color: var(--text-tertiary);
font-size: var(--step--2);
font-variant-numeric: tabular-nums;
}
.file-browser-file .file-browser-select-btn {
color: var(--accent-green, var(--success));
}
.file-browser-file .file-browser-select-btn:hover {
background: var(--bg-secondary);
}
/* ── Mobile-responsive layout ──────────────────────────────────────────── */ /* ── Mobile-responsive layout ──────────────────────────────────────────── */
/* Below 600px: labels stack above inputs, single-column form layout. /* Below 600px: labels stack above inputs, single-column form layout.

View File

@@ -1,221 +0,0 @@
/**
* upload-progress.js
*
* Intercepts admin form submissions with files and submits via XMLHttpRequest.
* Polls GET /admin/actions/upload-progress.php?token=xxx for server-side
* processing progress (PeerTube uploads, file moves).
*
* Progress display:
* 0%25% : browser → server upload (XHR upload.progress)
* 25%99% : server processing (polled from progress endpoint)
* 100% : response received — "Téléversé avec succès", then redirect
*/
(() => {
const FORMS = document.querySelectorAll("form[data-upload-progress]");
if (!FORMS.length) return;
const POLL_INTERVAL = 400;
const UPLOAD_CAP = 25;
const PROCESSING_MAX = 99;
const SUCCESS_DELAY = 800;
for (const form of FORMS) {
const progressWrap = form.querySelector("#upload-progress-wrap");
const progressBar = form.querySelector("#upload-progress-bar");
const progressLabel = form.querySelector("#upload-progress-label");
const progressFile = form.querySelector("#upload-progress-file");
const submitBtn = form.querySelector('button[type="submit"]');
const tokenInput = form.querySelector('input[name="progress_token"]');
if (!progressBar || !progressWrap) continue;
function collectFileNames() {
const names = [];
// Check raw <input type="file"> elements (non-FilePond)
const inputs = form.querySelectorAll('input[type="file"]');
for (const fi of inputs) {
if (fi.files) {
for (const f of fi.files) {
if (f.name) names.push(f.name);
}
}
}
// Read processed file names from FilePond instances (async mode)
if (typeof FilePond !== "undefined") {
const pondInputs = form.querySelectorAll(".tfe-file-picker");
for (const pi of pondInputs) {
const pond = FilePond.find(pi);
if (pond) {
const pondFiles = pond.getFiles();
for (const pf of pondFiles) {
// Only count successfully uploaded files (have serverId)
if (pf.serverId) {
const name = pf.filename || pf.file?.name || pf.serverId;
if (name) names.push(name);
}
}
}
}
}
return names;
}
form.addEventListener("submit", (e) => {
// ── Guard: block submit if any FilePond item is still uploading ──
if (typeof FilePond !== "undefined") {
let stillUploading = false;
const pondInputs = form.querySelectorAll(".tfe-file-picker");
for (const pi of pondInputs) {
const pond = FilePond.find(pi);
if (pond) {
const pondFiles = pond.getFiles();
for (const pf of pondFiles) {
if (
pf.status === FilePond.FileStatus.PROCESSING ||
pf.status === FilePond.FileStatus.IDLE
) {
stillUploading = true;
break;
}
}
}
if (stillUploading) break;
}
if (stillUploading) {
e.preventDefault();
progressLabel.textContent =
"Veuillez attendre la fin du téléversement…";
progressWrap.style.display = "";
return;
}
}
const fileNames = collectFileNames();
if (!fileNames.length) return;
e.preventDefault();
const token = tokenInput ? tokenInput.value : "";
progressWrap.style.display = "";
progressBar.value = 0;
progressBar.removeAttribute("data-complete");
progressLabel.textContent = "Téléversement en cours…";
progressFile.textContent =
fileNames.length === 1 ? fileNames[0] : `${fileNames.length} fichiers`;
if (submitBtn) submitBtn.disabled = true;
const fd = new FormData(form);
const xhr = new XMLHttpRequest();
let _uploadDone = false;
let lastUploadPct = 0;
let pollingTimer = null;
/** Poll server-side progress */
function startPolling() {
if (pollingTimer || !token) return;
progressLabel.textContent = "Traitement en cours…";
pollingTimer = setInterval(() => {
fetch(
"/admin/actions/upload-progress.php?token=" +
encodeURIComponent(token),
)
.then((r) => r.json())
.then((data) => {
if (data?.stage && data.stage !== "upload") {
const pct = Math.min(
PROCESSING_MAX,
Math.max(UPLOAD_CAP, data.pct || UPLOAD_CAP),
);
progressBar.value = pct;
if (data.file) {
progressFile.textContent = data.file;
}
}
})
.catch(() => {
/* ignore poll errors */
});
}, POLL_INTERVAL);
}
function stopPolling() {
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
}
function finishSuccess() {
stopPolling();
progressBar.value = 100;
progressBar.setAttribute("data-complete", "");
progressLabel.textContent = "Téléversé avec succès";
progressFile.textContent = "";
}
// ── Upload phase (0% → UPLOAD_CAP) ──
xhr.upload.addEventListener("progress", (evt) => {
if (evt.lengthComputable) {
const rawPct = Math.round((evt.loaded / evt.total) * 100);
const scaled = Math.round((rawPct / 100) * UPLOAD_CAP);
if (scaled > lastUploadPct) {
lastUploadPct = scaled;
progressBar.value = scaled;
}
}
});
xhr.upload.addEventListener("loadend", () => {
_uploadDone = true;
progressBar.value = UPLOAD_CAP;
startPolling();
});
// ── Response handling ──
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
stopPolling();
if (xhr.status >= 200 && xhr.status < 300) {
finishSuccess();
setTimeout(() => {
const finalUrl = xhr.responseURL || "";
if (finalUrl && finalUrl !== form.action) {
window.location.href = finalUrl;
} else {
document.open();
document.write(xhr.responseText);
document.close();
}
}, SUCCESS_DELAY);
} else {
progressLabel.textContent = "Erreur";
progressFile.textContent = "Échec du téléversement";
document.open();
document.write(xhr.responseText);
document.close();
}
});
xhr.addEventListener("error", () => {
stopPolling();
progressLabel.textContent = "Erreur réseau";
progressFile.textContent = "";
if (submitBtn) submitBtn.disabled = false;
});
xhr.addEventListener("abort", () => {
stopPolling();
progressWrap.style.display = "none";
if (submitBtn) submitBtn.disabled = false;
});
xhr.open("POST", form.action, true);
xhr.send(fd);
});
}
})();

View File

@@ -58,7 +58,7 @@ $pageTitle = 'Merci — TFE enregistré';
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="/assets/favicon/favicon.ico"> <link rel="shortcut icon" href="/assets/favicon/favicon.ico">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>"> <link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form.css') ?>"> <link rel="stylesheet" href="<?= App::assetV('/assets/css/form-base.css') ?>">
</head> </head>
<body class="student-body"> <body class="student-body">
<main id="main-content" class="partage-recap"> <main id="main-content" class="partage-recap">

View File

@@ -71,7 +71,7 @@ $pageTitle = 'Corriger l\'adresse e-mail';
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="/assets/favicon/favicon.ico"> <link rel="shortcut icon" href="/assets/favicon/favicon.ico">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>"> <link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form.css') ?>"> <link rel="stylesheet" href="<?= App::assetV('/assets/css/form-base.css') ?>">
</head> </head>
<body class="student-body"> <body class="student-body">
<main id="main-content" class="partage-retry-email"> <main id="main-content" class="partage-retry-email">

View File

@@ -163,7 +163,7 @@ class ThesisEditController
* the transaction is still open, but this method rolls * the transaction is still open, but this method rolls
* back internally before re-throwing). * back internally before re-throwing).
*/ */
public function save(int $thesisId, array $post, array $files, ?string $progressToken = null): void public function save(int $thesisId, array $post, array $files): void
{ {
if ($thesisId <= 0) { if ($thesisId <= 0) {
throw new InvalidArgumentException('ID de TFE invalide.'); throw new InvalidArgumentException('ID de TFE invalide.');

View File

@@ -455,30 +455,4 @@ class PeerTubeService
throw new \RuntimeException('Erreur réseau PeerTube : ' . $e->getMessage(), 0, $e); throw new \RuntimeException('Erreur réseau PeerTube : ' . $e->getMessage(), 0, $e);
} }
} }
// -------------------------------------------------------------------------
// Progress reporting (for upload-progress.js polling)
// -------------------------------------------------------------------------
/**
* Write upload progress to a temp file polled by the progress endpoint.
*/
public static function writeProgress(string $token, string $stage, int $pct, string $file = ''): void
{
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
file_put_contents($progressFile, json_encode([
'stage' => $stage,
'pct' => $pct,
'file' => $file,
]), LOCK_EX);
}
/**
* Remove the progress file for a given token.
*/
public static function clearProgress(string $token): void
{
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
@unlink($progressFile);
}
} }

View File

@@ -7,7 +7,7 @@
// Admin: append suffix to title and prepend admin.css // Admin: append suffix to title and prepend admin.css
if (!empty($isAdmin)) { if (!empty($isAdmin)) {
$pageTitle = isset($pageTitle) ? $pageTitle . ' Admin' : 'Admin'; $pageTitle = isset($pageTitle) ? $pageTitle . ' Admin' : 'Admin';
$extraCss = array_merge(['/assets/css/admin.css'], $extraCss ?? []); $extraCss = array_merge(['/assets/css/admin.css'], $extraCssAdmin ?? [], $extraCss ?? []);
} }
?> ?>
<title><?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?></title> <title><?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?></title>