2026-04-29 17:49:07 +03:00
|
|
|
{% extends "admin/layout.html" %}
|
|
|
|
|
{% let active_page = "media" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}{{ t.media_upload_title }}{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="page-head">
|
2026-08-09 03:19:47 +01:00
|
|
|
<h1>{{ t.media_title }}</h1>
|
2026-04-29 17:49:07 +03:00
|
|
|
</div>
|
|
|
|
|
|
2026-08-09 03:19:47 +01:00
|
|
|
<section class="admin-section">
|
|
|
|
|
<header class="admin-section-head">
|
|
|
|
|
<span class="admin-section-icon" aria-hidden="true">📤</span>
|
|
|
|
|
<div>
|
|
|
|
|
<h2>{{ t.media_upload_title }}</h2>
|
|
|
|
|
<p>{{ client_name }} · {{ visit_label }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
<div class="admin-section-body">
|
2026-04-29 17:49:07 +03:00
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
<form id="uploadForm" action="/admin/media/{{ visit_id }}/upload/submit" enctype="multipart/form-data">
|
2026-04-29 17:49:07 +03:00
|
|
|
<div class="field">
|
|
|
|
|
<label class="label">{{ t.media_choose_files }}</label>
|
|
|
|
|
<div class="control">
|
2026-05-18 14:47:04 +03:00
|
|
|
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
2026-04-29 17:49:07 +03:00
|
|
|
</div>
|
2026-08-09 03:19:47 +01:00
|
|
|
<p id="fileCount" class="field-help"></p>
|
2026-08-09 01:05:47 +01:00
|
|
|
<div id="uploadQueue" class="upload-queue"></div>
|
2026-04-29 17:49:07 +03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label class="label">{{ t.media_caption }}</label>
|
|
|
|
|
<div class="control">
|
|
|
|
|
<input class="input" type="text" name="caption" placeholder="{{ t.media_caption }}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
<!-- Progress -->
|
2026-08-09 03:19:47 +01:00
|
|
|
<div id="uploadProgress" class="upload-progress">
|
|
|
|
|
<div class="upload-progress-head">
|
2026-05-18 14:47:04 +03:00
|
|
|
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
|
|
|
|
<span id="uploadPercent">0%</span>
|
|
|
|
|
</div>
|
2026-08-09 03:19:47 +01:00
|
|
|
<div class="upload-progress-track">
|
|
|
|
|
<div id="uploadBar" class="upload-progress-bar"></div>
|
2026-05-18 14:47:04 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button type="submit" id="uploadSubmit" class="button is-primary is-fullwidth">{{ t.media_upload }}</button>
|
2026-04-29 17:49:07 +03:00
|
|
|
</form>
|
2026-05-18 14:47:04 +03:00
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
var form = document.getElementById('uploadForm');
|
|
|
|
|
var filesInput = document.getElementById('uploadFiles');
|
|
|
|
|
var fileCount = document.getElementById('fileCount');
|
2026-08-09 01:05:47 +01:00
|
|
|
var queue = document.getElementById('uploadQueue');
|
2026-05-18 14:47:04 +03:00
|
|
|
var progress = document.getElementById('uploadProgress');
|
|
|
|
|
var bar = document.getElementById('uploadBar');
|
|
|
|
|
var percent = document.getElementById('uploadPercent');
|
|
|
|
|
var statusText = document.getElementById('uploadStatusText');
|
|
|
|
|
var submitBtn = document.getElementById('uploadSubmit');
|
|
|
|
|
|
|
|
|
|
filesInput.addEventListener('change', function() {
|
|
|
|
|
var n = this.files.length;
|
2026-08-09 01:05:47 +01:00
|
|
|
fileCount.textContent = n > 0 ? ('{{ t.media_files_selected }}: ' + n) : '';
|
|
|
|
|
queue.replaceChildren();
|
|
|
|
|
Array.from(this.files).forEach(function(file) {
|
|
|
|
|
var item = document.createElement('div');
|
|
|
|
|
item.className = 'upload-queue-item';
|
|
|
|
|
var icon = document.createElement('span');
|
|
|
|
|
icon.className = 'upload-queue-icon';
|
|
|
|
|
icon.textContent = file.type.indexOf('video/') === 0 ? '🎬' : '🖼️';
|
|
|
|
|
var details = document.createElement('div');
|
|
|
|
|
var name = document.createElement('div');
|
|
|
|
|
name.className = 'upload-queue-name';
|
|
|
|
|
name.textContent = file.name;
|
|
|
|
|
var state = document.createElement('div');
|
|
|
|
|
state.className = 'upload-queue-state';
|
|
|
|
|
state.textContent = '0%';
|
|
|
|
|
var track = document.createElement('div');
|
|
|
|
|
track.className = 'upload-queue-track';
|
|
|
|
|
var itemBar = document.createElement('div');
|
|
|
|
|
itemBar.className = 'upload-queue-bar';
|
|
|
|
|
track.appendChild(itemBar);
|
|
|
|
|
details.append(name, state, track);
|
|
|
|
|
item.append(icon, details);
|
|
|
|
|
queue.appendChild(item);
|
|
|
|
|
});
|
2026-05-18 14:47:04 +03:00
|
|
|
});
|
|
|
|
|
|
2026-08-09 01:05:47 +01:00
|
|
|
function updateQueue(state, progressValue, processing) {
|
|
|
|
|
queue.querySelectorAll('.upload-queue-item').forEach(function(item) {
|
|
|
|
|
item.querySelector('.upload-queue-state').textContent = state;
|
|
|
|
|
var itemBar = item.querySelector('.upload-queue-bar');
|
|
|
|
|
itemBar.classList.toggle('is-processing', processing);
|
|
|
|
|
if (!processing) itemBar.style.width = progressValue + '%';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
form.addEventListener('submit', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!filesInput.files.length) return;
|
|
|
|
|
|
|
|
|
|
var data = new FormData(form);
|
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
|
|
|
|
|
progress.style.display = 'block';
|
|
|
|
|
submitBtn.disabled = true;
|
2026-08-09 01:05:47 +01:00
|
|
|
submitBtn.textContent = '{{ t.media_upload_sending }}';
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_sending }}';
|
|
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
bar.style.width = '0%';
|
|
|
|
|
percent.textContent = '0%';
|
|
|
|
|
|
|
|
|
|
xhr.upload.addEventListener('progress', function(ev) {
|
|
|
|
|
if (!ev.lengthComputable) return;
|
|
|
|
|
var pct = Math.round(ev.loaded / ev.total * 100);
|
|
|
|
|
bar.style.width = pct + '%';
|
|
|
|
|
percent.textContent = pct + '%';
|
2026-08-09 01:05:47 +01:00
|
|
|
updateQueue(pct + '%', pct, false);
|
|
|
|
|
if (pct === 100) {
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_processing }}';
|
|
|
|
|
percent.textContent = '•••';
|
|
|
|
|
bar.classList.add('is-processing');
|
|
|
|
|
updateQueue('{{ t.media_upload_processing }}', 100, true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
xhr.upload.addEventListener('load', function() {
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_processing }}';
|
|
|
|
|
percent.textContent = '•••';
|
|
|
|
|
bar.classList.add('is-processing');
|
|
|
|
|
updateQueue('{{ t.media_upload_processing }}', 100, true);
|
2026-05-18 14:47:04 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('load', function() {
|
|
|
|
|
if (xhr.status >= 200 && xhr.status < 400) {
|
|
|
|
|
bar.style.width = '100%';
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
percent.textContent = '100%';
|
2026-08-09 01:05:47 +01:00
|
|
|
statusText.textContent = '{{ t.media_upload_done }}';
|
|
|
|
|
updateQueue('{{ t.media_upload_done }}', 100, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
setTimeout(function() { window.location.href = xhr.responseURL || '/admin/media'; }, 300);
|
|
|
|
|
} else {
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
statusText.textContent = 'Ошибка загрузки (' + xhr.status + ')';
|
2026-08-09 01:05:47 +01:00
|
|
|
updateQueue('Ошибка загрузки', 0, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
submitBtn.textContent = '{{ t.media_upload }}';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('error', function() {
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_connection_error }}';
|
|
|
|
|
updateQueue('{{ t.media_upload_connection_error }}', 0, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
submitBtn.textContent = '{{ t.media_upload }}';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.open('POST', form.action);
|
|
|
|
|
xhr.send(data);
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
2026-08-09 03:19:47 +01:00
|
|
|
</div>
|
|
|
|
|
</section>
|
2026-08-09 01:05:47 +01:00
|
|
|
|
2026-04-29 17:49:07 +03:00
|
|
|
{% endblock %}
|