1
0
Files
medical-notes/stroma/templates/upload_files.html
Johan Dahlin 50366b9b9c
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
vault backup: 2025-12-26 02:09:22
2025-12-26 02:09:22 +01:00

455 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Files</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
max-width: 600px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
h1 {
color: #2d3748;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #718096;
margin-bottom: 30px;
font-size: 14px;
}
.upload-section {
margin-bottom: 25px;
}
.upload-actions {
margin-top: 20px;
display: flex;
gap: 15px;
justify-content: center;
align-items: center;
}
.upload-btn-select {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 24px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.upload-btn-select:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.upload-zone {
border: 2px dashed #cbd5e0;
border-radius: 12px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #f7fafc;
}
.upload-zone:hover {
border-color: #667eea;
background: #edf2f7;
}
.upload-zone.dragover {
border-color: #667eea;
background: #e6f2ff;
}
.upload-icon {
font-size: 48px;
margin-bottom: 15px;
color: #a0aec0;
}
.upload-text {
color: #4a5568;
font-size: 16px;
margin-bottom: 8px;
}
.upload-hint {
color: #a0aec0;
font-size: 13px;
}
input[type="file"] {
display: none;
}
.file-list {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
}
.file-item {
background: #f7fafc;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.file-info {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.file-name {
color: #2d3748;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-path {
color: #a0aec0;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
color: #718096;
font-size: 12px;
margin-left: 10px;
}
.upload-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-top: 20px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.upload-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.upload-btn:active {
transform: translateY(0);
}
.upload-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.progress-container {
margin-top: 20px;
display: none;
}
.progress-bar {
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
margin-top: 10px;
color: #4a5568;
font-size: 14px;
}
.success-message {
background: #c6f6d5;
color: #22543d;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: none;
animation: slideIn 0.3s ease;
}
.error-message {
background: #fed7d7;
color: #742a2a;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: none;
animation: slideIn 0.3s ease;
}
</style>
</head>
<body>
<div class="container">
<h1>📁 Upload Content</h1>
<p class="subtitle">Drag & drop files or folders, or use the buttons below</p>
<div class="upload-section">
<div class="upload-zone" id="uploadZone">
<div class="upload-icon">📤</div>
<div class="upload-text">Drag & drop here</div>
<div class="upload-hint">or click to browse:</div>
<div class="upload-actions">
<button type="button" class="upload-btn-select" id="selectFilesBtn">Select Files</button>
<button type="button" class="upload-btn-select" id="selectFolderBtn">Select Folder</button>
</div>
</div>
<input type="file" id="fileInput" multiple>
<input type="file" id="folderInput" webkitdirectory directory>
</div>
<div class="file-list" id="fileList"></div>
<div class="progress-container" id="progressContainer">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">Uploading...</div>
</div>
<div class="success-message" id="successMessage">
✓ Files uploaded successfully!
</div>
<div class="error-message" id="errorMessage"></div>
</div>
<script>
const fileInput = document.getElementById('fileInput');
const folderInput = document.getElementById('folderInput');
const uploadZone = document.getElementById('uploadZone');
const selectFilesBtn = document.getElementById('selectFilesBtn');
const selectFolderBtn = document.getElementById('selectFolderBtn');
const fileList = document.getElementById('fileList');
const fileCount = document.getElementById('fileCount');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const successMessage = document.getElementById('successMessage');
const errorMessage = document.getElementById('errorMessage');
let selectedFiles = [];
// Manual selection buttons
selectFilesBtn.addEventListener('click', () => fileInput.click());
selectFolderBtn.addEventListener('click', () => folderInput.click());
// Upload zone click (default to file selection)
uploadZone.addEventListener('click', (e) => {
// Only trigger if clicking the zone itself, not the buttons
if (e.target.tagName !== 'BUTTON') {
fileInput.click();
}
});
// File selection
fileInput.addEventListener('change', (e) => {
addFiles(Array.from(e.target.files));
});
folderInput.addEventListener('change', (e) => {
addFiles(Array.from(e.target.files));
});
// Drag and drop
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
addFiles(files);
});
function addFiles(files) {
selectedFiles = [...selectedFiles, ...files];
renderFileList();
if (selectedFiles.length > 0) {
startUpload();
}
}
function renderFileList() {
fileList.innerHTML = '';
selectedFiles.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'file-item';
const info = document.createElement('div');
info.className = 'file-info';
const name = document.createElement('div');
name.className = 'file-name';
name.textContent = file.name;
const path = document.createElement('div');
path.className = 'file-path';
path.textContent = file.webkitRelativePath || file.name;
info.appendChild(name);
if (file.webkitRelativePath) {
info.appendChild(path);
}
const size = document.createElement('div');
size.className = 'file-size';
size.textContent = formatFileSize(file.size);
item.appendChild(info);
item.appendChild(size);
fileList.appendChild(item);
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
async function startUpload() {
if (selectedFiles.length === 0) return;
progressContainer.style.display = 'block';
successMessage.style.display = 'none';
errorMessage.style.display = 'none';
const formData = new FormData();
selectedFiles.forEach((file, index) => {
formData.append('files', file);
if (file.webkitRelativePath) {
formData.append(`path_${index}`, file.webkitRelativePath);
}
});
try {
const response = await fetch('/api/upload/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
if (response.ok) {
const result = await response.json();
progressFill.style.width = '100%';
progressText.textContent = `Uploaded ${result.count} file(s) successfully!`;
setTimeout(() => {
successMessage.style.display = 'block';
progressContainer.style.display = 'none';
selectedFiles = [];
renderFileList();
progressFill.style.width = '0%';
}, 500);
} else {
throw new Error('Upload failed');
}
} catch (error) {
errorMessage.textContent = '✗ Upload failed: ' + error.message;
errorMessage.style.display = 'block';
progressContainer.style.display = 'none';
}
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
</body>
</html>