baseline: pre-deepseek-harness state

This commit is contained in:
krisf
2026-08-22 16:09:22 -04:00
commit 9cdaa3f176
11 changed files with 3470 additions and 0 deletions
+772
View File
@@ -0,0 +1,772 @@
/* ===== Reset & Base ===== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-primary: #14141c;
--bg-secondary: #1b1b2f;
--bg-tertiary: #252540;
--accent-green: #1bb76e;
--accent-green-hover: #15995d;
--accent-red: #e74c3c;
--accent-red-hover: #c0392b;
--text-primary: #ffffff;
--text-secondary: #a0a0b8;
--text-muted: #6c6c85;
--border-color: #2a2a45;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
--radius: 8px;
--radius-lg: 12px;
--transition: 0.2s ease;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
/* ===== Header ===== */
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow);
}
.header-inner {
max-width: 1400px;
margin: 0 auto;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 24px;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
flex-shrink: 0;
}
.logo-icon {
font-size: 28px;
}
.logo-text {
font-size: 20px;
font-weight: 700;
color: var(--accent-green);
}
.search-bar {
flex: 1;
display: flex;
max-width: 500px;
}
.search-bar input {
flex: 1;
padding: 10px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-right: none;
border-radius: var(--radius) 0 0 var(--radius);
color: var(--text-primary);
font-size: 14px;
outline: none;
transition: border-color var(--transition);
}
.search-bar input:focus {
border-color: var(--accent-green);
}
.search-bar input::placeholder {
color: var(--text-muted);
}
.search-bar button {
padding: 10px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 0 var(--radius) var(--radius) 0;
cursor: pointer;
font-size: 14px;
}
.btn-new-post {
padding: 10px 20px;
background: var(--accent-green);
color: white;
border: none;
border-radius: var(--radius);
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background var(--transition);
flex-shrink: 0;
}
.btn-new-post:hover {
background: var(--accent-green-hover);
}
/* ===== Filter Bar ===== */
.filter-bar {
max-width: 1400px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.filter-tabs {
display: flex;
gap: 8px;
}
.filter-tab {
padding: 8px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-secondary);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all var(--transition);
}
.filter-tab:hover {
border-color: var(--accent-green);
color: var(--text-primary);
}
.filter-tab.active {
background: var(--accent-green);
border-color: var(--accent-green);
color: white;
}
.post-count {
color: var(--text-muted);
font-size: 13px;
}
/* ===== Gallery Grid ===== */
.gallery-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 24px 48px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
/* ===== Post Card ===== */
.post-card {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
overflow: hidden;
cursor: pointer;
transition: transform var(--transition), box-shadow var(--transition);
border: 1px solid var(--border-color);
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.post-card-image {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
display: block;
background: var(--bg-tertiary);
}
.post-card-body {
padding: 12px 16px;
}
.post-card-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-card-stats {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--text-secondary);
}
.post-card-stats .upvotes {
color: var(--accent-green);
font-weight: 600;
}
.post-card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.tag-pill {
padding: 2px 8px;
background: var(--bg-tertiary);
border-radius: 12px;
font-size: 11px;
color: var(--text-muted);
}
/* ===== Loading ===== */
.loading {
text-align: center;
padding: 48px;
color: var(--text-muted);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ===== Modal ===== */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 24px;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
width: 100%;
max-width: 560px;
max-height: 90vh;
overflow-y: auto;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-lg);
}
.modal-large {
max-width: 960px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-size: 18px;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 18px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all var(--transition);
}
.modal-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
/* ===== Upload Zone ===== */
.upload-zone {
border: 2px dashed var(--border-color);
border-radius: var(--radius-lg);
padding: 48px 24px;
text-align: center;
cursor: pointer;
transition: border-color var(--transition), background var(--transition);
margin-bottom: 24px;
}
.upload-zone:hover,
.upload-zone.dragover {
border-color: var(--accent-green);
background: rgba(27, 183, 110, 0.05);
}
.upload-icon {
font-size: 48px;
margin-bottom: 16px;
}
.upload-prompt p {
color: var(--text-secondary);
margin-bottom: 4px;
}
.upload-hint {
font-size: 13px;
color: var(--text-muted);
}
.upload-formats {
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
}
.upload-preview {
position: relative;
display: inline-block;
}
.upload-preview img {
max-width: 100%;
max-height: 300px;
border-radius: var(--radius);
}
.btn-remove {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 14px;
}
/* ===== Form ===== */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
}
.form-group input {
width: 100%;
padding: 10px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 14px;
outline: none;
transition: border-color var(--transition);
}
.form-group input:focus {
border-color: var(--accent-green);
}
.form-group input::placeholder {
color: var(--text-muted);
}
.btn-submit {
width: 100%;
padding: 12px;
background: var(--accent-green);
color: white;
border: none;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background var(--transition);
}
.btn-submit:hover {
background: var(--accent-green-hover);
}
.btn-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-message {
background: rgba(231, 76, 60, 0.1);
border: 1px solid var(--accent-red);
color: var(--accent-red);
padding: 10px 14px;
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 13px;
}
/* ===== Post Detail ===== */
.post-detail-grid {
display: grid;
grid-template-columns: 1fr 280px;
gap: 24px;
margin-bottom: 24px;
}
.post-image-container img {
width: 100%;
border-radius: var(--radius);
cursor: zoom-in;
}
.vote-box {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.vote-btn {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-secondary);
cursor: pointer;
font-size: 16px;
font-weight: 700;
transition: all var(--transition);
}
.vote-btn:hover {
border-color: var(--accent-green);
color: var(--accent-green);
}
.vote-btn.active-up {
background: var(--accent-green);
border-color: var(--accent-green);
color: white;
}
.vote-btn.active-down {
background: var(--accent-red);
border-color: var(--accent-red);
color: white;
}
.vote-count {
font-size: 20px;
font-weight: 700;
color: var(--accent-green);
}
.post-meta {
display: flex;
gap: 16px;
margin-bottom: 16px;
font-size: 13px;
color: var(--text-secondary);
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
}
.post-tags .tag-pill {
padding: 4px 10px;
font-size: 12px;
}
.post-author {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.post-author strong {
color: var(--accent-green);
}
.post-date {
font-size: 12px;
color: var(--text-muted);
}
/* ===== Comments ===== */
.comments-section {
border-top: 1px solid var(--border-color);
padding-top: 24px;
}
.comments-section h3 {
font-size: 16px;
margin-bottom: 16px;
}
.comment-form {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.comment-form input {
flex: 1;
padding: 10px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 14px;
outline: none;
}
.comment-form input:focus {
border-color: var(--accent-green);
}
.btn-comment {
padding: 10px 20px;
background: var(--accent-green);
color: white;
border: none;
border-radius: var(--radius);
font-weight: 600;
cursor: pointer;
transition: background var(--transition);
}
.btn-comment:hover {
background: var(--accent-green-hover);
}
.comment-item {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.comment-item:last-child {
border-bottom: none;
}
.comment-item.reply {
margin-left: 32px;
border-left: 2px solid var(--border-color);
padding-left: 16px;
}
.comment-author {
font-size: 13px;
font-weight: 600;
color: var(--accent-green);
margin-bottom: 4px;
}
.comment-text {
font-size: 14px;
color: var(--text-secondary);
}
.comment-time {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
/* ===== Fullscreen ===== */
.fullscreen-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 2000;
align-items: center;
justify-content: center;
cursor: zoom-out;
}
.fullscreen-overlay.active {
display: flex;
}
.fullscreen-overlay img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
/* ===== Toast ===== */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 3000;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
padding: 12px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 14px;
box-shadow: var(--shadow-lg);
animation: slideIn 0.3s ease;
}
.toast.success {
border-color: var(--accent-green);
color: var(--accent-green);
}
.toast.error {
border-color: var(--accent-red);
color: var(--accent-red);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* ===== Responsive ===== */
@media (max-width: 1024px) {
.post-detail-grid {
grid-template-columns: 1fr;
}
.post-sidebar {
order: -1;
}
}
@media (max-width: 768px) {
.header-inner {
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px;
}
.search-bar {
order: 3;
max-width: 100%;
width: 100%;
}
.filter-bar {
padding: 12px 16px;
}
.filter-tabs {
overflow-x: auto;
gap: 6px;
}
.filter-tab {
padding: 6px 12px;
font-size: 12px;
white-space: nowrap;
}
.gallery-container {
padding: 0 16px 32px;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.modal {
max-width: 100%;
margin: 16px;
}
.modal-large {
max-width: 100%;
}
}
@media (max-width: 375px) {
.gallery-grid {
grid-template-columns: 1fr;
}
.logo-text {
display: none;
}
.btn-new-post {
padding: 8px 14px;
font-size: 13px;
}
}
+136
View File
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-inner">
<div class="logo" onclick="navigateHome()">
<span class="logo-icon">📷</span>
<span class="logo-text">Imgur Clone</span>
</div>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Search images..." oninput="filterPosts()">
<button onclick="filterPosts()">🔍</button>
</div>
<button class="btn-new-post" onclick="openUploadModal()">+ New Post</button>
</div>
</header>
<!-- Filter Bar -->
<div class="filter-bar">
<div class="filter-tabs">
<button class="filter-tab active" data-sort="new" onclick="setSort('new')">🆕 New</button>
<button class="filter-tab" data-sort="viral" onclick="setSort('viral')">🔥 Most Viral</button>
<button class="filter-tab" data-sort="user" onclick="setSort('user')">👤 User Submitted</button>
<button class="filter-tab" data-sort="top" onclick="setSort('top')">📈 Top Today</button>
</div>
<div class="post-count" id="postCount"></div>
</div>
<!-- Main Gallery -->
<main class="gallery-container">
<div class="gallery-grid" id="gallery">
<!-- Posts rendered here -->
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Loading images...</p>
</div>
</main>
<!-- 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>
</div>
<div class="modal-body">
<div class="upload-zone" id="uploadZone">
<div class="upload-prompt">
<div class="upload-icon">📁</div>
<p>Drag & drop an image here</p>
<p class="upload-hint">or click to browse</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>
</div>
</div>
<div class="form-group">
<label for="postTitle">Title</label>
<input type="text" id="postTitle" placeholder="Give your post a title..." maxlength="100">
</div>
<div class="form-group">
<label for="postTags">Tags (comma separated)</label>
<input type="text" id="postTags" placeholder="e.g., nature, sunset, photography">
</div>
<div class="form-group">
<label for="postAuthor">Your Name</label>
<input type="text" id="postAuthor" placeholder="Anonymous" maxlength="30">
</div>
<div id="uploadError" class="error-message" style="display:none;"></div>
<button class="btn-submit" id="submitBtn" onclick="submitPost()">Upload Post</button>
</div>
</div>
</div>
<!-- Post Detail Modal -->
<div class="modal-overlay" id="postModal">
<div class="modal modal-large">
<div class="modal-header">
<h2 id="postModalTitle"></h2>
<button class="modal-close" onclick="closePostModal()"></button>
</div>
<div class="modal-body post-detail">
<div class="post-detail-grid">
<div class="post-image-container">
<img id="postModalImage" src="" alt="" onclick="openFullscreen()">
</div>
<div class="post-sidebar">
<div class="vote-box">
<button class="vote-btn upvote" onclick="votePost('up')"></button>
<span class="vote-count" id="postModalVotes">0</span>
<button class="vote-btn downvote" onclick="votePost('down')"></button>
</div>
<div class="post-meta">
<span class="meta-item">👁️ <span id="postModalViews">0</span> views</span>
<span class="meta-item">💬 <span id="postModalComments">0</span> comments</span>
</div>
<div class="post-tags" id="postModalTags"></div>
<div class="post-author" id="postModalAuthor"></div>
<div class="post-date" id="postModalDate"></div>
</div>
</div>
<div class="comments-section">
<h3>Comments</h3>
<div class="comment-form">
<input type="text" id="commentInput" placeholder="Add a comment..." maxlength="500">
<button class="btn-comment" onclick="postComment()">Post</button>
</div>
<div class="comments-list" id="commentsList"></div>
</div>
</div>
</div>
</div>
<!-- Fullscreen Image -->
<div class="fullscreen-overlay" id="fullscreenOverlay" onclick="closeFullscreen()">
<img id="fullscreenImage" src="" alt="">
</div>
<!-- Toast Notifications -->
<div class="toast-container" id="toastContainer"></div>
<script src="/js/app.js"></script>
</body>
</html>
+448
View File
@@ -0,0 +1,448 @@
// ===== State =====
let currentSort = 'new';
let currentPosts = [];
let currentPostId = null;
let selectedFile = null;
// ===== Initialize =====
document.addEventListener('DOMContentLoaded', () => {
loadPosts();
setupDragDrop();
});
// ===== API Helpers =====
async function apiGet(endpoint) {
const res = await fetch(endpoint);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function apiPost(endpoint, formData = null, body = null) {
let res;
if (formData) {
res = await fetch(endpoint, {
method: 'POST',
body: formData
});
} else {
res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
// ===== Posts =====
async function loadPosts() {
const loading = document.getElementById('loading');
loading.style.display = 'block';
try {
currentPosts = await apiGet(`/api/posts?sort=${currentSort}`);
renderGallery();
} catch (err) {
showToast('Failed to load posts', 'error');
} finally {
loading.style.display = 'none';
}
}
function renderGallery() {
const gallery = document.getElementById('gallery');
const count = document.getElementById('postCount');
count.textContent = `${currentPosts.length} posts`;
if (currentPosts.length === 0) {
gallery.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:48px;color:var(--text-muted);">No posts found</div>';
return;
}
gallery.innerHTML = currentPosts.map(post => `
<article class="post-card" 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>
<div class="post-card-stats">
<span class="upvotes">▲ ${formatNumber(post.upvotes)}</span>
<span>👁️ ${formatNumber(post.views)}</span>
<span>💬 ${post.commentCount || 0}</span>
</div>
${post.tags && post.tags.length ? `
<div class="post-card-tags">
${post.tags.slice(0, 3).map(t => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
</div>
` : ''}
</div>
</article>
`).join('');
}
// ===== Sorting =====
function setSort(sort) {
currentSort = sort;
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.sort === sort);
});
loadPosts();
}
// ===== Search =====
function filterPosts() {
const query = document.getElementById('searchInput').value.toLowerCase();
if (!query) {
loadPosts();
return;
}
const filtered = currentPosts.filter(p =>
p.title.toLowerCase().includes(query) ||
(p.tags && p.tags.some(t => t.toLowerCase().includes(query)))
);
const gallery = document.getElementById('gallery');
const count = document.getElementById('postCount');
count.textContent = `${filtered.length} posts matching "${query}"`;
if (filtered.length === 0) {
gallery.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:48px;color:var(--text-muted);">No posts match your search</div>';
return;
}
gallery.innerHTML = filtered.map(post => `
<article class="post-card" onclick="openPostModal('${post.id}')">
<img class="post-card-image" src="${post.url}" alt="${escapeHtml(post.title)}" loading="lazy">
<div class="post-card-body">
<h3 class="post-card-title">${escapeHtml(post.title)}</h3>
<div class="post-card-stats">
<span class="upvotes">▲ ${formatNumber(post.upvotes)}</span>
<span>👁️ ${formatNumber(post.views)}</span>
<span>💬 ${post.commentCount || 0}</span>
</div>
${post.tags && post.tags.length ? `
<div class="post-card-tags">
${post.tags.slice(0, 3).map(t => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
</div>
` : ''}
</div>
</article>
`).join('');
}
// ===== Upload Modal =====
function openUploadModal() {
document.getElementById('uploadModal').classList.add('active');
selectedFile = null;
document.getElementById('uploadPreview').style.display = 'none';
document.querySelector('.upload-prompt').style.display = '';
document.getElementById('postTitle').value = '';
document.getElementById('postTags').value = '';
document.getElementById('postAuthor').value = '';
hideError();
}
function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('active');
}
function setupDragDrop() {
const zone = document.getElementById('uploadZone');
const input = document.getElementById('fileInput');
zone.addEventListener('click', () => input.click());
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.classList.add('dragover');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('dragover');
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) handleFile(file);
});
}
function handleFile(file) {
// Validate
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowed.includes(file.type)) {
showError(`Unsupported file type: ${file.type}. Please upload JPEG, PNG, GIF, or WebP.`);
return;
}
if (file.size > 20 * 1024 * 1024) {
showError('File too large. Maximum size is 20MB.');
return;
}
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.querySelector('.upload-prompt').style.display = 'none';
};
reader.readAsDataURL(file);
}
function removePreview() {
selectedFile = null;
document.getElementById('uploadPreview').style.display = 'none';
document.querySelector('.upload-prompt').style.display = '';
document.getElementById('fileInput').value = '';
}
async function submitPost() {
const btn = document.getElementById('submitBtn');
const title = document.getElementById('postTitle').value.trim();
const tags = document.getElementById('postTags').value.trim();
const author = document.getElementById('postAuthor').value.trim() || 'Anonymous';
if (!selectedFile && !title) {
showError('Please upload an image or enter a title.');
return;
}
btn.disabled = true;
btn.textContent = 'Uploading...';
try {
if (selectedFile) {
const formData = new FormData();
formData.append('image', selectedFile);
formData.append('title', title || 'Untitled');
formData.append('tags', tags);
formData.append('author', author);
await apiPost('/api/upload', formData);
showToast('Post uploaded successfully! 🎉', 'success');
}
closeUploadModal();
await loadPosts();
} catch (err) {
showError(err.message);
showToast(err.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Upload Post';
}
}
// ===== Post Detail Modal =====
async function openPostModal(postId) {
currentPostId = postId;
const modal = document.getElementById('postModal');
try {
const post = await apiGet(`/api/posts/${postId}`);
document.getElementById('postModalTitle').textContent = post.title;
document.getElementById('postModalImage').src = post.url;
document.getElementById('postModalImage').alt = post.title;
document.getElementById('postModalVotes').textContent = formatNumber(post.upvotes);
document.getElementById('postModalViews').textContent = formatNumber(post.views);
document.getElementById('postModalComments').textContent = post.commentCount || 0;
document.getElementById('postModalAuthor').innerHTML = `By <strong>${escapeHtml(post.author)}</strong>`;
document.getElementById('postModalDate').textContent = timeAgo(post.createdAt);
// Tags
const tagsEl = document.getElementById('postModalTags');
tagsEl.innerHTML = post.tags && post.tags.length
? post.tags.map(t => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')
: '';
// Load comments
await loadComments(postId);
modal.classList.add('active');
} catch (err) {
showToast('Failed to load post details', 'error');
}
}
function closePostModal() {
document.getElementById('postModal').classList.remove('active');
currentPostId = null;
}
// ===== Voting =====
async function votePost(direction) {
if (!currentPostId) return;
try {
const result = await apiPost('/api/posts/' + currentPostId + '/vote', null, {
direction,
userId: 'session-' + Math.random().toString(36).slice(2, 8)
});
document.getElementById('postModalVotes').textContent = formatNumber(result.upvotes);
document.getElementById('postModalViews').textContent = formatNumber(result.views);
// Update the gallery view too
await loadPosts();
} catch (err) {
showToast('Failed to vote', 'error');
}
}
// ===== Comments =====
async function loadComments(postId) {
try {
const postComments = await apiGet(`/api/comments/${postId}`);
const list = document.getElementById('commentsList');
if (postComments.length === 0) {
list.innerHTML = '<p style="color:var(--text-muted);text-align:center;padding:24px;">No comments yet. Be the first!</p>';
return;
}
const topLevel = postComments.filter(c => !c.parentId);
const replies = postComments.filter(c => c.parentId);
list.innerHTML = topLevel.map(c => {
const childReplies = replies.filter(r => r.parentId === c.id);
return `
<div class="comment-item">
<div class="comment-author">${escapeHtml(c.author)}</div>
<div class="comment-text">${escapeHtml(c.text)}</div>
<div class="comment-time">${timeAgo(c.timestamp)}</div>
${childReplies.map(r => `
<div class="comment-item reply">
<div class="comment-author">${escapeHtml(r.author)}</div>
<div class="comment-text">${escapeHtml(r.text)}</div>
<div class="comment-time">${timeAgo(r.timestamp)}</div>
</div>
`).join('')}
</div>
`;
}).join('');
document.getElementById('postModalComments').textContent = postComments.length;
} catch (err) {
showToast('Failed to load comments', 'error');
}
}
async function postComment() {
const input = document.getElementById('commentInput');
const text = input.value.trim();
if (!text || !currentPostId) return;
try {
await apiPost('/api/comments', null, {
postId: currentPostId,
text,
author: 'Anonymous'
});
input.value = '';
await loadComments(currentPostId);
showToast('Comment posted! 💬', 'success');
} catch (err) {
showToast('Failed to post comment', 'error');
}
}
// ===== Fullscreen =====
function openFullscreen() {
const img = document.getElementById('postModalImage').src;
document.getElementById('fullscreenImage').src = img;
document.getElementById('fullscreenOverlay').classList.add('active');
}
function closeFullscreen() {
document.getElementById('fullscreenOverlay').classList.remove('active');
}
// ===== Navigation =====
function navigateHome() {
document.getElementById('searchInput').value = '';
loadPosts();
}
// ===== Utilities =====
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatNumber(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return n.toString();
}
function timeAgo(dateStr) {
const now = new Date();
const date = new Date(dateStr);
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return date.toLocaleDateString();
}
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function showError(msg) {
const el = document.getElementById('uploadError');
el.textContent = msg;
el.style.display = 'block';
}
function hideError() {
document.getElementById('uploadError').style.display = 'none';
}
// Close modals on overlay click
document.getElementById('uploadModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeUploadModal();
});
document.getElementById('postModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closePostModal();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeFullscreen();
closePostModal();
closeUploadModal();
}
});