/* ============================================================ GLASSGRID — POST CARD COMPONENT ⚠️ STRUCTURE IS FIXED. Do not modify class names. ⚠️ All visual changes through tokens/components.css only. ⚠️ Schema-validated post objects only. No raw HTML. ============================================================ */ 'use strict'; import { store, showToast } from '../lib/store.js'; import { api } from '../lib/api.js'; /** * Render a post card element. * @param {Object} post - Schema.Post validated object * @param {Object} user - Schema.User for the post author * @returns {HTMLElement} */ export function PostCard(post, user) { const liked = store.get('liked')?.[post.id] ?? false; const saved = store.get('saved')?.[post.id] ?? false; const config = store.get('config') ?? {}; const features = store.get('features') ?? {}; const el = document.createElement('article'); el.className = 'post-card glass animate-fadeInUp'; el.dataset.postId = post.id; el.setAttribute('role', 'article'); el.innerHTML = `
${escHtml(user.displayName)}'s avatar
${escHtml(user.displayName)}
@${escHtml(user.username)} ${post.location ? ` · ${escHtml(post.location)}` : ''}
${formatRelativeTime(post.createdAt)}
${escHtml(post.altText || post.caption || 'Post image')}
${features.likes ? ` ` : ''} ${features.comments ? ` ` : ''} ${features.saves ? ` ` : ''}
${post.caption ? `

${escHtml(user.username)}${escHtml(post.caption)}

` : ''} ${post.commentCount > 0 && features.comments ? `
` : ''} `; // ── Event Delegation ── el.addEventListener('click', async (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; switch (btn.dataset.action) { case 'like': await handleLike(el, post); break; case 'comment': handleComment(post.id); break; case 'save': await handleSave(el, post); break; case 'more': handleMore(post, user); break; } }); return el; } // ── Handlers ── async function handleLike(el, post) { const liked = store.get('liked') ?? {}; const isLiked = liked[post.id] ?? false; const likeBtn = el.querySelector('[data-action="like"]'); const countEl = el.querySelector('.post-card__like-count'); // Optimistic update const newLiked = !isLiked; store.update('liked', prev => ({ ...prev, [post.id]: newLiked })); likeBtn?.classList.toggle('liked', newLiked); likeBtn?.setAttribute('aria-pressed', String(newLiked)); if (likeBtn) likeBtn.innerHTML = (newLiked ? IconHeartFilled() : IconHeart()) + `${formatCount(post.likeCount + (newLiked ? 1 : -1))}`; if (newLiked) likeBtn?.classList.add('animate-heartPop'); setTimeout(() => likeBtn?.classList.remove('animate-heartPop'), 600); try { if (newLiked) await api.likePost(post.id); else await api.unlikePost(post.id); } catch { // Revert on failure store.update('liked', prev => ({ ...prev, [post.id]: isLiked })); likeBtn?.classList.toggle('liked', isLiked); showToast('Could not update like. Try again.'); } } async function handleSave(el, post) { const saved = store.get('saved') ?? {}; const isSaved = saved[post.id] ?? false; const saveBtn = el.querySelector('[data-action="save"]'); store.update('saved', prev => ({ ...prev, [post.id]: !isSaved })); saveBtn?.classList.toggle('saved', !isSaved); showToast(!isSaved ? 'Saved to collection' : 'Removed from collection'); } function handleComment(postId) { const ui = store.get('ui'); store.set('ui', { ...ui, commentModal: postId }); } function handleMore(post, user) { document.dispatchEvent(new CustomEvent('gg:post:more', { detail: { post, user } })); } // ── Helpers ── function escHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatRelativeTime(timestamp) { const diff = (Date.now() - new Date(timestamp)) / 1000; if (diff < 60) return 'now'; if (diff < 3600) return `${Math.floor(diff/60)}m`; if (diff < 86400) return `${Math.floor(diff/3600)}h`; if (diff < 604800)return `${Math.floor(diff/86400)}d`; return new Date(timestamp).toLocaleDateString(); } function formatCount(n = 0) { if (n >= 1_000_000) return `${(n/1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n/1_000).toFixed(1)}K`; return String(n); } // ── SVG Icons ── function IconHeart() { return ``; } function IconHeartFilled() { return ``; } function IconComment() { return ``; } function IconBookmark() { return ``; } function IconBookmarkFilled(){ return ``; } function IconMore() { return ``; }