diff --git a/README.md b/README.md index e9abb2a..f734478 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# imgur-clone +# Picta -Imgur-style image sharing app: dark-mode masonry gallery, drag-and-drop / click / **clipboard paste** uploads, 4K re-encoding, voting, and threaded comments. +Image sharing app: dark-mode masonry gallery, drag-and-drop / click / **clipboard paste** uploads, 4K re-encoding, voting, and threaded comments. ## Stack - **Backend**: Node 20 (Alpine), Express, multer (multipart), sharp (image pipeline), cors @@ -40,6 +40,8 @@ docker build -t imgur-clone:latest . docker run -d --name imgur-clone --restart unless-stopped -p 3001:3000 imgur-clone:latest ``` +> Note: The Docker image and container names remain `imgur-clone` for deployment compatibility. + ## Security - Runtime image strips npm/corepack (only `node server.js` runs at runtime) - `apk upgrade` on every build for base-image CVE coverage diff --git a/REPORT.md b/REPORT.md index 10076c4..1102653 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,6 +1,6 @@ # MCP Deployment & Test Summary Report -## Imgur Clone — Pipeline Validation Project +## Picta — Pipeline Validation Project --- @@ -13,7 +13,7 @@ | **Container Port** | `3001:3000` (host:container) | | **Backend Port** | `3000` (internal) | | **Caddy Reverse Proxy** | `192.168.0.123:2080/2443` → `192.168.0.123:3001` | -| **Gitea Repo** | `https://gitea.krisforbes.ca/krisf/imgur-clone` | +| **Gitea Repo** | `https://gitea.krisforbes.ca/krisf/imgur-clone` (Picta) | | **Build Engine** | Docker BuildKit (node:20-alpine) | | **Build Time** | ~2s (cached layers) | | **Container Status** | `running` (healthy) | @@ -146,7 +146,7 @@ User → Cloudflare DNS → dyn.krisforbes.ca → Caddy (2443) → imgur-clone:3 ### Files in Gitea Repository ``` -imgur-clone/ +imgur-clone/ (Picta) ├── .gitignore ├── Dockerfile ├── README.md diff --git a/public/css/style.css b/public/css/style.css index 034edcf..d398a95 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -62,6 +62,10 @@ --text-3xl: 24px; } +html, body { + overflow-x: hidden; +} + body { font-family: var(--font-family); background: var(--bg-primary); @@ -73,6 +77,19 @@ body { -moz-osx-font-smoothing: grayscale; } +body.modal-open { + overflow: hidden; +} + +/* Hide scrollbar tracks but keep scrollability */ +* { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ +} +*::-webkit-scrollbar { + display: none; /* WebKit */ +} + /* ===== Focus Visible ===== */ :focus-visible { outline: 2px solid var(--accent-green); @@ -116,8 +133,11 @@ body { opacity: 0.85; } -.logo-icon { - font-size: 28px; +.logo-img { + width: 28px; + height: 28px; + border-radius: 6px; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); } .logo-text { @@ -304,6 +324,8 @@ body { .post-card-image { width: 100%; + max-width: 100%; + height: auto; aspect-ratio: 4/3; object-fit: cover; display: block; @@ -324,6 +346,8 @@ body { -webkit-box-orient: vertical; overflow: hidden; letter-spacing: -0.01em; + overflow-wrap: break-word; + word-break: break-word; } .post-card-stats { @@ -410,9 +434,14 @@ body { max-width: 560px; max-height: 90vh; overflow-y: auto; + overflow-x: hidden; border: 1px solid var(--border-color); box-shadow: var(--shadow-lg); animation: modalIn 200ms ease; + scrollbar-width: none; +} +.modal::-webkit-scrollbar { + display: none; } @keyframes modalIn { @@ -652,6 +681,9 @@ body { .post-image-container img { width: 100%; + max-width: 100%; + height: auto; + display: block; border-radius: var(--radius); cursor: zoom-in; box-shadow: var(--shadow); @@ -815,6 +847,8 @@ body { font-size: var(--text-base); color: var(--text-secondary); line-height: 1.5; + overflow-wrap: break-word; + word-break: break-word; } .comment-time { @@ -922,6 +956,43 @@ body { } } +/* ===== Full-Page Drag Overlay ===== */ +.drag-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(20, 20, 28, 0.92); + backdrop-filter: blur(6px); + z-index: 5000; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.drag-overlay.active { + display: flex; +} + +.drag-overlay-inner { + text-align: center; + padding: var(--space-10) var(--space-9); + border: 3px dashed var(--accent-green); + border-radius: var(--radius-xl); + background: rgba(27, 183, 110, 0.06); +} + +.drag-overlay-icon { + font-size: 56px; + margin-bottom: var(--space-4); +} + +.drag-overlay-text { + font-size: var(--text-2xl); + font-weight: 700; + color: var(--accent-green); + letter-spacing: -0.02em; +} + /* ===== Responsive ===== */ @media (max-width: 1024px) { .post-detail-grid { diff --git a/public/index.html b/public/index.html index 0665632..30438ec 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - Imgur Clone - Share Your World + Picta — Share Your World @@ -12,8 +12,8 @@
+ +
+
+
📥
+

Drop to share

+
+
+
diff --git a/public/js/app.js b/public/js/app.js index 2d15e88..1109959 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -3,11 +3,13 @@ let currentSort = 'new'; let currentPosts = []; let currentPostId = null; let selectedFile = null; +let dragCounter = 0; // ===== Initialize ===== document.addEventListener('DOMContentLoaded', () => { loadPosts(); setupDragDrop(); + setupPageDrag(); setupPasteUpload(); }); @@ -141,6 +143,7 @@ function filterPosts() { // ===== Upload Modal ===== function openUploadModal() { document.getElementById('uploadModal').classList.add('active'); + document.body.classList.add('modal-open'); selectedFile = null; document.getElementById('uploadPreview').style.display = 'none'; document.getElementById('uploadPrompt').style.display = ''; @@ -153,6 +156,7 @@ function openUploadModal() { function closeUploadModal() { document.getElementById('uploadModal').classList.remove('active'); + document.body.classList.remove('modal-open'); } function setupDragDrop() { @@ -191,6 +195,43 @@ function setupDragDrop() { }); } +// ===== Full-Page Drag Overlay ===== +function setupPageDrag() { + const overlay = document.getElementById('dragOverlay'); + + document.addEventListener('dragenter', (e) => { + e.preventDefault(); + dragCounter++; + if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { + overlay.classList.add('active'); + } + }); + + document.addEventListener('dragleave', (e) => { + e.preventDefault(); + dragCounter--; + if (dragCounter <= 0) { + dragCounter = 0; + overlay.classList.remove('active'); + } + }); + + document.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + document.addEventListener('drop', (e) => { + e.preventDefault(); + dragCounter = 0; + overlay.classList.remove('active'); + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + openUploadModal(); + handleFile(file); + } + }); +} + function handleFile(file) { // Validate const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; @@ -213,6 +254,8 @@ function handleFile(file) { document.getElementById('previewImage').src = e.target.result; document.getElementById('uploadPreview').style.display = 'block'; document.getElementById('uploadPrompt').style.display = 'none'; + // Auto-focus the Share button so user just presses Enter/clicks + document.getElementById('submitBtn').focus(); }; reader.readAsDataURL(file); } @@ -341,6 +384,8 @@ async function handlePasteUpload(blob) { document.getElementById('previewImage').src = e.target.result; document.getElementById('uploadPreview').style.display = 'block'; document.getElementById('uploadPrompt').style.display = 'none'; + // Auto-focus the Share button + document.getElementById('submitBtn').focus(); }; reader.readAsDataURL(file); diff --git a/seed/data.js b/seed/data.js index e9c3186..0b9ab48 100644 --- a/seed/data.js +++ b/seed/data.js @@ -1,4 +1,4 @@ -// Seed data for Imgur Clone - 12 sample posts with placeholder images +// Seed data for Picta — 12 sample posts with placeholder images // Using picsum.photos for realistic placeholder images const posts = [ diff --git a/server.js b/server.js index 51f5af9..36b4145 100644 --- a/server.js +++ b/server.js @@ -268,7 +268,7 @@ app.get('/api/health', (req, res) => { // Initialize and start loadSeedData(); -console.log(`🚀 Imgur Clone running on port ${PORT}`); +console.log(`🚀 Picta running on port ${PORT}`); console.log(`📦 ${posts.length} seed posts loaded`); const server = app.listen(PORT, () => {