diff --git a/templates/_layout.html b/templates/_layout.html index fb75ad3..35cbd24 100644 --- a/templates/_layout.html +++ b/templates/_layout.html @@ -32,6 +32,8 @@ + + <!-- ::PAGE_TITLE:: --> diff --git a/website/dynamic-rotation.js b/website/dynamic-rotation.js new file mode 100644 index 0000000..12a373e --- /dev/null +++ b/website/dynamic-rotation.js @@ -0,0 +1,55 @@ +(function() { + function updatePostRotations() { + const posts = document.querySelectorAll('.post'); + + posts.forEach(post => { + const height = post.offsetHeight; + + // Calculate rotation: 3deg for 200px posts, 0.5deg for 500px+ posts + const minRotation = 0.5; + const maxRotation = 2.5; + const minHeight = 200; + const maxHeight = 700; + + // Linear interpolation between min and max rotation based on height + let rotation = maxRotation - ((height - minHeight) / (maxHeight - minHeight)) * (maxRotation - minRotation); + + // Clamp between min and max + rotation = Math.max(minRotation, Math.min(maxRotation, rotation)); + + // Apply alternating direction + const isOdd = Array.from(posts).indexOf(post) % 2 === 0; + const finalRotation = isOdd ? -rotation : rotation; + + // Set CSS custom property + post.style.setProperty('--dynamic-rotation', `${finalRotation}deg`); + post.style.transform = `rotate(var(--dynamic-rotation, ${finalRotation}deg))`; + }); + } + + // Update rotations when DOM is ready + function init() { + // Wait for images to load to get accurate heights + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(updatePostRotations, 100); + }); + } else { + setTimeout(updatePostRotations, 100); + } + + // Update after window load for images + window.addEventListener('load', () => { + setTimeout(updatePostRotations, 200); + }); + + // Update on resize with debouncing + let resizeTimeout; + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(updatePostRotations, 300); + }); + } + + init(); +})(); diff --git a/website/index.html b/website/index.html index a9473f4..9d212bd 100644 --- a/website/index.html +++ b/website/index.html @@ -32,6 +32,8 @@ + + Litruv - Dev/Tech Artist diff --git a/website/main.js b/website/main.js index 2abcb6f..6189164 100644 --- a/website/main.js +++ b/website/main.js @@ -1 +1,162 @@ -function formatDate(t){return new Date(t).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function getRandomRotation(){return 3*Math.random()-1.5}function createPost(t){const e=document.createElement("div");e.className="post",e.style.transform=`rotate(${getRandomRotation()}deg)`;const o=document.createElement("a");o.href=t.url,o.target="_blank",o.style.textDecoration="none";const n=t.embed.length>0?t.embed.map((t=>`\n \n ${t.alt||\n \n `)).join(""):"",a=t.text.replace(/\n/g,"
");return e.innerHTML=`\n
\n
\n ${t.author}\n
\n
\n
${t.author} (@${t.handle})
\n
\n ${formatDate(t.createdAt)}\n
\n
\n
\n
${a}
\n ${n}\n
\n \n \n
\n `,o.href=t.url,o.appendChild(e),o}function filterOriginalPosts(t){return t.filter((t=>{const e="app.bsky.feed.defs#reasonRepost"===t?.reason?.$type,o=t?.post?.record?.reply;return!e&&!o}))}async function fetchPosts(){try{const t=await fetch("https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=lit.mates.dev&limit=20&filter=posts_no_replies");if(!t.ok)throw new Error("Failed to fetch posts");const e=await t.json(),o=filterOriginalPosts(e.feed).map((t=>({author:t.post.author.displayName,handle:t.post.author.handle,avatar:t.post.author.avatar,createdAt:t.post.record.createdAt,text:t.post.record.text,likes:t.post.likeCount||0,reposts:t.post.repostCount||0,url:`https://bsky.app/profile/${t.post.author.handle}/post/${t.post.uri.split("/").pop()}`,embed:t.post.embed?.images?.map((t=>({url:t.thumb,alt:t.alt||""})))||[]}))),n=document.getElementById("posts");n.innerHTML="",o.forEach((t=>{n.appendChild(createPost(t))}))}catch(t){console.error("Error fetching posts:",t)}}fetchPosts(); \ No newline at end of file +function formatDate(t) { + return new Date(t).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }); +} + +function getRandomRotation() { + return 3 * Math.random() - 1.5; +} + +function createPost(t) { + const post = document.createElement("div"); + post.className = "post"; + post.style.transform = `rotate(${getRandomRotation()}deg)`; + + const link = document.createElement("a"); + link.href = t.url; + link.target = "_blank"; + link.style.textDecoration = "none"; + + const embedHtml = t.embed.length > 0 + ? t.embed.map(img => + ` + + ${img.alt || + + ` + ).join("") + : ""; + + const textHtml = t.text.replace(/\n/g, "
"); + + post.innerHTML = ` +
+
+ ${t.author} +
+
+
${t.author} (@${t.handle})
+
+ ${formatDate(t.createdAt)} +
+
+
+
${textHtml}
+ ${embedHtml} +
+ + +
+ `; + + link.href = t.url; + link.appendChild(post); + return link; +} + +function filterOriginalPosts(feed) { + return feed.filter(item => { + const isRepost = item?.reason?.$type === "app.bsky.feed.defs#reasonRepost"; + const isReply = item?.post?.record?.reply; + return !isRepost && !isReply; + }); +} + +let postElementsCache = null; + +function createColumns(num) { + const postsContainer = document.getElementById("posts"); + postsContainer.innerHTML = ""; + const columns = []; + for (let i = 0; i < num; i++) { + const col = document.createElement("div"); + col.className = "masonry-col"; + postsContainer.appendChild(col); + columns.push(col); + } + return columns; +} + +function getNumColumns() { + if (window.innerWidth >= 1200) return 3; + if (window.innerWidth >= 800) return 2; + return 1; +} + +function getShortestColumn(columns) { + let minHeight = columns[0].offsetHeight; + let minCol = columns[0]; + for (const col of columns) { + if (col.offsetHeight < minHeight) { + minHeight = col.offsetHeight; + minCol = col; + } + } + return minCol; +} + +function layoutPosts(postElements) { + const numCols = getNumColumns(); + const columns = createColumns(numCols); + postElements.forEach(postEl => { + const col = getShortestColumn(columns); + col.appendChild(postEl); + }); +} + +async function fetchPosts() { + try { + const res = await fetch( + "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=lit.mates.dev&limit=20&filter=posts_no_replies" + ); + if (!res.ok) throw new Error("Failed to fetch posts"); + const data = await res.json(); + + const posts = filterOriginalPosts(data.feed).map(item => ({ + author: item.post.author.displayName, + handle: item.post.author.handle, + avatar: item.post.author.avatar, + createdAt: item.post.record.createdAt, + text: item.post.record.text, + likes: item.post.likeCount || 0, + reposts: item.post.repostCount || 0, + url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`, + embed: item.post.embed?.images?.map(img => ({ + url: img.thumb, + alt: img.alt || "" + })) || [] + })); + + // Only create post elements once + 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; + }); + })); + })); + + layoutPosts(postElementsCache); + + } catch (err) { + console.error("Error fetching posts:", err); + } +} + +// Only re-layout on resize, do not re-fetch or re-create posts +window.addEventListener("resize", () => { + if (postElementsCache) { + layoutPosts(postElementsCache); + } +}); + +fetchPosts(); \ No newline at end of file diff --git a/website/masonry-smooth.js b/website/masonry-smooth.js new file mode 100644 index 0000000..6a8cc44 --- /dev/null +++ b/website/masonry-smooth.js @@ -0,0 +1,53 @@ +(function() { + let currentLayout = null; + let posts = []; + let columns = []; + let originalPositions = new Map(); + + function initMasonry() { + const postsContainer = document.getElementById('posts'); + if (!postsContainer) return; + + posts = Array.from(postsContainer.querySelectorAll('.post')); + columns = Array.from(postsContainer.querySelectorAll('.masonry-col')); + + if (posts.length === 0 || columns.length === 0) return; + + // Store original positions of posts + posts.forEach((post, index) => { + const rect = post.getBoundingClientRect(); + originalPositions.set(post, { + originalParent: post.parentNode, + originalIndex: index, + originalRect: rect + }); + }); + + checkLayout(); + } + + function checkLayout() { + const isMobile = window.innerWidth <= 799; + const newLayout = isMobile ? 'mobile' : 'desktop'; + + if (newLayout !== currentLayout) { + currentLayout = newLayout; + // Don't redistribute - just let CSS media queries handle it + // The layout change is handled purely by CSS + } + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initMasonry); + } else { + initMasonry(); + } + + // Listen for resize events but don't do anything that affects layout + let resizeTimeout; + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(checkLayout, 150); + }); +})(); diff --git a/website/privacy.html b/website/privacy.html index 6a793b0..2f1f215 100644 --- a/website/privacy.html +++ b/website/privacy.html @@ -66,6 +66,8 @@ + + Privacy Policy - Litruv diff --git a/website/style.css b/website/style.css index d382aee..f0c905c 100644 --- a/website/style.css +++ b/website/style.css @@ -1,9 +1,40 @@ +:root { + --main-bg: #242424; + --main-fg: #f0f0f0; + --accent: #2563eb; + --like: #db2777; + --github: #181717; + --youtube: #FF0000; + --steam: #1b2838; + --discord: #7289da; + --bluesky: #3E5BFF; + --post-bg: #fff; + --post-border: #000; + --avatar-bg: #f0f0f0; + --avatar-border: #000; + --shadow-light: rgba(0, 0, 0, 0.1); + --shadow-heavy: rgba(0, 0, 0, 1); + --shadow-mid: rgba(0, 0, 0, 0.05); + --header-shadow1: #ffffff52; + --header-shadow2: #000; +} + +html { + box-sizing: border-box; + scroll-behavior: smooth; +} + +*, *::before, *::after { + box-sizing: inherit; +} + body { margin: 0; min-height: 100vh; - background: #242424; + background: var(--main-bg); font-family: monospace; background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h20v20H0V0zm2 2v16h16V2H2z' fill='%23000000' fill-opacity='0.05'/%3E%3C/svg%3E"); + overflow-x: hidden; } a { @@ -20,25 +51,29 @@ a { } .container { - max-width: 600px; + max-width: 1200px; /* set max width */ + width: 100%; margin: 0 auto; - padding: 2rem; + padding: 2rem 0 2rem 0; position: relative; z-index: 1; - padding-top: 100px; + padding-top: 160px; /* increased from 100px */ + box-sizing: border-box; + overflow: visible; /* ensure expanded content is visible */ } header { text-align: center; margin-bottom: 80px; - color: #f0f0f0; + color: var(--main-fg); font-size: 20px; + padding-top: 2rem; /* add extra space above header */ + overflow: visible; } - h1 { transition: all 0.3s ease; - text-shadow: 2px 2px 0 #ffffff52, 3px 3px 0 #000; + text-shadow: 2px 2px 0 var(--header-shadow1), 3px 3px 0 var(--header-shadow2); margin-bottom: 0.5rem; transform-origin: center; cursor: pointer; @@ -53,24 +88,129 @@ h1 { .social-links a { margin: 0 10px; - color: #f0f0f0; + color: var(--main-fg); text-decoration: none; font-weight: bold; + font-size: 2rem; + display: inline-block; } .social-links a:hover { - color: #2563eb; + color: var(--accent); + transform: scale(1.1); + transition: transform 0.3s ease; + text-shadow: 0 0 3px white; +} + +.social-links a:hover .fab.fa-github { + color: var(--github); +} + +social-links a:hover .fab.fa-youtube { + color: var(--youtube); +} + +.social-links a:hover .fab.fa-steam { + color: var(--steam); +} + +.social-links a:hover .fab.fa-discord { + color: var(--discord); +} + +.social-links a:hover .fa-brands.fa-bluesky { + color: var(--bluesky); +} + +#posts { + max-width: 1200px; + width: 100%; + margin-left: auto; + margin-right: auto; + margin-bottom: 2rem; + display: flex; + justify-content: center; + align-items: flex-start; + padding-left: 80px; /* Increase left padding to balance the negative margin */ + padding-right: 40px; + box-sizing: border-box; + text-align: left; + min-height: 0; /* Prevent flex container from causing jumps */ + /* Add stable height to prevent jumps */ + contain: layout style; +} + +.masonry-col { + flex: 1 1 0; + min-width: 320px; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 0; /* Remove gap so negative margins overlap */ + transition: none; /* Prevent transitions during layout changes */ + will-change: auto; /* Prevent unnecessary layer promotion */ + /* Prevent reflow during resize */ + contain: layout; +} + +@media (max-width: 799px) { + #posts { + flex-direction: column; + gap: 0; + transition: none; /* Prevent transitions during layout changes */ + /* Remove the min-height that was causing issues */ + min-height: auto; + /* Use layout containment to minimize reflow */ + contain: layout style size; + } + .masonry-col { + min-width: 0; + width: 100%; + /* Prevent content reflow during breakpoint change */ + contain: layout style; + /* Hide overflow columns on mobile */ + } + .masonry-col:not(:first-child) { + display: none; + } } .post { - background: white; - padding: 1.5rem; - margin-bottom: 2rem; - border: 2px solid #000; + background: var(--post-bg); + padding: 2rem 1.5rem; + margin-bottom: -0.2rem; + margin-top: -0.2rem; + margin-left: -40px; + margin-right: -40px; + border: 2px solid var(--post-border); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1), 6px 6px 0 rgba(0, 0, 0, 0.05); + box-shadow: 3px 3px 0 var(--shadow-light), 6px 6px 0 var(--shadow-mid); transition: transform 0.35s cubic-bezier(0.68, -0.55, 0.27, 1.55), - box-shadow 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); + box-shadow 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), + opacity 0.2s ease; + display: inline-block; + width: 100%; + min-width: 320px; + break-inside: avoid; + z-index: 1; + + /* Dynamic rotation based on height */ + --post-height: 300px; /* Default fallback */ + --min-rotation: 0.5deg; /* Minimum rotation for tall posts */ + --max-rotation: 3deg; /* Maximum rotation for short posts */ + --rotation-factor: clamp(var(--min-rotation), calc(var(--max-rotation) - (var(--post-height) - 200px) * 0.005), var(--max-rotation)); + + /* Apply rotation with random direction */ + transform: rotate(var(--rotation-factor)); +} + +/* Alternate rotation directions for visual variety */ +.post:nth-child(odd) { + transform: rotate(calc(-1 * var(--rotation-factor))); +} + +.post:nth-child(even) { + transform: rotate(var(--rotation-factor)); } .post-header { @@ -82,14 +222,14 @@ h1 { .avatar { width: 48px; height: 48px; - background: #f0f0f0; - border: 2px solid black; + background: var(--avatar-bg); + border: 2px solid var(--avatar-border); display: flex; align-items: center; justify-content: center; font-weight: bold; transform: rotate(-2deg); - box-shadow: 2px 2px 0 #000; + box-shadow: 2px 2px 0 var(--avatar-border); } .post-content { @@ -98,11 +238,11 @@ h1 { .image-placeholder { margin-top: 1rem; - border: 4px solid black; - background: #f0f0f0; + border: 4px solid var(--post-border); + background: var(--avatar-bg); text-align: center; transform: rotate(1deg); - box-shadow: 4px 4px 0 #000; + box-shadow: 4px 4px 0 var(--post-border); } .post-actions { @@ -122,12 +262,12 @@ h1 { } .action-btn.like { - color: #db2777; + color: var(--like); transform: rotate(-1deg); } .action-btn.repost { - color: #2563eb; + color: var(--accent); transform: rotate(1deg); } @@ -137,51 +277,16 @@ h1 { display: block; } - .post:hover { - transform: scale(1.1) translateY(-5px) !important; - box-shadow: 6px 6px 30px rgba(0, 0, 0, 1); + transform: scale(1.1) translateY(-5px) rotate(0deg) !important; + box-shadow: 6px 6px 30px var(--shadow-heavy); + z-index: 10; } .subtext { margin-bottom: 30px; } -.social-links a { - margin: 0 10px; - color: #f0f0f0; - font-size: 2rem; - text-decoration: none; - display: inline-block; -} - -.social-links a:hover { - transform: scale(1.1); - transition: transform 0.3s ease; - text-shadow: - 0 0 3px white, -} - -.social-links a:hover .fab.fa-github { - color: #181717; -} - -.social-links a:hover .fab.fa-youtube { - color: #FF0000; -} - -.social-links a:hover .fab.fa-steam { - color: #1b2838; -} - -.social-links a:hover .fab.fa-discord { - color: #7289da; -} - -.social-links a:hover .fa-brands.fa-bluesky { - color: #3E5BFF; -} - @keyframes shake { 0%, @@ -209,11 +314,11 @@ h1:hover { shake 0.5s ease-in-out infinite; } -.social-links a { +social-links a { position: relative; } -.social-links a::after { +social-links a::after { content: attr(title); position: absolute; bottom: 100%; @@ -229,7 +334,7 @@ h1:hover { white-space: nowrap; } -.social-links a:hover::after { +social-links a:hover::after { opacity: 1; } @@ -242,8 +347,8 @@ h1:hover { display: inline-block; margin: 0.5rem 0; padding: 0.5rem 1rem; - background: white; - color: #242424; + background: var(--post-bg); + color: var(--main-bg); text-decoration: none; font-weight: bold; border-radius: 20px; @@ -251,8 +356,8 @@ h1:hover { } .project-links a:hover { - background: #2563eb; - color: white; + background: var(--accent); + color: var(--post-bg); } footer { @@ -260,14 +365,15 @@ footer { margin-top: 3rem; margin-bottom: 2rem; font-size: 0.9rem; + color: var(--main-fg); } footer a { - color: #f0f0f0; + color: var(--main-fg); text-decoration: underline; } footer a:hover { - color: #2563eb; + color: var(--accent); } diff --git a/website/viewport-scroll-fix.js b/website/viewport-scroll-fix.js new file mode 100644 index 0000000..4a8ad38 --- /dev/null +++ b/website/viewport-scroll-fix.js @@ -0,0 +1,50 @@ +(function() { + let scrollPosition = 0; + let isResizing = false; + let resizeTimeout; + + function preserveScroll() { + if (!isResizing) { + scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + isResizing = true; + + // Temporarily disable smooth scrolling during resize + document.documentElement.style.scrollBehavior = 'auto'; + } + + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + // Force restore scroll position multiple times to combat layout changes + const restoreScroll = () => { + window.scrollTo(0, scrollPosition); + }; + + restoreScroll(); + requestAnimationFrame(restoreScroll); + setTimeout(restoreScroll, 10); + setTimeout(restoreScroll, 50); + + setTimeout(() => { + // Re-enable smooth scrolling + document.documentElement.style.scrollBehavior = 'smooth'; + isResizing = false; + }, 100); + }, 100); + } + + // Listen for resize events + window.addEventListener('resize', preserveScroll); + + // Listen for orientation changes + window.addEventListener('orientationchange', () => { + preserveScroll(); + // Additional delay for orientation changes + setTimeout(() => { + window.scrollTo(0, scrollPosition); + }, 500); + }); + + // Also intercept media query changes that affect layout + const mediaQuery = window.matchMedia('(max-width: 799px)'); + mediaQuery.addListener(preserveScroll); +})();