DeepSeek-harness pass: CVE audit (clean), clipboard paste upload, floating upload CTA, master-designer visual overhaul (glow CTA, staggered entrance, highlight-new, focus states) + full sync (README, lockfile, REPORT)

This commit is contained in:
krisf
2026-08-22 16:25:48 -04:00
parent 9cdaa3f176
commit 513ae4827d
5 changed files with 607 additions and 158 deletions
+165 -12
View File
@@ -8,6 +8,7 @@ let selectedFile = null;
document.addEventListener('DOMContentLoaded', () => {
loadPosts();
setupDragDrop();
setupPasteUpload();
});
// ===== API Helpers =====
@@ -64,8 +65,8 @@ function renderGallery() {
return;
}
gallery.innerHTML = currentPosts.map(post => `
<article class="post-card" onclick="openPostModal('${post.id}')">
gallery.innerHTML = currentPosts.map((post, index) => `
<article class="post-card" style="animation-delay:${Math.min(index * 40, 400)}ms" onclick="openPostModal('${post.id}')">
<img class="post-card-image" src="${post.url}" alt="${escapeHtml(post.title)}" loading="lazy" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22300%22><rect fill=%22%23252540%22 width=%22400%22 height=%22300%22/><text fill=%22%236c6c85%22 x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 font-size=%2216%22>Image not available</text></svg>'">
<div class="post-card-body">
<h3 class="post-card-title">${escapeHtml(post.title)}</h3>
@@ -142,7 +143,8 @@ function openUploadModal() {
document.getElementById('uploadModal').classList.add('active');
selectedFile = null;
document.getElementById('uploadPreview').style.display = 'none';
document.querySelector('.upload-prompt').style.display = '';
document.getElementById('uploadPrompt').style.display = '';
document.getElementById('uploadProcessing').style.display = 'none';
document.getElementById('postTitle').value = '';
document.getElementById('postTags').value = '';
document.getElementById('postAuthor').value = '';
@@ -157,19 +159,27 @@ function setupDragDrop() {
const zone = document.getElementById('uploadZone');
const input = document.getElementById('fileInput');
zone.addEventListener('click', () => input.click());
zone.addEventListener('click', (e) => {
// Don't trigger file picker if clicking the remove button
if (e.target.closest('.btn-remove')) return;
input.click();
});
zone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
zone.classList.add('dragover');
});
zone.addEventListener('dragleave', () => {
zone.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
zone.classList.remove('dragover');
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
zone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
@@ -202,7 +212,7 @@ function handleFile(file) {
reader.onload = (e) => {
document.getElementById('previewImage').src = e.target.result;
document.getElementById('uploadPreview').style.display = 'block';
document.querySelector('.upload-prompt').style.display = 'none';
document.getElementById('uploadPrompt').style.display = 'none';
};
reader.readAsDataURL(file);
}
@@ -210,7 +220,7 @@ function handleFile(file) {
function removePreview() {
selectedFile = null;
document.getElementById('uploadPreview').style.display = 'none';
document.querySelector('.upload-prompt').style.display = '';
document.getElementById('uploadPrompt').style.display = '';
document.getElementById('fileInput').value = '';
}
@@ -228,6 +238,14 @@ async function submitPost() {
btn.disabled = true;
btn.textContent = 'Uploading...';
// Show processing state in upload zone
const processing = document.getElementById('uploadProcessing');
const prompt = document.getElementById('uploadPrompt');
const preview = document.getElementById('uploadPreview');
processing.style.display = 'block';
prompt.style.display = 'none';
preview.style.display = 'none';
try {
if (selectedFile) {
const formData = new FormData();
@@ -236,21 +254,151 @@ async function submitPost() {
formData.append('tags', tags);
formData.append('author', author);
await apiPost('/api/upload', formData);
const newPost = await apiPost('/api/upload', formData);
showToast('Post uploaded successfully! 🎉', 'success');
}
closeUploadModal();
await loadPosts();
closeUploadModal();
await loadPosts();
// Scroll to and highlight the new post
highlightNewPost(newPost.id);
}
} catch (err) {
showError(err.message);
showToast(err.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Upload Post';
processing.style.display = 'none';
if (selectedFile) {
preview.style.display = 'block';
} else {
prompt.style.display = '';
}
}
}
// ===== Paste Upload =====
function setupPasteUpload() {
document.addEventListener('paste', (e) => {
// Ignore if a modal is open and user is typing in an input
const activeModal = document.querySelector('.modal-overlay.active');
if (activeModal) {
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
return;
}
}
const items = e.clipboardData && e.clipboardData.items;
if (!items) return;
let imageBlob = null;
let imageType = '';
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
imageBlob = items[i].getAsFile();
imageType = items[i].type;
break;
}
}
if (!imageBlob) return;
// Validate type
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowed.includes(imageType)) {
showToast(`Unsupported paste type: ${imageType}`, 'error');
return;
}
// Validate size
if (imageBlob.size > 20 * 1024 * 1024) {
showToast('Pasted image too large. Maximum size is 20MB.', 'error');
return;
}
e.preventDefault();
handlePasteUpload(imageBlob);
});
}
async function handlePasteUpload(blob) {
// Create a File from the blob with a generated name
const ext = blob.type.split('/')[1] || 'png';
const fileName = `pasted-${Date.now()}.${ext}`;
const file = new File([blob], fileName, { type: blob.type });
// Open the upload modal and set the file
openUploadModal();
selectedFile = file;
hideError();
// Show preview
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('previewImage').src = e.target.result;
document.getElementById('uploadPreview').style.display = 'block';
document.getElementById('uploadPrompt').style.display = 'none';
};
reader.readAsDataURL(file);
// Auto-submit after a brief delay so user sees the preview
setTimeout(async () => {
const btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.textContent = 'Uploading...';
const processing = document.getElementById('uploadProcessing');
const prompt = document.getElementById('uploadPrompt');
const preview = document.getElementById('uploadPreview');
processing.style.display = 'block';
prompt.style.display = 'none';
preview.style.display = 'none';
try {
const formData = new FormData();
formData.append('image', file);
formData.append('title', 'Untitled');
formData.append('tags', '');
formData.append('author', 'Anonymous');
const newPost = await apiPost('/api/upload', formData);
showToast('Image pasted & uploaded! 🎉', 'success');
closeUploadModal();
await loadPosts();
highlightNewPost(newPost.id);
} catch (err) {
showToast(`Paste upload failed: ${err.message}`, 'error');
// Re-show preview so user can retry
processing.style.display = 'none';
preview.style.display = 'block';
} finally {
btn.disabled = false;
btn.textContent = 'Upload Post';
processing.style.display = 'none';
}
}, 600);
}
// ===== Highlight New Post =====
function highlightNewPost(postId) {
// Wait a tick for the DOM to update
requestAnimationFrame(() => {
const cards = document.querySelectorAll('.post-card');
for (const card of cards) {
if (card.getAttribute('onclick') && card.getAttribute('onclick').includes(postId)) {
card.classList.add('highlight-new');
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => card.classList.remove('highlight-new'), 2500);
break;
}
}
});
}
// ===== Post Detail Modal =====
async function openPostModal(postId) {
currentPostId = postId;
@@ -416,7 +564,12 @@ function showToast(message, type = 'success') {
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
setTimeout(() => {
toast.style.transition = 'opacity 200ms ease, transform 200ms ease';
toast.style.opacity = '0';
toast.style.transform = 'translateX(20px)';
setTimeout(() => toast.remove(), 200);
}, 3000);
}
function showError(msg) {