upload
This commit is contained in:
@@ -0,0 +1,819 @@
|
||||
<?php
|
||||
/**
|
||||
* Image Uploader with EXIF Date Extraction (Multi-File Support)
|
||||
*
|
||||
* Features:
|
||||
* - Mutiple Files Upload
|
||||
* - Specific Naming Convention: <Date> - <Description>.<Extension>
|
||||
* - Date from EXIF "DateTimeOriginal"
|
||||
* - Max size 15MB per file
|
||||
* - Live Countdown & Progress Bar
|
||||
* - Modern Dark UI
|
||||
*/
|
||||
|
||||
// EVENT CONFIGURATION
|
||||
const EVENT_NAME = 'Event Bilder Upload';
|
||||
const EVENT_SUBTITLE = 'Speichere deine Momente datumsgerecht.';
|
||||
|
||||
// DEADLINE CONFIGURATION
|
||||
// Format: YYYY-MM-DD HH:MM:SS
|
||||
const UPLOAD_DEADLINE = '2026-11-30 23:59:59';
|
||||
|
||||
// Timezone
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$messages = []; // Array to store status messages for each file
|
||||
|
||||
// Maximum file size in bytes (15 MB)
|
||||
const MAX_FILE_SIZE = 15 * 1024 * 1024;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json');
|
||||
$responseMessages = [];
|
||||
|
||||
// Check Deadline
|
||||
if (time() > strtotime(UPLOAD_DEADLINE)) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'messages' => [['type' => 'error', 'text' => 'Der Upload-Zeitraum ist abgelaufen.']]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetDir = __DIR__ . "/images/";
|
||||
|
||||
// Ensure directory exists
|
||||
if (!is_dir($targetDir)) {
|
||||
if (!@mkdir($targetDir, 0755, true)) {
|
||||
$errorInfo = error_get_last();
|
||||
$sysError = $errorInfo ? $errorInfo['message'] : 'Unbekannter Fehler';
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'messages' => [['type' => 'error', 'text' => "Server-Fehler: Konnte Verzeichnis 'images' nicht erstellen. ($sysError)"]]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_FILES['images'])) {
|
||||
$fileCount = count($_FILES['images']['name']);
|
||||
$descriptions = $_POST['descriptions'] ?? [];
|
||||
|
||||
for ($i = 0; $i < $fileCount; $i++) {
|
||||
$fileName = $_FILES['images']['name'][$i];
|
||||
$fileTmpPath = $_FILES['images']['tmp_name'][$i];
|
||||
$fileSize = $_FILES['images']['size'][$i];
|
||||
$fileError = $_FILES['images']['error'][$i];
|
||||
$description = trim($descriptions[$i] ?? '');
|
||||
|
||||
// Context prefix for messages
|
||||
$msgPrefix = "<strong>$fileName</strong>: ";
|
||||
|
||||
// Validate Description
|
||||
$descriptionSafe = preg_replace('/[^a-zA-Z0-9\-_äöüÄÖÜß ]/u', '', $description);
|
||||
$descriptionSafe = trim(preg_replace('/\s+/', ' ', $descriptionSafe));
|
||||
|
||||
if (empty($descriptionSafe)) {
|
||||
$responseMessages[] = ['type' => 'error', 'text' => $msgPrefix . "Keine gültige Beschreibung."];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fileError !== UPLOAD_ERR_OK) {
|
||||
$errorMessages = [
|
||||
UPLOAD_ERR_INI_SIZE => 'Datei zu groß (server limit).',
|
||||
UPLOAD_ERR_FORM_SIZE => 'Datei zu groß (form limit).',
|
||||
UPLOAD_ERR_PARTIAL => 'Nur teilweise hochgeladen.',
|
||||
UPLOAD_ERR_NO_FILE => 'Keine Datei.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Temporärer Ordner fehlt.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Schreibfehler.',
|
||||
];
|
||||
$err = $errorMessages[$fileError] ?? "Fehler Code $fileError";
|
||||
$responseMessages[] = ['type' => 'error', 'text' => $msgPrefix . $err];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fileSize > MAX_FILE_SIZE) {
|
||||
$responseMessages[] = ['type' => 'error', 'text' => $msgPrefix . "Größer als 15MB."];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extension Check
|
||||
$fileNameCmps = explode(".", $fileName);
|
||||
$fileExtension = strtolower(end($fileNameCmps));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'heic'];
|
||||
|
||||
if (!in_array($fileExtension, $allowedExtensions)) {
|
||||
$responseMessages[] = ['type' => 'error', 'text' => $msgPrefix . "Format nicht erlaubt."];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exif Date
|
||||
$dateStr = null;
|
||||
if (function_exists('exif_read_data')) {
|
||||
$exif = @exif_read_data($fileTmpPath);
|
||||
if ($exif && !empty($exif['DateTimeOriginal'])) {
|
||||
$dateTimeObj = DateTime::createFromFormat('Y:m:d H:i:s', $exif['DateTimeOriginal']);
|
||||
if ($dateTimeObj) {
|
||||
$dateStr = $dateTimeObj->format('d-m-Y');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$dateStr) {
|
||||
$dateStr = date('d-m-Y');
|
||||
}
|
||||
|
||||
// Construct Target Path
|
||||
$newFileName = "{$dateStr} - {$descriptionSafe}.{$fileExtension}";
|
||||
$dest_path = $targetDir . $newFileName;
|
||||
|
||||
// Handle Duplicates
|
||||
$counter = 1;
|
||||
$info = pathinfo($dest_path);
|
||||
while (file_exists($dest_path)) {
|
||||
$dest_path = $info['dirname'] . '/' . $info['filename'] . " ($counter)." . $info['extension'];
|
||||
$newFileName = basename($dest_path);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||
$responseMessages[] = ['type' => 'success', 'text' => $msgPrefix . "Gespeichert als \"$newFileName\"."];
|
||||
} else {
|
||||
$errorInfo = error_get_last();
|
||||
$sysError = $errorInfo ? $errorInfo['message'] : 'Unbekannter Fehler';
|
||||
$responseMessages[] = ['type' => 'error', 'text' => $msgPrefix . "Upload gescheitert: $sysError"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'done', 'messages' => $responseMessages]);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars(EVENT_NAME) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f5f3f0;
|
||||
--card-bg: #ffffff;
|
||||
--text-main: #2c2c2c;
|
||||
--text-muted: #7a7a7a;
|
||||
--primary: #4a4a4a;
|
||||
--primary-hover: #333333;
|
||||
--accent: #6b7b6e;
|
||||
--success: #5a7a5e;
|
||||
--error: #a0503c;
|
||||
--border: #ddd8d2;
|
||||
--border-focus: #b0aaa2;
|
||||
--input-bg: #faf9f7;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
p.subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Countdown */
|
||||
.countdown-box {
|
||||
text-align: center;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 28px;
|
||||
color: var(--text-main);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Batch Controls */
|
||||
.batch-controls {
|
||||
background: var(--input-bg);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-group-batch {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group-batch label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-main);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]::placeholder {
|
||||
color: #b0aaa2;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 10px 18px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-main);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
/* Drop Area */
|
||||
.file-drop-area {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 36px;
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg);
|
||||
transition: border-color 0.2s;
|
||||
cursor: pointer;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.file-drop-area:hover,
|
||||
.file-drop-area.dragover {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.fake-btn {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
pointer-events: none;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.file-msg {
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* File List */
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: var(--input-bg);
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
background: #e8e5e0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
transition: color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--accent);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* Submit */
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
background: var(--border);
|
||||
cursor: not-allowed;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Status */
|
||||
.status-container {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-msg {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-msg.success {
|
||||
background: #eef4ef;
|
||||
color: var(--success);
|
||||
border: 1px solid #d3e0d4;
|
||||
}
|
||||
|
||||
.status-msg.error {
|
||||
background: #f8efec;
|
||||
color: var(--error);
|
||||
border: 1px solid #e6d4ce;
|
||||
}
|
||||
|
||||
/* Mobile Optimization */
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 24px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.batch-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h1><?= htmlspecialchars(EVENT_NAME) ?></h1>
|
||||
<p class="subtitle"><?= htmlspecialchars(EVENT_SUBTITLE) ?></p>
|
||||
|
||||
<!-- Countdown -->
|
||||
<div id="countdown" class="countdown-box">Lädt...</div>
|
||||
|
||||
<!-- Batch Controls -->
|
||||
<div class="batch-controls">
|
||||
<div class="form-group-batch">
|
||||
<label>Sammel-Beschreibung</label>
|
||||
<input type="text" id="globalDesc" placeholder="Eine Beschreibung für alle...">
|
||||
</div>
|
||||
<button type="button" class="btn-secondary" id="applyAllBtn">Auf alle anwenden</button>
|
||||
</div>
|
||||
|
||||
<form action="" method="POST" enctype="multipart/form-data" id="uploadForm">
|
||||
|
||||
<!-- Drop Area -->
|
||||
<div class="file-drop-area" id="dropArea">
|
||||
<span class="fake-btn">Dateien wählen</span>
|
||||
<span class="file-msg">oder per Drag & Drop (Max. 15MB/Bild)</span>
|
||||
<input class="file-input" type="file" name="images[]" id="fileInput" accept="image/*" multiple>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic File List -->
|
||||
<div class="file-list" id="fileList"></div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit" id="submitBtn">Alles hochladen</button>
|
||||
</form>
|
||||
|
||||
<!-- Status Output -->
|
||||
<div class="status-container" id="statusContainer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Configuration from PHP
|
||||
const DEADLINE_TIMESTAMP = <?php echo strtotime(UPLOAD_DEADLINE) * 1000; ?>;
|
||||
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const dropArea = document.getElementById('dropArea');
|
||||
const fileListEl = document.getElementById('fileList');
|
||||
const globalDescInput = document.getElementById('globalDesc');
|
||||
const applyAllBtn = document.getElementById('applyAllBtn');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const statusContainer = document.getElementById('statusContainer');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const countdownEl = document.getElementById('countdown');
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
/* --- Countdown Logic --- */
|
||||
function updateCountdown() {
|
||||
const now = new Date().getTime();
|
||||
const distance = DEADLINE_TIMESTAMP - now;
|
||||
|
||||
if (distance < 0) {
|
||||
countdownEl.innerHTML = "Upload-Zeitraum abgelaufen.";
|
||||
countdownEl.style.color = "var(--error)";
|
||||
countdownEl.style.borderColor = "#e6d4ce";
|
||||
countdownEl.style.background = "#f8efec";
|
||||
disableForm();
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
countdownEl.innerHTML = `Noch ${days}t ${hours}h ${minutes}m ${seconds}s bis zum Upload-Schluss`;
|
||||
}
|
||||
|
||||
function disableForm() {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = "Upload geschlossen";
|
||||
fileInput.disabled = true;
|
||||
dropArea.style.pointerEvents = 'none'; // Disable drop area interaction
|
||||
dropArea.style.opacity = '0.6'; // Visual cue
|
||||
applyAllBtn.disabled = true;
|
||||
globalDescInput.disabled = true;
|
||||
// Also disable individual description inputs if any are rendered
|
||||
Array.from(document.getElementsByName('descriptions[]')).forEach(input => input.disabled = true);
|
||||
}
|
||||
|
||||
setInterval(updateCountdown, 1000);
|
||||
updateCountdown(); // Initial call to display immediately
|
||||
|
||||
/* --- File Handling --- */
|
||||
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
|
||||
|
||||
dropArea.addEventListener('dragenter', () => dropArea.classList.add('dragover'));
|
||||
dropArea.addEventListener('dragleave', () => dropArea.classList.remove('dragover'));
|
||||
dropArea.addEventListener('drop', (e) => dropArea.classList.remove('dragover'));
|
||||
|
||||
function handleFiles(files) {
|
||||
// Check if form is disabled due to deadline
|
||||
if (submitBtn.disabled) {
|
||||
alert("Der Upload-Zeitraum ist abgelaufen. Es können keine neuen Dateien hinzugefügt werden.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) dataTransfer.items.add(files[i]);
|
||||
fileInput.files = dataTransfer.files;
|
||||
renderFileList();
|
||||
}
|
||||
|
||||
function renderFileList() {
|
||||
fileListEl.innerHTML = '';
|
||||
Array.from(fileInput.files).forEach((file, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'file-item';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'file-preview';
|
||||
img.src = URL.createObjectURL(file);
|
||||
img.onload = () => URL.revokeObjectURL(img.src);
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'file-info';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'file-name';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const descInput = document.createElement('input');
|
||||
descInput.type = 'text';
|
||||
descInput.name = 'descriptions[]';
|
||||
descInput.placeholder = 'Beschreibung...';
|
||||
descInput.required = true;
|
||||
if (submitBtn.disabled) { // If deadline passed, disable new inputs
|
||||
descInput.disabled = true;
|
||||
}
|
||||
|
||||
infoDiv.appendChild(nameSpan);
|
||||
infoDiv.appendChild(descInput);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'btn-remove';
|
||||
removeBtn.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
||||
removeBtn.onclick = () => removeFile(index);
|
||||
if (submitBtn.disabled) { // If deadline passed, disable remove button
|
||||
removeBtn.disabled = true;
|
||||
removeBtn.style.opacity = '0.5';
|
||||
removeBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
|
||||
item.appendChild(img);
|
||||
item.appendChild(infoDiv);
|
||||
item.appendChild(removeBtn);
|
||||
|
||||
fileListEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
const dt = new DataTransfer();
|
||||
const files = fileInput.files;
|
||||
const currentDescs = Array.from(document.getElementsByName('descriptions[]')).map(i => i.value);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (i !== index) dt.items.add(files[i]);
|
||||
}
|
||||
|
||||
fileInput.files = dt.files;
|
||||
renderFileList();
|
||||
|
||||
const newInputs = document.getElementsByName('descriptions[]');
|
||||
let offset = 0;
|
||||
for (let i = 0; i < currentDescs.length; i++) {
|
||||
if (i === index) {
|
||||
offset = 1;
|
||||
continue;
|
||||
}
|
||||
if (newInputs[i - offset]) newInputs[i - offset].value = currentDescs[i];
|
||||
}
|
||||
|
||||
dataTransfer.items.clear();
|
||||
for (let i = 0; i < dt.files.length; i++) dataTransfer.items.add(dt.files[i]);
|
||||
}
|
||||
|
||||
applyAllBtn.addEventListener('click', () => {
|
||||
const val = globalDescInput.value;
|
||||
if (!val) return;
|
||||
const inputs = document.getElementsByName('descriptions[]');
|
||||
for (let input of inputs) input.value = val;
|
||||
});
|
||||
|
||||
/* --- AJAX Upload --- */
|
||||
uploadForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Client-side deadline check (redundant but good UX)
|
||||
if (new Date().getTime() > DEADLINE_TIMESTAMP) {
|
||||
alert("Der Upload-Zeitraum ist abgelaufen. Bitte aktualisieren Sie die Seite.");
|
||||
disableForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (fileInput.files.length === 0) {
|
||||
alert("Bitte wähle mindestens eine Datei aus.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all description fields are filled
|
||||
const descriptionInputs = document.getElementsByName('descriptions[]');
|
||||
for (let input of descriptionInputs) {
|
||||
if (!input.value.trim()) {
|
||||
alert("Bitte füllen Sie alle Beschreibungsfelder aus.");
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData(uploadForm);
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// UI Reset
|
||||
statusContainer.innerHTML = ''; // Clear previous messages
|
||||
progressContainer.style.display = 'block';
|
||||
progressBar.style.width = '0%';
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'Wird hochgeladen...';
|
||||
fileInput.disabled = true; // Disable file input during upload
|
||||
dropArea.style.pointerEvents = 'none';
|
||||
applyAllBtn.disabled = true;
|
||||
globalDescInput.disabled = true;
|
||||
Array.from(document.getElementsByName('descriptions[]')).forEach(input => input.disabled = true);
|
||||
Array.from(document.querySelectorAll('.btn-remove')).forEach(btn => btn.disabled = true);
|
||||
|
||||
|
||||
// Progress Listener
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percent = (event.loaded / event.total) * 100;
|
||||
progressBar.style.width = percent + '%';
|
||||
}
|
||||
});
|
||||
|
||||
// Load Listener
|
||||
xhr.addEventListener('load', () => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = 'Alles hochladen';
|
||||
progressContainer.style.display = 'none';
|
||||
fileInput.disabled = false; // Re-enable file input
|
||||
dropArea.style.pointerEvents = 'auto';
|
||||
applyAllBtn.disabled = false;
|
||||
globalDescInput.disabled = false;
|
||||
Array.from(document.getElementsByName('descriptions[]')).forEach(input => input.disabled = false);
|
||||
Array.from(document.querySelectorAll('.btn-remove')).forEach(btn => btn.disabled = false);
|
||||
|
||||
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
|
||||
if (json.messages && Array.isArray(json.messages)) {
|
||||
json.messages.forEach(msg => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `status-msg ${msg.type}`;
|
||||
div.innerHTML = msg.text;
|
||||
statusContainer.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// If all uploads were successful, clear the file list
|
||||
const allSuccess = json.messages.every(msg => msg.type === 'success');
|
||||
if (allSuccess && json.messages.length > 0) {
|
||||
dataTransfer.items.clear();
|
||||
fileInput.files = dataTransfer.files;
|
||||
renderFileList(); // Clear the displayed list
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'status-msg error';
|
||||
div.textContent = 'Fehler beim Verarbeiten der Antwort: ' + xhr.responseText;
|
||||
statusContainer.appendChild(div);
|
||||
}
|
||||
});
|
||||
|
||||
// Error Listener
|
||||
xhr.addEventListener('error', () => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = 'Alles hochladen';
|
||||
progressContainer.style.display = 'none';
|
||||
fileInput.disabled = false; // Re-enable file input
|
||||
dropArea.style.pointerEvents = 'auto';
|
||||
applyAllBtn.disabled = false;
|
||||
globalDescInput.disabled = false;
|
||||
Array.from(document.getElementsByName('descriptions[]')).forEach(input => input.disabled = false);
|
||||
Array.from(document.querySelectorAll('.btn-remove')).forEach(btn => btn.disabled = false);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'status-msg error';
|
||||
div.textContent = 'Netzwerkfehler beim Upload.';
|
||||
statusContainer.appendChild(div);
|
||||
});
|
||||
|
||||
xhr.open('POST', '', true);
|
||||
xhr.send(formData);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user