This commit is contained in:
vladislav 2026-08-30 23:20:16 -04:00
parent 26f4dbbd92
commit 82f1bd9b49
Signed by: Vladislav
GPG Key ID: D97C9E5E4860EE45

View File

@ -51,28 +51,27 @@
<div class="preview" id="preview"></div>
</div>
</form>
<script defer>
const post = JSON.parse({{post|tojson}})
const postId = post._id
<script>
const post = JSON.parse({{ post|tojson }});
const postId = post._id;
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const createBtn = document.getElementById('createBtn');
const form = document.getElementById('postForm');
// Fill form fields
document.getElementById('author').value = post.author;
if (document.getElementById('grade')){
if (document.getElementById('grade')) {
document.getElementById('grade').value = post.grade;
const inputEl = document.getElementById('grade');
let list = inputEl.dataset.list;
list = JSON.parse(list.replace(/'/g, '"'));
list.push(post.grade);
list = [...new Set(list)];
inputEl.dataset.list = JSON.stringify(list);
inputEl.dataset.value = post.grade;
}
document.getElementById('title').value = post.title;
document.getElementById('city').value = post.city;
document.getElementById('size').value = post.size;
@ -80,39 +79,110 @@
document.getElementById('genre').value = post.genre;
document.getElementById('price').value = post.price;
document.getElementById('description').value = post.description;
let filesOrdered = [];
console.log(post.medias_original)
const existingMedias = (post?.medias_original ?? []).map((media) => ({ url: `/b/posts/${media}`, name: `${media}`, isExisting: true, type: "image",}));
async function uploadMediaFiles(files) {
if (!files || files.length === 0) return [];
let filesOrdered = [];
let isSubmitting = false;
const existingMedias = (post?.medias_original ?? []).map((media) => ({
url: `/b/posts/${media}`,
name: `${media}`,
isExisting: true,
type: "image",
}));
const CHUNK_SIZE = 2 * 1024 * 1024; // 2 MB
// ========== Chunked upload ==========
async function uploadFileInChunks(file, onProgress) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileId = crypto.randomUUID();
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
for (let file of files) {
formData.append('medias', file);
}
formData.append('chunk', chunk, file.name);
formData.append('file_id', fileId);
formData.append('chunk_index', i);
formData.append('total_chunks', totalChunks);
formData.append('filename', file.name);
formData.append('content_type', file.type || 'application/octet-stream');
const res = await fetch('/api/posts/media', {
const res = await fetch('/api/posts/media/chunk', {
method: 'POST',
body: formData
});
if (!res.ok) throw new Error('Ошибка загрузки медиа');
return await res.json();
if (!res.ok) throw new Error(`Chunk ${i} failed`);
onProgress((i + 1) / totalChunks);
}
function loadExistingMedias() {
existingMedias.forEach(media => {
filesOrdered.push({
name: media.name,
url: media.url,
isExisting: true,
type: media.type
const finalRes = await fetch('/api/posts/media/finalize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_id: fileId })
});
});
renderPreviews();
if (!finalRes.ok) throw new Error('Finalize failed');
const data = await finalRes.json();
return data.post_medias[0];
}
function createProgressBar(fileName) {
const wrapper = document.createElement('div');
wrapper.className = 'upload-item';
wrapper.innerHTML = `
<div class="upload-name">${fileName}</div>
<div class="progress-bar"><div class="progress-fill"></div></div>
<div class="progress-text">0%</div>
`;
return wrapper;
}
async function uploadMediaFiles(files) {
if (!files || files.length === 0) return { post_medias: [] };
let progressContainer = document.getElementById('uploadProgress');
if (!progressContainer) {
progressContainer = document.createElement('div');
progressContainer.id = 'uploadProgress';
progressContainer.className = 'upload-progress-container';
preview.parentNode.insertBefore(progressContainer, preview);
}
progressContainer.innerHTML = '';
const results = [];
for (const file of files) {
const progressEl = createProgressBar(file.name);
progressContainer.appendChild(progressEl);
const fill = progressEl.querySelector('.progress-fill');
const text = progressEl.querySelector('.progress-text');
try {
const name = await uploadFileInChunks(file, (p) => {
const pct = Math.round(p * 100);
fill.style.width = pct + '%';
text.textContent = pct + '%';
});
results.push(name);
progressEl.classList.add('done');
text.textContent = '✓';
} catch (err) {
progressEl.classList.add('error');
text.textContent = 'Ошибка';
throw err;
}
}
setTimeout(() => progressContainer.remove(), 1200);
return { post_medias: results };
}
// ========== Preview helpers ==========
function createObjectURLSafe(item) {
if (item.isExisting || !item.file) return item.url;
if (!item.previewURL) {
@ -130,7 +200,6 @@
function renderPreviews() {
preview.innerHTML = '';
filesOrdered.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'item';
@ -142,13 +211,11 @@
title.textContent = idx === 0 ? 'Титульная' : `${idx + 1}`;
const isImage = item.type && item.type.startsWith('image');
const mediaEl = document.createElement(isImage ? 'img' : 'video');
mediaEl.className = 'thumb';
mediaEl.src = createObjectURLSafe(item);
if (!isImage) mediaEl.controls = true;
// Контролы
const controls = document.createElement('div');
controls.className = 'controls';
@ -159,13 +226,12 @@
makeFirstBtn.addEventListener('click', () => moveFile(idx, 0));
const leftBtn = document.createElement('button');
leftBtn.textContent = idx;
leftBtn.textContent = '←';
leftBtn.disabled = idx === 0;
leftBtn.addEventListener('click', () => moveFile(idx, idx - 1));
const rightBtn = document.createElement('button');
rightBtn.textContent = idx + 2;
rightBtn.textContent = '→';
rightBtn.disabled = idx === filesOrdered.length - 1;
rightBtn.addEventListener('click', () => moveFile(idx, idx + 1));
@ -173,7 +239,9 @@
removeBtn.textContent = '✖';
removeBtn.addEventListener('click', () => removeFile(idx));
controls.append(idx>0?makeFirstBtn:'', idx>0?leftBtn:'', idx < filesOrdered.length - 1?rightBtn:'', removeBtn);
if (idx > 0) controls.append(makeFirstBtn, leftBtn);
if (idx < filesOrdered.length - 1) controls.append(rightBtn);
controls.append(removeBtn);
div.append(mediaEl, title, controls);
preview.appendChild(div);
@ -183,19 +251,15 @@
e.dataTransfer.setData('text/plain', idx.toString());
div.classList.add('dragging');
});
div.addEventListener('dragend', () => {
div.classList.remove('dragging');
document.querySelectorAll('.item').forEach(el => el.classList.remove('drag-over'));
});
div.addEventListener('dragover', (e) => {
e.preventDefault();
div.classList.add('drag-over');
});
div.addEventListener('dragleave', () => div.classList.remove('drag-over'));
div.addEventListener('drop', (e) => {
e.preventDefault();
div.classList.remove('drag-over');
@ -220,53 +284,61 @@
renderPreviews();
}
function loadExistingMedias() {
existingMedias.forEach(media => {
filesOrdered.push({
name: media.name,
url: media.url,
isExisting: true,
type: media.type
});
});
renderPreviews();
}
// ========== File select → upload → show blob only after success ==========
fileInput.addEventListener('change', async (e) => {
const newFiles = Array.from(e.target.files);
if (newFiles.length === 0) return;
createBtn.disabled = true;
try {
const uploadedNames = await uploadMediaFiles(newFiles);
const uploaded = await uploadMediaFiles(newFiles);
newFiles.forEach((file, i) => {
filesOrdered.push({
name: uploadedNames['post_medias'][i],
url: `/b/posts/${uploadedNames['post_medias'][i]}`,
name: uploaded.post_medias[i],
url: `/b/posts/${uploaded.post_medias[i]}`,
file: file,
type: file.type,
isExisting: false
});
});
renderPreviews();
renderPreviews(); // blob appears only here
} catch (err) {
console.error('Ошибка загрузки фото:', err);
alert('Не удалось загрузить фото. Попробуйте ещё раз.');
}
} finally {
createBtn.disabled = false;
fileInput.value = '';
}
});
window.addEventListener('load', () => {
loadExistingMedias();
});
window.addEventListener('beforeunload', () => {
filesOrdered.forEach(revokeObjectURLSafe);
});
// === Отправка формы ===
// ========== Save button – only one request ==========
document.addEventListener('DOMContentLoaded', () => {
const author = '{{user.username}}';
const grade = '{{user.grade}}';
const form = document.getElementById('postForm');
const createBtn = document.getElementById('createBtn');
const author = '{{ user.username }}';
const grade = '{{ user.grade }}';
createBtn.addEventListener('click', async (e) => {
e.preventDefault();
if (isSubmitting) return;
// ... (проверка обязательных полей остаётся без изменений)
const requiredFields = [...document.querySelectorAll('input[required]'), ...document.querySelectorAll('textarea[required]')];
const requiredFields = [
...document.querySelectorAll('input[required]'),
...document.querySelectorAll('textarea[required]')
];
let hasError = false;
requiredFields.forEach(field => {
@ -276,24 +348,35 @@
: !field.value.trim();
if (isEmpty) {
group.classList.add('error');
group?.classList.add('error');
hasError = true;
} else {
group.classList.remove('error');
group?.classList.remove('error');
}
});
if (hasError) return;
// === Подготовка данных ===
if (filesOrdered.length === 0) {
showToast({ message: 'Поле фото должно быть заполнено', duration: 2500 });
return;
}
// Lock
isSubmitting = true;
createBtn.disabled = true;
createBtn.classList.add('disabled');
const data = new FormData(form);
if (document.querySelector('#author').value === '') {
if (document.querySelector('#author')?.value === '') {
data.set('author', author);
}
{% if 'user' in user.get('roles') and user.get('grade') is not none %}
data.set('grade', grade);
{% endif %}
filesOrdered.forEach(f => data.append('post_medias', f.name));
try {
@ -307,21 +390,31 @@
const next = params.get('next') || '/profile/posts';
window.location.href = next;
} else {
showToast({message: 'Ошибка при сохранение поста', status: "error", duration:2500});
showToast({ message: 'Ошибка при сохранении поста', status: 'error', duration: 2500 });
isSubmitting = false;
createBtn.disabled = false;
createBtn.classList.remove('disabled');
}
} catch (err) {
console.error(err);
showToast({message: 'Ошибка соединения', status: "error", duration:2500});
showToast({ message: 'Ошибка соединения', status: 'error', duration: 2500 });
isSubmitting = false;
createBtn.disabled = false;
createBtn.classList.remove('disabled');
}
});
// Удаление ошибки при вводе
document.querySelectorAll('.floating-group input, .floating-group textarea').forEach(input => {
input.addEventListener('input', () => {
input.closest('.floating-group').classList.remove('error');
input.closest('.floating-group')?.classList.remove('error');
});
});
});
window.addEventListener('load', loadExistingMedias);
window.addEventListener('beforeunload', () => {
filesOrdered.forEach(revokeObjectURLSafe);
});
</script>
@ -329,6 +422,62 @@
{% macro style() -%}
<style>
.upload-progress-container {
margin: 12px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.upload-item {
background: #f5f5f5;
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
}
.upload-name {
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.progress-bar {
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
border-radius: 3px;
transition: width 0.25s ease;
}
.progress-text {
margin-top: 4px;
font-size: 12px;
color: #666;
}
.upload-item.done .progress-fill {
background: #16a34a;
}
.upload-item.error .progress-fill {
background: #dc2626;
}
#createBtn.disabled,
#createBtn:disabled {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
.preview {
display: flex;
gap: 8px;