1
0

vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s

This commit is contained in:
2025-12-26 02:09:22 +01:00
parent 3fddadfe50
commit 50366b9b9c
288 changed files with 58893 additions and 750 deletions

View File

@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stroma File Explorer</title>
{% load static %}
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{% static 'file/editor_dist/assets/editor.css' %}">
<style>
body {
margin: 0;
height: 100vh;
overflow: hidden;
}
.folder-icon::before {
content: '';
display: inline-block;
margin-right: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
transition: transform 0.2s ease;
}
.folder-icon.open::before {
transform: rotate(90deg);
}
.tree-item {
cursor: pointer;
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.15s;
}
.tree-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.tree-item.selected {
background-color: rgba(59, 130, 246, 0.1);
}
.tree-children {
padding-left: 1rem;
display: none;
}
.tree-children.open {
display: block;
}
/* Editor Styles */
.ProseMirror {
outline: none;
padding: 1rem 2rem;
min-height: 100%;
}
.ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
/* Bubble Menu Styles */
.bubble-menu {
background-color: #fff;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
padding: 0.25rem;
gap: 0.25rem;
}
.bubble-menu button {
border: none;
background: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
color: #4a5568;
}
.bubble-menu button:hover,
.bubble-menu button.is-active {
background-color: #edf2f7;
color: #2d3748;
}
</style>
</head>
<body class="bg-gray-50">
<div class="flex h-full">
<!-- Sidebar -->
<div class="w-64 bg-white border-r border-gray-200 overflow-y-auto">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">Explorer</h2>
<a href="/upload/" class="text-blue-500 text-xs hover:underline">Upload</a>
</div>
<div class="p-2" id="file-tree">
<div class="p-4 text-gray-400 text-sm">Loading structure...</div>
</div>
</div>
<!-- Content Area -->
<div class="flex-1 flex flex-col bg-white">
<!-- Empty State -->
<div id="empty-state" class="flex flex-col items-center justify-center h-full text-gray-400">
<svg class="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p>Select a file to view or edit</p>
</div>
<!-- Markdown Editor -->
<div id="markdown-container" class="flex-1 flex-col hidden">
<!-- Header -->
<div class="bg-white border-b border-gray-200 px-4 py-3 flex justify-between items-center shrink-0 z-10">
<h3 id="markdown-title" class="text-sm font-medium text-gray-700 truncate mr-4"></h3>
<button id="save-btn"
class="bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold py-1.5 px-4 rounded transition">
Save Changes
</button>
</div>
<!-- Bubble Menu -->
<div id="bubble-menu" class="bubble-menu hidden">
<button id="btn-bold" class="font-bold">B</button>
<button id="btn-italic" class="italic">i</button>
<button id="btn-strike" class="line-through">S</button>
<button id="btn-code" class="font-mono">&lt;&gt;</button>
</div>
<!-- Editor Container -->
<div class="flex-1 overflow-auto relative cursor-text" id="editor-container">
<div id="editor" class="prose prose-sm sm:prose lg:prose-lg max-w-none h-full"></div>
</div>
</div>
<!-- PDF Viewer -->
<div id="pdf-container" class="flex-1 hidden">
<iframe id="pdf-iframe" class="w-full h-full border-none"></iframe>
</div>
</div>
</div>
<script>
const fileTree = document.getElementById('file-tree');
const emptyState = document.getElementById('empty-state');
const markdownContainer = document.getElementById('markdown-container');
const pdfContainer = document.getElementById('pdf-container');
const pdfIframe = document.getElementById('pdf-iframe');
const markdownTitle = document.getElementById('markdown-title');
let selectedFileId = null;
let selectedElement = null;
let currentFileType = null;
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;
}
async function loadTree() {
try {
const response = await fetch('{% url "file:tree_api" %}');
const structure = await response.json();
fileTree.innerHTML = '';
structure.forEach(item => createTreeItem(item, fileTree));
} catch (err) {
fileTree.innerHTML = '<div class="p-4 text-red-500 text-sm">Failed to load structure</div>';
}
}
function createTreeItem(item, parentElement) {
const itemDiv = document.createElement('div');
if (item.type === 'folder') {
const folderDiv = document.createElement('div');
folderDiv.className = 'tree-item folder-icon flex items-center';
folderDiv.innerHTML = `<span class="truncate">${item.name}</span>`;
const childrenDiv = document.createElement('div');
childrenDiv.className = 'tree-children';
folderDiv.addEventListener('click', (e) => {
e.stopPropagation();
folderDiv.classList.toggle('open');
childrenDiv.classList.toggle('open');
});
itemDiv.appendChild(folderDiv);
itemDiv.appendChild(childrenDiv);
item.children.forEach(child => createTreeItem(child, childrenDiv));
} else {
const fileDiv = document.createElement('div');
fileDiv.className = 'tree-item truncate';
fileDiv.textContent = item.name;
fileDiv.dataset.fileId = item.id;
fileDiv.dataset.filePath = item.path;
fileDiv.dataset.fileName = item.name;
fileDiv.dataset.mimeType = item.mime_type;
fileDiv.addEventListener('click', async (e) => {
e.stopPropagation();
if (selectedElement) selectedElement.classList.remove('selected');
fileDiv.classList.add('selected');
selectedElement = fileDiv;
loadFile(item.id, item.name, item.mime_type, item.path);
});
itemDiv.appendChild(fileDiv);
}
parentElement.appendChild(itemDiv);
}
async function loadFile(id, name, mimeType, path) {
selectedFileId = id;
// Update URL hash with the file path, replacing spaces with underscores
const hashPath = path.replace(/ /g, '_');
window.location.hash = `#${hashPath}`;
// Hide all containers
emptyState.classList.add('hidden');
markdownContainer.classList.add('hidden');
pdfContainer.classList.add('hidden');
if (mimeType === 'application/pdf') {
// Show PDF viewer
currentFileType = 'pdf';
pdfIframe.src = `{% url "file:pdf_viewer" 0 %}`.replace('0', id);
pdfContainer.classList.remove('hidden');
} else {
// Show markdown editor and load content
currentFileType = 'markdown';
markdownTitle.textContent = name;
markdownContainer.classList.remove('hidden');
// Set current file ID for editor
if (window.setEditorFileId) {
window.setEditorFileId(id);
}
// Load content via API
try {
const response = await fetch(`{% url "file:get_content" 0 %}`.replace('0', id));
const data = await response.json();
// Update editor content if editor is initialized
if (window.editorInstance) {
window.editorInstance.commands.setContent(data.content || '');
}
} catch (err) {
console.error('Failed to load file content:', err);
}
}
}
function loadFileFromHash() {
const hash = window.location.hash.substring(1); // Remove #
if (hash) {
// Convert underscores back to spaces to match the actual file path
const filePath = hash.replace(/_/g, ' ');
// Find the file in the tree and load it
const fileElement = findFileElement(filePath);
if (fileElement) {
// Expand all parent folders
expandParentFolders(fileElement);
// Click the file to load it
fileElement.click();
}
}
}
function findFileElement(filePath) {
// Search through all file tree items
const allItems = fileTree.querySelectorAll('.tree-item');
for (let item of allItems) {
// Skip folder items
if (item.classList.contains('folder-icon')) continue;
// Check if this item has the matching file path
if (item.dataset.filePath === filePath) {
return item;
}
}
return null;
}
function expandParentFolders(element) {
let parent = element.parentElement;
while (parent && parent !== fileTree) {
if (parent.classList.contains('tree-children')) {
parent.classList.add('open');
// Find the folder div and add open class to it
const folderDiv = parent.previousElementSibling;
if (folderDiv && folderDiv.classList.contains('folder-icon')) {
folderDiv.classList.add('open');
}
}
parent = parent.parentElement;
}
}
// Initialize
loadTree().then(() => {
// After tree is loaded, check for hash and load file if present
loadFileFromHash();
});
// Listen for hash changes (e.g., browser back/forward)
window.addEventListener('hashchange', loadFileFromHash);
</script>
<!-- Load editor module after DOM is ready -->
<script type="module" src="{% static 'file/editor_dist/assets/editor.js' %}"></script>
</body>
</html>

View File

@@ -0,0 +1,92 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Editor</title>
<link rel="stylesheet" href="{% static 'file/editor_dist/assets/editor.css' %}">
<style>
body {
margin: 0;
height: 100vh;
overflow: auto;
background-color: #ffffff;
}
/* Editor Styles */
.ProseMirror {
outline: none;
padding: 1rem 2rem;
min-height: 100%;
}
.ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
/* Basic Menu Styles (will be enhanced) */
.bubble-menu {
background-color: #fff;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
padding: 0.25rem;
gap: 0.25rem;
}
.bubble-menu button {
border: none;
background: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
color: #4a5568;
}
.bubble-menu button:hover,
.bubble-menu button.is-active {
background-color: #edf2f7;
color: #2d3748;
}
</style>
</head>
<body class="flex flex-col h-full">
<!-- Configuration -->
<div id="editor-config" data-get-url="{{ get_content_url }}" data-save-url="{{ save_content_url }}" class="hidden">
</div>
<!-- Header -->
<div class="bg-white border-b border-gray-200 px-4 py-3 flex justify-between items-center shrink-0 z-10">
<h3 class="text-sm font-medium text-gray-700 truncate mr-4">{{ file_name }}</h3>
<button id="save-btn"
class="bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold py-1.5 px-4 rounded transition">
Save Changes
</button>
</div>
<!-- Bubble Menu -->
<div id="bubble-menu" class="bubble-menu hidden">
<button id="btn-bold" class="font-bold">B</button>
<button id="btn-italic" class="italic">i</button>
<button id="btn-strike" class="line-through">S</button>
<button id="btn-code" class="font-mono">&lt;&gt;</button>
</div>
<!-- Editor Container -->
<div class="flex-1 overflow-auto relative cursor-text" id="editor-container">
<div id="editor" class="prose prose-sm sm:prose lg:prose-lg max-w-none h-full"></div>
</div>
<!-- Modules -->
<script type="module" src="{% static 'file/editor_dist/assets/editor.js' %}"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PDF Viewer - {{ file_name }}</title>
<style>
body,
html {
margin: 0;
padding: 0;
height: 100%;
overflow: auto;
background-color: #525659;
}
</style>
</head>
<body>
<embed src="{{ pdf_url }}" type="application/pdf" width="100%" height="100%" title="Embedded PDF Viewer" />
</body>
</html>

View File

@@ -0,0 +1,455 @@
<!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('/file/upload/api/', {
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>