Add TypeScript migration, image resizing, media upload UX, and multimedia support
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s
- Migrate static JS to TypeScript (static-ts/ → compiled to internal/api/static/) - Add image resizing on upload: JPEG/PNG/WebP scaled to max 1920px at quality 80 - Extract shared upload logic into upload.go (saveUpload, saveResizedImage, saveResizedWebP) - Add POST /media endpoint for drag-drop/paste media uploads with markdown ref return - Add background music player with video/audio coordination (autoplay.ts) - Add global nav, public feed, hashtags, visibility, Markdown rendering for entries - Add Dockerfile stage for TypeScript compilation (static-ts-builder) - Add goldmark, disintegration/imaging, golang.org/x/image dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
81
backend/static-ts/editor.ts
Normal file
81
backend/static-ts/editor.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
interface UploadResponse {
|
||||
filename: string;
|
||||
mime: string;
|
||||
ref: string;
|
||||
}
|
||||
|
||||
function initEditor(ta: HTMLTextAreaElement): void {
|
||||
async function upload(file: File): Promise<void> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const statusEl = ta.parentElement?.querySelector<HTMLElement>('.upload-status');
|
||||
if (statusEl) statusEl.textContent = '↑ ' + file.name + ' …';
|
||||
try {
|
||||
const res = await fetch('/media', { method: 'POST', body: form });
|
||||
if (!res.ok) {
|
||||
if (statusEl) statusEl.textContent = '✗ Fehler beim Hochladen';
|
||||
return;
|
||||
}
|
||||
const data: UploadResponse = await res.json();
|
||||
insertAtCursor('\n' + data.ref + '\n');
|
||||
if (statusEl) statusEl.textContent = '';
|
||||
} catch (_e) {
|
||||
if (statusEl) statusEl.textContent = '✗ Fehler beim Hochladen';
|
||||
}
|
||||
}
|
||||
|
||||
function insertAtCursor(text: string): void {
|
||||
const start = ta.selectionStart;
|
||||
ta.value = ta.value.slice(0, start) + text + ta.value.slice(ta.selectionEnd);
|
||||
ta.selectionStart = ta.selectionEnd = start + text.length;
|
||||
ta.focus();
|
||||
}
|
||||
|
||||
// Paste: catch file pastes
|
||||
ta.addEventListener('paste', function (e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === 'file') {
|
||||
e.preventDefault();
|
||||
const file = items[i].getAsFile();
|
||||
if (file) void upload(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Drag & Drop onto textarea
|
||||
ta.addEventListener('dragover', function (e: DragEvent) {
|
||||
e.preventDefault();
|
||||
ta.classList.add('drag-over');
|
||||
});
|
||||
ta.addEventListener('dragleave', function () {
|
||||
ta.classList.remove('drag-over');
|
||||
});
|
||||
ta.addEventListener('drop', function (e: DragEvent) {
|
||||
e.preventDefault();
|
||||
ta.classList.remove('drag-over');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (!files) return;
|
||||
for (let i = 0; i < files.length; i++) void upload(files[i]);
|
||||
});
|
||||
|
||||
// File picker button
|
||||
const picker = ta.parentElement?.querySelector<HTMLButtonElement>('.media-picker');
|
||||
const input = ta.parentElement?.querySelector<HTMLInputElement>('.media-file-input');
|
||||
if (picker && input) {
|
||||
picker.addEventListener('click', function () { input.click(); });
|
||||
input.addEventListener('change', function () {
|
||||
if (!input.files) return;
|
||||
Array.from(input.files).forEach(f => void upload(f));
|
||||
input.value = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll<HTMLTextAreaElement>('textarea[name="description"]').forEach(initEditor);
|
||||
})();
|
||||
Reference in New Issue
Block a user