mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
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:
@@ -32,12 +32,7 @@ require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
try {
|
||||
$ctrl = ThesisEditController::create();
|
||||
$progressToken = $_POST['progress_token'] ?? bin2hex(random_bytes(8));
|
||||
$ctrl->save($thesisId, $_POST, $_FILES, $progressToken);
|
||||
|
||||
// Clean up progress file
|
||||
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||
PeerTubeService::clearProgress($progressToken);
|
||||
$ctrl->save($thesisId, $_POST, $_FILES);
|
||||
|
||||
// Regenerate CSRF token after successful save
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
@@ -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);
|
||||
@@ -54,7 +54,8 @@ function wasSelected($key, $value) {
|
||||
|
||||
$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'];
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
|
||||
@@ -39,7 +39,8 @@ try {
|
||||
}
|
||||
|
||||
$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'];
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
|
||||
@@ -515,7 +515,7 @@ if ($isHtmx) {
|
||||
include APP_ROOT . '/templates/admin/index-table.php';
|
||||
}
|
||||
} 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'];
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
|
||||
@@ -74,7 +74,7 @@ if (empty($_SESSION['csrf_token'])) {
|
||||
}
|
||||
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
$extraCss = ['/assets/css/system.css'];
|
||||
$extraCssAdmin = ['/assets/css/system.css'];
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/parametres.php';
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* ── Form styles → see form.css ─────────────────────────────────────────── */
|
||||
/* ── Form styles → see form-base.css + form-admin.css ───────────────────── */
|
||||
|
||||
/* ── Buttons ────────────────────────────────────────────────────────────── */
|
||||
.admin-form-footer {
|
||||
@@ -763,7 +763,7 @@ th.admin-ap-col {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ── Jury fieldset → see form.css ───────────────────────────────────────── */
|
||||
/* ── Jury fieldset → see form-base.css ──────────────────────────────────── */
|
||||
|
||||
/* ── Inline form (tags page) ────────────────────────────────────────────── */
|
||||
.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 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
234
app/public/assets/css/form-admin.css
Normal file
234
app/public/assets/css/form-admin.css
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
Admin-only extensions live in form-admin.css.
|
||||
Variables loaded via style.css (colors.css + typography.css).
|
||||
============================================================ */
|
||||
|
||||
@@ -1156,61 +1157,7 @@
|
||||
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 ──────────────────────────────── */
|
||||
|
||||
@@ -1266,324 +1213,6 @@ legend {
|
||||
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 ──────────────────────────────────────────── */
|
||||
/* Below 600px: labels stack above inputs, single-column form layout.
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -58,7 +58,7 @@ $pageTitle = 'Merci — TFE enregistré';
|
||||
<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="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>
|
||||
<body class="student-body">
|
||||
<main id="main-content" class="partage-recap">
|
||||
|
||||
@@ -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="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/form.css') ?>">
|
||||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form-base.css') ?>">
|
||||
</head>
|
||||
<body class="student-body">
|
||||
<main id="main-content" class="partage-retry-email">
|
||||
|
||||
Reference in New Issue
Block a user