nordabiz/templates/forum/topic.html
Maciej Pienczyn cebe52f303 refactor: Rebranding i aktualizacja modelu AI
- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner"
- Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash
- Zachowano historyczne odniesienia w timeline i dokumentacji

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 14:08:39 +01:00

907 lines
27 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}{{ topic.title }} - Forum - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.topic-breadcrumb {
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.topic-breadcrumb a {
color: var(--primary);
text-decoration: none;
}
.topic-breadcrumb a:hover {
text-decoration: underline;
}
.topic-header {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow);
}
.topic-header.pinned {
border-left: 4px solid var(--primary);
background: linear-gradient(135deg, #eff6ff, var(--surface));
}
.topic-header.locked {
border-left: 4px solid var(--secondary);
}
.topic-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.topic-title {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.topic-badge {
font-size: var(--font-size-sm);
padding: 4px 10px;
border-radius: var(--radius-sm);
font-weight: 500;
}
.badge-pinned {
background: var(--primary);
color: white;
}
.badge-locked {
background: var(--secondary);
color: white;
}
/* Category badges */
.badge-category {
border: 1px solid;
}
.badge-feature_request {
background: #dbeafe;
color: #1e40af;
border-color: #93c5fd;
}
.badge-bug {
background: #fee2e2;
color: #991b1b;
border-color: #fca5a5;
}
.badge-question {
background: #dcfce7;
color: #166534;
border-color: #86efac;
}
.badge-announcement {
background: #fef3c7;
color: #92400e;
border-color: #fcd34d;
}
/* Status badges */
.badge-status {
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.badge-new {
background: #f3f4f6;
color: #374151;
}
.badge-in_progress {
background: #dbeafe;
color: #1e40af;
}
.badge-resolved {
background: #dcfce7;
color: #166534;
}
.badge-rejected {
background: #fee2e2;
color: #991b1b;
}
.topic-meta {
display: flex;
gap: var(--spacing-lg);
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-lg);
}
.topic-meta span {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.topic-content {
line-height: 1.8;
color: var(--text-primary);
white-space: pre-wrap;
}
/* Attachments */
.topic-attachment {
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
}
.attachment-image {
max-width: 100%;
max-height: 500px;
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
}
.attachment-image:hover {
opacity: 0.9;
}
.attachment-info {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
.replies-section {
margin-top: var(--spacing-xl);
}
.replies-header {
font-size: var(--font-size-xl);
font-weight: 600;
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
}
.replies-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.reply-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
border-left: 3px solid var(--border);
}
.reply-card:hover {
border-left-color: var(--primary);
}
.reply-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.reply-author {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-weight: 500;
color: var(--text-primary);
}
.reply-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: var(--font-size-sm);
}
/* AI generated indicator */
.ai-indicator {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
border-radius: var(--radius-sm);
font-size: 10px;
font-weight: 500;
color: #4338ca;
cursor: help;
margin-left: var(--spacing-xs);
}
.ai-indicator svg {
width: 12px;
height: 12px;
}
.reply-content {
line-height: 1.7;
white-space: pre-wrap;
}
.reply-attachments-container {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border);
}
.reply-attachments-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--spacing-sm);
}
.reply-attachment {
position: relative;
}
.reply-attachment img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: var(--radius);
cursor: pointer;
transition: transform 0.2s;
}
.reply-attachment img:hover {
transform: scale(1.02);
}
.reply-attachment .attachment-info {
font-size: 10px;
color: var(--text-secondary);
margin-top: 4px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Single attachment - larger display */
.reply-attachments-grid.single-attachment {
grid-template-columns: 1fr;
max-width: 400px;
}
.reply-attachments-grid.single-attachment .reply-attachment img {
height: auto;
max-height: 300px;
object-fit: contain;
}
.reply-form {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-top: var(--spacing-xl);
box-shadow: var(--shadow);
}
.reply-form h3 {
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-lg);
}
.reply-form textarea {
width: 100%;
min-height: 120px;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-family: var(--font-family);
font-size: var(--font-size-base);
resize: vertical;
margin-bottom: var(--spacing-md);
}
.reply-form textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Upload dropzone in reply form */
.upload-dropzone-mini {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: var(--spacing-md);
text-align: center;
background: var(--background);
transition: var(--transition);
cursor: pointer;
margin-bottom: var(--spacing-md);
}
.upload-dropzone-mini:hover,
.upload-dropzone-mini.drag-over {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.05);
}
.upload-dropzone-mini p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin: 0;
}
.upload-preview-mini {
display: none;
margin-bottom: var(--spacing-md);
padding: var(--spacing-sm);
background: var(--background);
border-radius: var(--radius);
border: 1px solid var(--border);
}
.upload-preview-mini.active {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.upload-preview-mini img {
max-width: 80px;
max-height: 60px;
border-radius: var(--radius-sm);
object-fit: cover;
}
.upload-preview-mini .file-info {
flex: 1;
font-size: var(--font-size-sm);
}
.upload-preview-mini .file-name {
font-weight: 500;
color: var(--text-primary);
}
.upload-preview-mini .file-size {
color: var(--text-secondary);
}
.upload-preview-mini .remove-file {
color: var(--error);
cursor: pointer;
padding: var(--spacing-xs);
}
.upload-preview-mini .remove-file:hover {
background: rgba(239, 68, 68, 0.1);
border-radius: var(--radius);
}
/* Multi-file upload preview grid */
.upload-previews-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.upload-preview-item {
position: relative;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--spacing-xs);
background: var(--surface);
}
.upload-preview-item img {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: var(--radius-sm);
}
.upload-preview-item .preview-info {
font-size: 10px;
color: var(--text-secondary);
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upload-preview-item .remove-preview {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
background: var(--error);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
line-height: 1;
}
.upload-preview-item .remove-preview:hover {
background: #c53030;
}
.upload-counter {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.upload-counter.limit-reached {
color: var(--warning);
}
.form-actions {
display: flex;
gap: var(--spacing-md);
align-items: center;
}
.locked-notice {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: var(--radius);
padding: var(--spacing-md);
margin-top: var(--spacing-xl);
text-align: center;
color: #92400e;
}
.empty-replies {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
background: var(--background);
border-radius: var(--radius);
}
/* Lightbox for images */
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 1000;
justify-content: center;
align-items: center;
cursor: pointer;
}
.lightbox.active {
display: flex;
}
.lightbox img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
@media (max-width: 768px) {
.topic-title-row {
flex-direction: column;
}
.topic-meta {
flex-wrap: wrap;
}
.form-actions {
flex-direction: column;
align-items: stretch;
}
}
</style>
{% endblock %}
{% block content %}
<nav class="topic-breadcrumb">
<a href="{{ url_for('forum_index') }}">Forum</a> &raquo; {{ topic.title[:50] }}{% if topic.title|length > 50 %}...{% endif %}
</nav>
<article class="topic-header {% if topic.is_pinned %}pinned{% endif %} {% if topic.is_locked %}locked{% endif %}">
<div class="topic-title-row">
<h1 class="topic-title">
{% if topic.is_pinned %}
<span class="topic-badge badge-pinned">Przypięty</span>
{% endif %}
{% if topic.is_locked %}
<span class="topic-badge badge-locked">Zamknięty</span>
{% endif %}
<span class="topic-badge badge-category badge-{{ topic.category or 'question' }}">
{{ category_labels.get(topic.category, 'Pytanie') }}
</span>
<span class="topic-badge badge-status badge-{{ topic.status or 'new' }}">
{{ status_labels.get(topic.status, 'Nowy') }}
</span>
{{ topic.title }}
</h1>
</div>
<div class="topic-meta">
<span>
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
{{ topic.author.name or topic.author.email.split('@')[0] }}
{% if topic.is_ai_generated %}
<span class="ai-indicator" title="Wygenerowano przez AI">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
AI
</span>
{% endif %}
</span>
<span>
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{{ topic.created_at.strftime('%d.%m.%Y %H:%M') }}
</span>
<span>
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
{{ topic.views_count }} wyświetleń
</span>
</div>
<div class="topic-content">{{ topic.content }}</div>
{% if topic.attachments %}
{% for attachment in topic.attachments %}
<div class="topic-attachment">
<img src="{{ url_for('static', filename='uploads/forum/topics/' ~ topic.created_at.strftime('%Y/%m/') ~ attachment.stored_filename) }}"
alt="{{ attachment.original_filename }}"
class="attachment-image"
onclick="openLightbox(this.src)">
<div class="attachment-info">
{{ attachment.original_filename }} ({{ (attachment.file_size / 1024)|int }} KB)
</div>
</div>
{% endfor %}
{% endif %}
</article>
<section class="replies-section">
<h2 class="replies-header">
Odpowiedzi ({{ topic.replies|length }})
</h2>
{% if topic.replies %}
<div class="replies-list">
{% for reply in topic.replies %}
<article class="reply-card">
<div class="reply-header">
<div class="reply-author">
<div class="reply-avatar">
{{ (reply.author.name or reply.author.email)[0].upper() }}
</div>
{{ reply.author.name or reply.author.email.split('@')[0] }}
{% if reply.is_ai_generated %}
<span class="ai-indicator" title="Wygenerowano przez AI">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
AI
</span>
{% endif %}
</div>
<span>{{ reply.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
</div>
<div class="reply-content">{{ reply.content }}</div>
{% if reply.attachments %}
<div class="reply-attachments-container">
<div class="reply-attachments-grid {% if reply.attachments|length == 1 %}single-attachment{% endif %}">
{% for attachment in reply.attachments %}
<div class="reply-attachment">
<img src="{{ url_for('static', filename='uploads/forum/replies/' ~ reply.created_at.strftime('%Y/%m/') ~ attachment.stored_filename) }}"
alt="{{ attachment.original_filename }}"
onclick="openLightbox(this.src)">
<div class="attachment-info">
{{ attachment.original_filename|truncate(20) }} ({{ (attachment.file_size / 1024)|int }} KB)
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% else %}
<div class="empty-replies">
Brak odpowiedzi. Bądź pierwszy!
</div>
{% endif %}
</section>
{% if topic.is_locked %}
<div class="locked-notice">
Ten temat jest zamknięty. Nie można dodawać nowych odpowiedzi.
</div>
{% else %}
<form class="reply-form" method="POST" action="{{ url_for('forum_reply', topic_id=topic.id) }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<h3>Dodaj odpowiedź</h3>
<textarea name="content" id="replyContent" placeholder="Twoja odpowiedź..." required></textarea>
<div class="upload-counter" id="uploadCounter"></div>
<div class="upload-previews-container" id="previewsContainer"></div>
<div class="upload-dropzone-mini" id="dropzone">
<p>Przeciągnij obrazy lub kliknij tutaj (max 10 plików, możesz też wkleić Ctrl+V)</p>
<input type="file" id="attachmentInput" name="attachments[]" accept="image/jpeg,image/png,image/gif" multiple style="display: none;">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Wyślij odpowiedź</button>
</div>
</form>
{% endif %}
<!-- Lightbox for enlarged images -->
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
<img id="lightboxImage" src="" alt="Enlarged image">
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
.toast.warning { border-left-color: var(--warning); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
// Lightbox functions
function openLightbox(src) {
document.getElementById('lightboxImage').src = src;
document.getElementById('lightbox').classList.add('active');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('active');
}
// Close lightbox with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeLightbox();
}
});
// Multi-file upload handling (only if form exists)
const dropzone = document.getElementById('dropzone');
if (dropzone) {
const fileInput = document.getElementById('attachmentInput');
const previewsContainer = document.getElementById('previewsContainer');
const uploadCounter = document.getElementById('uploadCounter');
const replyContent = document.getElementById('replyContent');
const MAX_FILES = 10;
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
// Store files in a Map for easy removal
let filesMap = new Map();
let fileIdCounter = 0;
// Click to upload
dropzone.addEventListener('click', () => fileInput.click());
// Drag and drop
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const droppedFiles = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
addFiles(droppedFiles);
});
// File input change
fileInput.addEventListener('change', (e) => {
const selectedFiles = Array.from(e.target.files);
addFiles(selectedFiles);
// Reset input to allow selecting same files again
fileInput.value = '';
});
// Paste from clipboard (Ctrl+V)
document.addEventListener('paste', (e) => {
// Only handle paste if reply textarea is focused
if (document.activeElement !== replyContent && !replyContent.contains(document.activeElement)) {
return;
}
const items = e.clipboardData?.items;
if (!items) return;
const pastedFiles = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
e.preventDefault();
const file = items[i].getAsFile();
if (file) {
pastedFiles.push(file);
}
}
}
if (pastedFiles.length > 0) {
addFiles(pastedFiles);
}
});
function addFiles(newFiles) {
const currentCount = filesMap.size;
const availableSlots = MAX_FILES - currentCount;
if (availableSlots <= 0) {
showToast('Osiągnięto limit ' + MAX_FILES + ' plików', 'warning');
return;
}
const filesToAdd = newFiles.slice(0, availableSlots);
const errors = [];
filesToAdd.forEach(file => {
// Validate size
if (file.size > MAX_SIZE) {
errors.push(file.name + ': za duży (max 5MB)');
return;
}
// Validate type
if (!ALLOWED_TYPES.includes(file.type)) {
errors.push(file.name + ': niedozwolony format');
return;
}
const fileId = 'file_' + (fileIdCounter++);
filesMap.set(fileId, file);
createPreview(fileId, file);
});
if (errors.length > 0) {
showToast('Błędy: ' + errors.join(', '), 'error');
}
updateCounter();
syncFilesToInput();
}
function createPreview(fileId, file) {
const preview = document.createElement('div');
preview.className = 'upload-preview-item';
preview.dataset.fileId = fileId;
const img = document.createElement('img');
const info = document.createElement('div');
info.className = 'preview-info';
info.textContent = file.name.substring(0, 15) + (file.name.length > 15 ? '...' : '') + ' (' + formatFileSize(file.size) + ')';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'remove-preview';
removeBtn.innerHTML = '&times;';
removeBtn.title = 'Usuń';
removeBtn.onclick = () => removeFile(fileId);
preview.appendChild(img);
preview.appendChild(info);
preview.appendChild(removeBtn);
previewsContainer.appendChild(preview);
// Load image preview
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function removeFile(fileId) {
filesMap.delete(fileId);
const preview = previewsContainer.querySelector('[data-file-id="' + fileId + '"]');
if (preview) {
preview.remove();
}
updateCounter();
syncFilesToInput();
}
function updateCounter() {
const count = filesMap.size;
if (count === 0) {
uploadCounter.textContent = '';
uploadCounter.classList.remove('limit-reached');
dropzone.style.display = 'block';
} else {
uploadCounter.textContent = 'Wybrano: ' + count + '/' + MAX_FILES + ' plikow';
uploadCounter.classList.toggle('limit-reached', count >= MAX_FILES);
dropzone.style.display = count >= MAX_FILES ? 'none' : 'block';
}
}
function syncFilesToInput() {
// Create DataTransfer and add all files from Map
const dataTransfer = new DataTransfer();
filesMap.forEach(file => {
dataTransfer.items.add(file);
});
fileInput.files = dataTransfer.files;
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
}
{% endblock %}