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
+369 -137
View File
File diff suppressed because it is too large Load Diff
+22 -9
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imgur Clone - Share Your World</title>
<link rel="stylesheet" href="/css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<!-- Header -->
@@ -17,7 +17,7 @@
</div>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Search images..." oninput="filterPosts()">
<button onclick="filterPosts()">🔍</button>
<button onclick="filterPosts()" aria-label="Search">🔍</button>
</div>
<button class="btn-new-post" onclick="openUploadModal()">+ New Post</button>
</div>
@@ -45,25 +45,38 @@
</div>
</main>
<!-- Floating Action Button -->
<button class="fab" onclick="openUploadModal()" aria-label="Upload image" title="Upload image (or paste with Ctrl+V)">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<!-- Upload Modal -->
<div class="modal-overlay" id="uploadModal">
<div class="modal">
<div class="modal-header">
<h2>Create New Post</h2>
<button class="modal-close" onclick="closeUploadModal()"></button>
<button class="modal-close" onclick="closeUploadModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="upload-zone" id="uploadZone">
<div class="upload-prompt">
<div class="upload-prompt" id="uploadPrompt">
<div class="upload-icon">📁</div>
<p>Drag & drop an image here</p>
<p class="upload-main-text">Drag &amp; drop an image here</p>
<p class="upload-hint">or click to browse</p>
<p class="upload-paste-hint">or paste an image with <kbd>Ctrl</kbd>+<kbd>V</kbd></p>
<p class="upload-formats">JPEG, PNG, GIF, WebP (max 20MB)</p>
</div>
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp" hidden>
<div class="upload-preview" id="uploadPreview" style="display:none;">
<img id="previewImage" src="" alt="Preview">
<button class="btn-remove" onclick="removePreview()"> Remove</button>
<button class="btn-remove" onclick="removePreview()" aria-label="Remove image"></button>
</div>
<div class="upload-processing" id="uploadProcessing" style="display:none;">
<div class="spinner spinner-sm"></div>
<p>Optimizing for 4K...</p>
</div>
</div>
<div class="form-group">
@@ -89,7 +102,7 @@
<div class="modal modal-large">
<div class="modal-header">
<h2 id="postModalTitle"></h2>
<button class="modal-close" onclick="closePostModal()"></button>
<button class="modal-close" onclick="closePostModal()" aria-label="Close"></button>
</div>
<div class="modal-body post-detail">
<div class="post-detail-grid">
@@ -98,9 +111,9 @@
</div>
<div class="post-sidebar">
<div class="vote-box">
<button class="vote-btn upvote" onclick="votePost('up')"></button>
<button class="vote-btn upvote" onclick="votePost('up')" aria-label="Upvote"></button>
<span class="vote-count" id="postModalVotes">0</span>
<button class="vote-btn downvote" onclick="votePost('down')"></button>
<button class="vote-btn downvote" onclick="votePost('down')" aria-label="Downvote"></button>
</div>
<div class="post-meta">
<span class="meta-item">👁️ <span id="postModalViews">0</span> views</span>
+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) {