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:
+165
-12
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user