diff --git a/website/main.js b/website/main.js index 9f603ea..7aa36d4 100644 --- a/website/main.js +++ b/website/main.js @@ -11,7 +11,6 @@ function getRandomRotation() { return 3 * Math.random() - 1.5; } -// Post-it note colors const postItColors = [ '#E6F3FF', // faded blue '#FFF9C4', // yellow @@ -25,87 +24,116 @@ function getRandomPostItColor() { } function getPostItColorByDate(dateString) { - // Create a simple hash from the date string let hash = 0; for (let i = 0; i < dateString.length; i++) { hash = ((hash << 5) - hash) + dateString.charCodeAt(i); - hash = hash & hash; // Convert to 32-bit integer + hash = hash & hash; } - // Use absolute value and modulo to get color index const colorIndex = Math.abs(hash) % postItColors.length; return postItColors[colorIndex]; } -// Global variable to track last user-set volume (default to 1) let lastMediaVolume = 1; +// Define attachHoverListeners function +function attachHoverListeners(postElement) { + // The base rotation is what was used for the loading animation. + // It's stored in the CSS custom property '--post-rotation'. + const baseRotation = postElement.style.getPropertyValue('--post-rotation'); + + postElement.addEventListener('mouseenter', function () { + postElement.style.zIndex = 100; + // Apply specific transition for this hover effect + postElement.style.transition = "transform 0.22s cubic-bezier(.5,1.5,.5,1), box-shadow 0.22s cubic-bezier(.5,1.5,.5,1)"; + // Hover transform: new random rotation + scale + postElement.style.transform = `rotate(${getRandomRotation()}deg) scale(1.1)`; + }); + + postElement.addEventListener('mouseleave', function () { + // Apply specific transition for unhover + postElement.style.transition = "transform 0.18s cubic-bezier(.5,0,.5,1), box-shadow 0.18s cubic-bezier(.5,0,.5,1)"; + // Return to base rotation + scale(1) + postElement.style.transform = `rotate(${baseRotation || '0deg'}) scale(1)`; + postElement.style.zIndex = 1; + }); +} + function createPost(t) { const post = document.createElement("div"); post.className = "post"; + // Set initial transform. This will be used by addPostToLayout to set --post-rotation. post.style.transform = `rotate(${getRandomRotation()}deg)`; post.style.backgroundColor = getPostItColorByDate(t.createdAt); - // --- Shrink-then-grow hover effect --- - post.addEventListener('mouseenter', function () { - post.style.zIndex = 100; - post.style.transition = "transform 0.22s cubic-bezier(.5,1.5,.5,1)"; - post.style.transform = `rotate(${getRandomRotation()}deg) scale(1.1)`; - }); - post.addEventListener('mouseleave', function () { - post.style.transition = "transform 0.18s cubic-bezier(.5,0,.5,1)"; - post.style.transform = `rotate(${getRandomRotation()}deg)`; - post.style.zIndex = 1; - }); - // --- end hover effect --- + // Hover listeners are now attached in addPostToLayout after the loading animation. const link = document.createElement("a"); link.href = t.url; link.target = "_blank"; link.style.textDecoration = "none"; - // --- EMBED HTML (images and videos) --- let embedHtml = ""; if (t.embed && t.embed.length > 0) { embedHtml = t.embed.map((embed, idx) => { if (embed.type === "image") { - // Image embed return ` - ${embed.alt || + ${embed.alt || `; } else if (embed.type === "video") { - // Video embed - // Use a data attribute for m3u8 playlist, set src only if not m3u8 const isM3u8 = embed.playlist && embed.playlist.endsWith('.m3u8'); return ` - +
+ +
`; } - // ...other embed types can be handled here... return ""; }).join(""); } - const textHtml = t.text.replace(/\n/g, "
"); + function linkifyText(text, highlightColor) { + text = text.replace(/(^|[\s.,;:!?])#([a-zA-Z0-9_]{2,50})\b/g, (m, pre, tag) => + `${pre}#${tag}` + ); + text = text.replace(/(^|[\s.,;:!?])@([a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+)/g, (m, pre, handle) => + `${pre}@${handle}` + ); + return text; + } + + function getHighlighterColor(cardColor) { + if (cardColor === '#E6F3FF') return '#ffb7ce'; + if (cardColor === '#FFF9C4') return '#b7e0fd'; + if (cardColor === '#E8F5E8') return '#fff89a'; + if (cardColor === '#FFE4B5') return '#b6fcb6'; + if (cardColor === '#FFE1E6') return '#ffd59e'; + return '#fff89a'; + } + + const cardColor = getPostItColorByDate(t.createdAt); + const highlightColor = getHighlighterColor(cardColor); + + const textHtml = linkifyText(t.text, highlightColor).replace(/\n/g, "
"); post.innerHTML = `
- ${t.author} + ${t.author}
-
${t.author} (@${t.handle})
-
+ +
@@ -118,7 +146,6 @@ function createPost(t) {
`; - // Add shadowbox event to images (after DOM is ready) setTimeout(() => { const imgs = post.querySelectorAll('img'); imgs.forEach(img => { @@ -129,24 +156,19 @@ function createPost(t) { }); }); - // --- HLS.js video support for .m3u8 --- - const videos = post.querySelectorAll('video[data-hls-src],video.video-embed'); - videos.forEach(video => { - // Set initial volume to lastMediaVolume + post.querySelectorAll('.video-embed-container').forEach(container => { + const video = container.querySelector('video'); + if (!video) return; + video.volume = lastMediaVolume; - // Prevent drag-selection when interacting with video controls (esp. volume) video.addEventListener('mousedown', function (e) { - // Only prevent drag if the event is on the controls bar (not the video area) - // This is a best-effort: always prevent drag for any mousedown on video controls e.stopPropagation(); }); - // Listen for volume changes and sync to all players video.addEventListener('volumechange', function () { if (!isNaN(video.volume)) { lastMediaVolume = video.volume; - // Sync all other videos on the page document.querySelectorAll('video.video-embed').forEach(v => { if (v !== video && Math.abs(v.volume - lastMediaVolume) > 0.01) { v.volume = lastMediaVolume; @@ -154,9 +176,27 @@ function createPost(t) { }); } }); + + video.addEventListener('play', function () { + document.querySelectorAll('video.video-embed').forEach(v => { + if (v !== video && !v.paused) { + v.pause(); + } + }); + }); + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (!entry.isIntersecting && !video.paused) { + video.pause(); + } + }); + }, { + threshold: 0.25 + }); + observer.observe(video); }); - // HLS.js setup for m3u8 const hlsVideos = post.querySelectorAll('video[data-hls-src]'); if (hlsVideos.length > 0 && window.Hls) { hlsVideos.forEach(video => { @@ -172,7 +212,6 @@ function createPost(t) { } }); } - // --- end HLS.js support --- }, 0); link.href = t.url; @@ -183,52 +222,26 @@ function createPost(t) { return link; } -// --- Shadowbox Modal with Smooth Animation --- function createShadowbox(postEl, imgEl) { - // Remove any existing shadowbox const existing = document.getElementById('shadowbox-modal'); if (existing) existing.remove(); - // Clone the post node const modal = document.createElement('div'); modal.id = 'shadowbox-modal'; - modal.style.position = 'fixed'; - modal.style.top = 0; - modal.style.left = 0; - modal.style.width = '100vw'; - modal.style.height = '100vh'; - modal.style.background = 'rgba(0,0,0,0.0)'; - modal.style.display = 'flex'; - modal.style.alignItems = 'flex-end'; // print from bottom - modal.style.justifyContent = 'center'; - modal.style.zIndex = 9999; - modal.style.cursor = 'zoom-out'; - modal.style.transition = 'background 0.35s cubic-bezier(.5,1.5,.5,1)'; + modal.className = 'shadowbox-modal'; - // Container for the post const postClone = postEl.cloneNode(true); - postClone.style.transform = 'translateY(100vh) scale(0.98)'; + postClone.className += ' shadowbox-post'; postClone.style.background = postEl.style.backgroundColor || '#fff'; - postClone.style.maxWidth = '95vw'; - postClone.style.maxHeight = '90vh'; - postClone.style.overflow = 'auto'; - postClone.style.position = 'relative'; - postClone.style.zIndex = 10001; - postClone.style.cursor = 'default'; - postClone.style.opacity = '0'; - postClone.style.transition = 'opacity 0.25s cubic-bezier(.5,1.5,.5,1), transform 0.5s cubic-bezier(.5,1.5,.5,1)'; - // Remove tape elements from the shadowbox const tapeElements = postClone.querySelectorAll('.tape'); tapeElements.forEach(tape => tape.remove()); - // Focus on the clicked image: zoom it in const imgs = postClone.querySelectorAll('img'); const originalImgs = postEl.querySelectorAll('img'); let focusedImg = null; let clickedImageIndex = -1; - // Find the index of the clicked image in the original post originalImgs.forEach((originalImg, idx) => { if (originalImg === imgEl) { clickedImageIndex = idx; @@ -236,83 +249,49 @@ function createShadowbox(postEl, imgEl) { }); imgs.forEach((cloneImg, idx) => { - // Always use the full-size image for all images in the modal if (cloneImg.classList.contains('image-placeholder')) { const fullSizeUrl = getFullSizeImageUrl(cloneImg.src); cloneImg.src = fullSizeUrl; - // Apply modal styles to all images immediately - cloneImg.style.maxWidth = '90vw'; - cloneImg.style.maxHeight = '80vh'; - cloneImg.style.width = ''; - cloneImg.style.objectFit = 'contain'; - cloneImg.style.display = 'block'; - cloneImg.style.margin = '24px auto'; - cloneImg.style.opacity = 1; + cloneImg.className += ' shadowbox-image'; } - // Use index matching instead of URL comparison if (idx === clickedImageIndex && cloneImg.classList.contains('image-placeholder')) { focusedImg = cloneImg; } }); - // Add close button const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; - closeBtn.style.position = 'absolute'; - closeBtn.style.top = '10px'; - closeBtn.style.right = '16px'; - closeBtn.style.fontSize = '2rem'; - closeBtn.style.background = 'rgba(0,0,0,0.2)'; - closeBtn.style.color = '#fff'; - closeBtn.style.border = 'none'; - closeBtn.style.width = '40px'; - closeBtn.style.height = '40px'; - closeBtn.style.cursor = 'pointer'; - closeBtn.style.zIndex = 10002; - closeBtn.style.transition = 'background 0.2s'; - closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = 'rgba(0,0,0,0.5)'); - closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = 'rgba(0,0,0,0.2)'); + closeBtn.className = 'shadowbox-close'; closeBtn.addEventListener('click', e => { e.stopPropagation(); closeShadowbox(); }); postClone.appendChild(closeBtn); - // Prevent click inside post from closing modal postClone.addEventListener('click', e => e.stopPropagation()); - modal.appendChild(postClone); - - // Click outside closes modal modal.addEventListener('click', closeShadowbox); - document.body.appendChild(modal); - // --- Animate modal background and post "printing" from bottom --- setTimeout(() => { - modal.style.background = 'rgba(0,0,0,0.7)'; - postClone.style.transform = 'translateY(0) scale(1)'; - postClone.style.opacity = '1'; + modal.classList.add('visible'); + postClone.classList.add('visible'); }, 10); - // Scroll to focused image smoothly after animation setTimeout(() => { if (focusedImg) { focusedImg.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 400); - // Animate out and remove function closeShadowbox() { - modal.style.background = 'rgba(0,0,0,0.0)'; - postClone.style.transform = 'translateY(100vh) scale(0.98)'; - postClone.style.opacity = '0'; + modal.classList.remove('visible'); + postClone.classList.remove('visible'); setTimeout(() => { modal.remove(); }, 350); } - // Escape key closes modal function escListener(e) { if (e.key === 'Escape') { closeShadowbox(); @@ -321,25 +300,20 @@ function createShadowbox(postEl, imgEl) { } window.addEventListener('keydown', escListener); - // --- Snap scroll to next image in post on scroll --- let lastFocusedIdx = Array.from(imgs).findIndex(cloneImg => cloneImg === focusedImg); let snapTimeout = null; let isInitialScroll = true; - // Disable initial scroll detection after the automatic scroll completes setTimeout(() => { isInitialScroll = false; }, 800); postClone.addEventListener('scroll', () => { - // Skip scroll detection during initial automatic scroll if (isInitialScroll) return; - // Find the image closest to the center of the viewport let modalRect = postClone.getBoundingClientRect(); let modalCenter = modalRect.top + modalRect.height / 2; - // Find all images whose center is within a threshold of the modal center let candidates = []; imgs.forEach((img, idx) => { const rect = img.getBoundingClientRect(); @@ -348,18 +322,14 @@ function createShadowbox(postEl, imgEl) { candidates.push({ idx, dist, center, rect }); }); - // Sort by distance to center candidates.sort((a, b) => a.dist - b.dist); - // If the closest image is not the current, and its center is within 15% of its height from modal center, focus it let best = candidates[0]; if ( best.idx !== lastFocusedIdx && best.dist < best.rect.height * 0.75 ) { - // Just update the lastFocusedIdx and scroll, don't change styles lastFocusedIdx = best.idx; - // Snap scroll after short delay if (snapTimeout) clearTimeout(snapTimeout); snapTimeout = setTimeout(() => { imgs[lastFocusedIdx].scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -368,10 +338,8 @@ function createShadowbox(postEl, imgEl) { }); } -// Helper function to convert thumbnail URL to full-size URL function getFullSizeImageUrl(thumbUrl) { if (!thumbUrl) return thumbUrl; - // Convert Bluesky thumbnail URLs to full-size by removing size parameters return thumbUrl.replace(/@jpeg$/, '').replace(/\?.*$/, ''); } @@ -384,6 +352,9 @@ function filterOriginalPosts(feed) { } let postElementsCache = null; +let layoutColumns = null; +let postQueue = []; +let isProcessingQueue = false; function createColumns(num) { const postsContainer = document.getElementById("posts"); @@ -405,19 +376,128 @@ function getNumColumns() { } function getShortestColumn(columns) { - let minHeight = columns[0].offsetHeight; - let minCol = columns[0]; + let minHeight = Infinity; + let shortestColumns = []; + + // Find all columns with the minimum height for (const col of columns) { - if (col.offsetHeight < minHeight) { - minHeight = col.offsetHeight; - minCol = col; + const height = col.offsetHeight; + if (height < minHeight) { + minHeight = height; + shortestColumns = [col]; + } else if (height === minHeight) { + shortestColumns.push(col); } } - return minCol; + + // If multiple columns have the same height, prefer the middle one + if (shortestColumns.length > 1) { + const middleIndex = Math.floor(columns.length / 2); + const middleColumn = columns[middleIndex]; + if (shortestColumns.includes(middleColumn)) { + return middleColumn; + } + // If middle column isn't shortest, return the one closest to middle + let closestToMiddle = shortestColumns[0]; + let minDistance = Math.abs(columns.indexOf(closestToMiddle) - middleIndex); + + for (const col of shortestColumns) { + const distance = Math.abs(columns.indexOf(col) - middleIndex); + if (distance < minDistance) { + minDistance = distance; + closestToMiddle = col; + } + } + return closestToMiddle; + } + + return shortestColumns[0]; +} + +function addPostToLayout(postEl, zIndex) { + if (!layoutColumns) return; + + const col = getShortestColumn(layoutColumns); + const postDiv = postEl.firstElementChild; // This is the .post div + + // Store the current rotation (from postDiv.style.transform) for the animation + const currentTransform = postDiv.style.transform; + const rotationMatch = currentTransform.match(/rotate\(([-\d.]+deg)\)/); + const rotation = rotationMatch ? rotationMatch[1] : '0deg'; + + postDiv.style.setProperty('--post-rotation', rotation); // Used by slideUpFadeIn animation + postDiv.style.zIndex = zIndex; + + // Add loading animation class + postDiv.classList.add('loading'); + + col.appendChild(postEl); + + // Remove animation class after animation completes AND THEN attach hover listeners + setTimeout(() => { + postDiv.classList.remove('loading'); + // Now that the loading animation is done, attach hover listeners + attachHoverListeners(postDiv); + }, 600); // Duration of slideUpFadeIn animation (0.6s) + + // Add tape only for this specific post + setTimeout(() => { + const imgs = postEl.querySelectorAll('img.image-placeholder'); + if (imgs.length > 0 && window.addTapeToPost) { + window.addTapeToPost(postEl); + } + }, 50); +} + +async function processPostQueue() { + if (isProcessingQueue || postQueue.length === 0) return; + + isProcessingQueue = true; + + while (postQueue.length > 0) { + const { postEl, zIndex } = postQueue.shift(); + + // Create promises for this post's media + const promises = []; + + // Preload images + const imgs = postEl.querySelectorAll("img"); + if (imgs.length) { + promises.push(...Array.from(imgs).map(img => { + if (img.complete) return Promise.resolve(); + return new Promise(res => { + img.onload = img.onerror = res; + }); + })); + } + + // Preload video metadata + const videos = postEl.querySelectorAll("video"); + if (videos.length) { + promises.push(...Array.from(videos).map(video => { + if (video.readyState >= 1) return Promise.resolve(); + return new Promise(res => { + video.onloadedmetadata = video.onerror = res; + }); + })); + } + + // Wait for this post's media to load before adding to layout + if (promises.length > 0) { + await Promise.all(promises); + } + + // Add post to layout + addPostToLayout(postEl, zIndex); + + // Small delay between posts for visual effect + await new Promise(resolve => setTimeout(resolve, 100)); + } + + isProcessingQueue = false; } function layoutPosts(postElements) { - // Prevent layout if any video is in fullscreen if (document.fullscreenElement && document.fullscreenElement.tagName === "VIDEO") { console.log('[Main] Skipping layoutPosts: video is fullscreen'); return; @@ -426,18 +506,16 @@ function layoutPosts(postElements) { const columns = createColumns(numCols); postElements.forEach((postEl, index) => { const col = getShortestColumn(columns); - // Set descending z-index - first post has highest z-index postEl.firstElementChild.style.zIndex = postElements.length - index; col.appendChild(postEl); }); - // Use refreshTape to avoid duplicate tape on resize/layout - console.log('[Main] Posts laid out, refreshing tape for all images'); + console.log('[Main] Posts laid out, adding tape for all images'); setTimeout(() => { - if (window.refreshTape) { - window.refreshTape(); + if (window.addTapeToImages) { + window.addTapeToImages(); } else { - console.error('[Main] refreshTape function not available'); + console.error('[Main] addTapeToImages function not available'); } }, 200); } @@ -451,20 +529,15 @@ async function fetchPosts() { const data = await res.json(); const posts = filterOriginalPosts(data.feed).map(item => { - // --- EMBED HANDLING --- let embeds = []; const embedObj = item.post.embed; if (embedObj) { - // Debug output (optional) - // console.log("[Main] Post embed:", embedObj); - // console.log("[Main] Embed $type:", embedObj.$type); - if (embedObj.$type === "app.bsky.embed.images#view" && Array.isArray(embedObj.images)) { embeds = embedObj.images.map(img => ({ type: "image", url: img.thumb, alt: img.alt || "", - fullres: img.fullsize || img.full || img.thumb // fallback to thumb if no fullres + fullres: img.fullsize || img.full || img.thumb })); } else if (embedObj.$type === "app.bsky.embed.video#view") { embeds = [{ @@ -475,7 +548,6 @@ async function fetchPosts() { aspectRatio: embedObj.aspectRatio || null }]; } - // Optionally handle other embed types (external, record, etc) here } return { @@ -493,42 +565,50 @@ async function fetchPosts() { console.log(`[Main] Fetched ${posts.length} posts, ${posts.filter(p => p.embed.some(e => e.type === "image")).length} have images, ${posts.filter(p => p.embed.some(e => e.type === "video")).length} have videos`); - // Only create post elements once + // Initialize layout columns + const numCols = getNumColumns(); + layoutColumns = createColumns(numCols); + + // Create all post elements postElementsCache = posts.map(post => createPost(post)); - // Preload images - await Promise.all(postElementsCache.map(el => { - const imgs = el.querySelectorAll("img"); - if (!imgs.length) return Promise.resolve(); - return Promise.all(Array.from(imgs).map(img => { - if (img.complete) return Promise.resolve(); - return new Promise(res => { - img.onload = img.onerror = res; - }); - })); + + // Add posts to queue in order + postQueue = postElementsCache.map((postEl, index) => ({ + postEl, + zIndex: postElementsCache.length - index })); - - layoutPosts(postElementsCache); + + // Start processing the queue + processPostQueue(); } catch (err) { console.error("Error fetching posts:", err); } } -// Only re-layout on resize, do not re-fetch or re-create posts window.addEventListener("resize", () => { - // Prevent layout if any video is in fullscreen if (document.fullscreenElement && document.fullscreenElement.tagName === "VIDEO") { console.log('[Main] Skipping resize layout: video is fullscreen'); return; } - if (postElementsCache) { - layoutPosts(postElementsCache); - // Re-add tape after layout changes - if (window.refreshTape) { - setTimeout(() => { + if (postElementsCache && layoutColumns) { + // Re-layout all existing posts + layoutColumns = createColumns(getNumColumns()); + postElementsCache.forEach((postEl, index) => { + if (postEl.parentNode) { // Only re-layout posts that are already added + const zIndex = postElementsCache.length - index; + const col = getShortestColumn(layoutColumns); + const postDiv = postEl.firstElementChild; + postDiv.style.zIndex = zIndex; + col.appendChild(postEl); + } + }); + // Re-add tape after layout changes using the full refresh + setTimeout(() => { + if (window.refreshTape) { window.refreshTape(); - }, 100); - } + } + }, 100); } }); diff --git a/website/style.css b/website/style.css index ebc7582..3bbeaa7 100644 --- a/website/style.css +++ b/website/style.css @@ -265,16 +265,6 @@ social-links a:hover .fab.fa-youtube { text-shadow: 1px 1px 0 #fff; } -.image-placeholder { - margin-top: 1rem; - border: 4px solid var(--post-border); - background: var(--avatar-bg); - text-align: center; - transform: rotate(0deg); /* Remove rotation from images */ - box-shadow: 4px 4px 0 var(--post-border); - display: block; /* Ensure it's a block element */ -} - /* Ensure image containers can hold positioned tape */ .image-placeholder, .image-placeholder img { @@ -442,3 +432,211 @@ footer a:hover { color: var(--accent); } +.hashtag { + color: #222; + /* Remove text rotation/skew, only background is "sloppy" */ + background: none; + border-radius: 6px 12px 8px 10px / 10px 8px 12px 6px; + padding: 0 6px 2px 6px; + font-weight: 500; + cursor: pointer; + box-shadow: + 0 2px 8px 0 rgba(255, 248, 154, 0.18), + 0 1px 0 0 rgba(255, 248, 154, 0.25); + display: inline-block; + margin: 0 2px; + position: relative; + z-index: 1; +} +.hashtag::before { + content: ""; + position: absolute; + z-index: -1; + left: -3px; + right: -3px; + top: 2px; + bottom: 2px; + /* Default fallback color, will be overridden inline by JS */ + background: linear-gradient( + 4deg, + var(--hashtag-bg, #fff89a) 0%, + var(--hashtag-bg, #fff89a) 100% + ); + opacity: 0.7; + border-radius: 8px 14px 10px 12px / 12px 10px 14px 8px; + transform: rotate(-2deg) skewX(-2deg); + pointer-events: none; +} + +.mention { + color: #388e3c; + background: #e8f5e9; + border-radius: 4px; + color: #388e3c; + background: #e8f5e9; + border-radius: 4px; + padding: 0 4px; + font-weight: 500; cursor: pointer; +} + +/* Video center play button styling */ +.video-center-play { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; + background: rgba(0, 0, 0, 0.7); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + z-index: 10; + pointer-events: auto; +} + +.video-center-play::before { + content: "▶"; + color: white; + font-size: 24px; + margin-left: 4px; /* Slight offset to center the triangle visually */ +} + +.video-center-play:hover { + background: rgba(0, 0, 0, 0.9); + transform: translate(-50%, -50%) scale(1.1); +} + +.video-center-play:focus { + outline: 2px solid white; + outline-offset: 2px; +} + +/* Video embed container and video styling */ +.video-embed-container { + position: relative; +} + +.video-embed { + width: 100%; + max-height: 60vh; + margin: 1.5rem 0 12px 0; + background: #000; + border-radius: 8px; +} + +/* Shadowbox modal styling */ +.shadowbox-modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.0); + display: flex; + align-items: flex-end; + justify-content: center; + z-index: 9999; + cursor: zoom-out; + transition: background 0.35s cubic-bezier(.5,1.5,.5,1); +} + +.shadowbox-modal.visible { + background: rgba(0,0,0,0.7); +} + +.shadowbox-post { + transform: translateY(100vh) scale(0.98); + max-width: 75vw; + max-height: 90vh; + overflow: auto; + position: relative; + z-index: 10001; + cursor: default; + opacity: 0; + transition: opacity 0.25s cubic-bezier(.5,1.5,.5,1), transform 0.5s cubic-bezier(.5,1.5,.5,1); +} + +.shadowbox-post.visible { + transform: translateY(0) scale(1) rotate(0deg); + opacity: 1; +} + +.shadowbox-image { + max-width: 90vw !important; + max-height: 80vh !important; + width: auto !important; + object-fit: contain !important; + display: block !important; + margin: 24px auto !important; + opacity: 1 !important; +} + +.shadowbox-close { + position: absolute; + top: 10px; + right: 16px; + font-size: 2rem; + background: rgba(0,0,0,0.2); + color: #fff; + border: none; + width: 40px; + height: 40px; + cursor: pointer; + z-index: 10002; + transition: background 0.2s; +} + +.shadowbox-close:hover { + background: rgba(0,0,0,0.5); +} + +/* Post header avatar image styling */ +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Post metadata styling */ +.post-author { + font-weight: bold; +} + +.post-date { + font-size: 0.875rem; + color: #666; +} + +/* Image placeholder styling */ +.image-placeholder { + width: 100%; + margin-top: 1.5rem; + border: 4px solid var(--post-border); + background: var(--avatar-bg); + text-align: center; + transform: rotate(0deg); + box-shadow: 4px 4px 0 var(--post-border); + display: block; +} + +/* Post animation for loading */ +@keyframes slideUpFadeIn { + from { + opacity: 0; + transform: translateY(50px) rotate(var(--post-rotation, 0deg)); + } + to { + opacity: 1; + transform: translateY(0) rotate(var(--post-rotation, 0deg)); + } +} + +.post.loading { + opacity: 0; + transform: translateY(50px) rotate(var(--post-rotation, 0deg)); + animation: slideUpFadeIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} \ No newline at end of file diff --git a/website/tape.js b/website/tape.js index 89512fe..51231a9 100644 --- a/website/tape.js +++ b/website/tape.js @@ -125,6 +125,51 @@ function refreshTape() { addTapeToImages(); } +// Add tape to a specific post element +function addTapeToPost(postElement) { + const images = postElement.querySelectorAll('.image-placeholder'); + console.log(`[Tape] Found ${images.length} images in post to add tape to`); + + if (images.length === 0) { + return; + } + + images.forEach((image, index) => { + // Check if image already has tape + const existingTape = image.parentElement.querySelectorAll('.tape'); + if (existingTape.length > 0) { + console.log(`[Tape] Image ${index + 1} already has tape, skipping`); + return; + } + + // Ensure the image parent has relative positioning + const imageParent = image.parentElement; + if (imageParent && window.getComputedStyle(imageParent).position === 'static') { + imageParent.style.position = 'relative'; + } + + // Create two tape pieces + const tape1 = createTapeElement(index, 1); + const tape2 = createTapeElement(index, 2); + + // Position based on pattern + const usePattern1 = index % 2 === 0; + if (usePattern1) { + positionTape(tape1, 'top-left'); + positionTape(tape2, 'bottom-right'); + } else { + positionTape(tape1, 'top-right'); + positionTape(tape2, 'bottom-left'); + } + + // Add to the image's parent container + imageParent.appendChild(tape1); + imageParent.appendChild(tape2); + + console.log(`[Tape] Added tape to image ${index + 1} in specific post`); + }); +} + // Simple test function function testSimpleTape() { console.log('[Tape Test] Creating simple test...'); @@ -168,6 +213,7 @@ function testSimpleTape() { // Export functions window.addTapeToImages = addTapeToImages; +window.addTapeToPost = addTapeToPost; window.refreshTape = refreshTape; window.testSimpleTape = testSimpleTape;