diff --git a/public/s/css/f0ck.css b/public/s/css/f0ck.css index 0cc97d5..a845fb3 100644 --- a/public/s/css/f0ck.css +++ b/public/s/css/f0ck.css @@ -1197,7 +1197,9 @@ body { overscroll-behavior-y: contain; overflow: unset; font-size: 14px; - height: /* 100%; */auto; + height: + /* 100%; */ + auto; } .wrapper { @@ -1318,6 +1320,13 @@ div.posts>a:hover::after { grid-template-columns: auto 1fr 0fr; justify-content: start; border-bottom: 1px solid var(--nav-border-color); + background: var(--nav-bg); + transition: background 0.2s ease; +} + +.navbar.scrolled { + background: #000 !important; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); } .navbar-brand { @@ -1424,7 +1433,7 @@ ul.navbar-nav li.nav-item { } .nav-link[data-toggle="dropdown"].ddcontent::after { - content: "\00a0("attr(content) ")\00a0\25bc"; + content: "\00a0(" attr(content) ")\00a0\25bc"; } .nav-link[data-toggle="dropdown"]:not(.ddcontent)::after { @@ -1608,7 +1617,7 @@ span.placeholder { } @media (max-width: 1325px) { -/* ranking page - idea */ + /* ranking page - idea */ /* .ranking { grid-template-columns: 1fr 1fr !important; } */ @@ -1783,8 +1792,9 @@ span.placeholder { .index-container { width: 100%; padding: 5px; -/* background-color: var(--navigation-links-bg); - */} + /* background-color: var(--navigation-links-bg); + */ +} @media (min-width: 361px) { .embed-responsive-image { @@ -2779,7 +2789,9 @@ ul.navbar-nav-guests li.nav-item { margin-right: .5rem; } -.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span { +.navbar-expand-lg .navbar-nav-guests .nav-link, +.pagination>a, +.pagination>span { padding-right: .5rem; padding-left: .5rem; } @@ -2792,7 +2804,7 @@ ul.navbar-nav-guests li.nav-item { margin: 0; padding: 5px; } - + ul.navbar-nav-guests li.nav-item { margin-right: unset; } @@ -2812,16 +2824,20 @@ ul.navbar-nav-guests li.nav-item { /* Pagination Responsiveness */ @media (max-width: 799px) { - .navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span { + + .navbar-expand-lg .navbar-nav-guests .nav-link, + .pagination>a, + .pagination>span { padding-right: 2px; padding-left: 2px; } - - .pagination > a, .pagination > span { + + .pagination>a, + .pagination>span { margin-right: 2px; margin-left: 2px; } - + .pagination { justify-content: center !important; } @@ -2830,15 +2846,19 @@ ul.navbar-nav-guests li.nav-item { /* fadeIn effect */ @keyframes fadeIn { 0% { - opacity: 0; + opacity: 0; } + 100% { - opacity: 1; + opacity: 1; } } -img#f0ck-image, div.imageDoor, div.posts a, video { - animation: 1s ease-out 0s 1 fadeInFX; +img#f0ck-image, +div.imageDoor, +div.posts a, +video { + animation: 1s ease-out 0s 1 fadeInFX; } /* f0ckgle */ @@ -2867,7 +2887,7 @@ img#f0ck-image, div.imageDoor, div.posts a, video { background: var(--nav-bg); padding: 5px; } - + .profile_head_avatar { margin: 0; } @@ -2892,11 +2912,13 @@ img#f0ck-image, div.imageDoor, div.posts a, video { padding: 5px; } -.f0cks h5, .favs h5 { +.f0cks h5, +.favs h5 { background: var(--dropdown-bg); } -.f0cks-header, .favs-header { +.f0cks-header, +.favs-header { display: grid; grid-template-columns: 1fr auto; background: var(--img-border-color); @@ -2940,46 +2962,50 @@ button#togglebg { 0% { opacity: 0; } -100% { + + 100% { opacity: 1; - } + } } @keyframes fadeOutFX { 0% { opacity: 1; } -100% { + + 100% { opacity: 0; - } + } } @keyframes fadeIn { 0% { - opacity: 0; - } + opacity: 0; + } + 100% { - opacity: 0.2; - } + opacity: 0.2; + } } @keyframes fadeOut { - 0% { - opacity: 0.2; - } - 100% { - opacity: 0; - } + 0% { + opacity: 0.2; + } + + 100% { + opacity: 0; + } } .fader-in { - animation: fadeIn .8s steps(100) forwards; + animation: fadeIn .8s steps(100) forwards; } .fader-out { - animation: fadeOut .8s steps(100) forwards -} + animation: fadeOut .8s steps(100) forwards +} .settings { display: grid; @@ -3004,6 +3030,7 @@ input#s_avatar { #s_avatar:hover { background: #ffffff0f; } + .theforceofthree { display: grid; grid-template-columns: 0.4fr 1fr 0.4fr; @@ -3011,4 +3038,24 @@ input#s_avatar { .upload { padding: 10px; +} + +/* Infinite scroll loading indicator */ +.loading-spinner { + display: inline-block; + font-size: 12px; + color: var(--footbar-color); + animation: pulse 1s ease-in-out infinite; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 0.4; + } + + 50% { + opacity: 1; + } } \ No newline at end of file diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js index 8deb7b2..bc62ee0 100644 --- a/public/s/js/f0ck.js +++ b/public/s/js/f0ck.js @@ -68,6 +68,18 @@ window.requestAnimFrame = (function () { } }; + // Navbar scroll effect - make background black when scrolling + const navbar = document.querySelector('.navbar'); + if (navbar) { + window.addEventListener('scroll', () => { + if (window.scrollY > 50) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + }); + } + const loadItemAjax = async (url, inheritContext = true) => { console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext); // Show loading indicator @@ -335,15 +347,109 @@ window.requestAnimFrame = (function () { } // - // - let tts = 0; - const scroll_treshold = 1; - if ([...document.querySelectorAll("div.posts")].length === 1) { + // + const postsContainer = document.querySelector("div.posts"); + if (postsContainer) { + // Infinite scroll state + let infiniteState = { + loading: false, + hasMore: true, + currentPage: 1 + }; + + // Extract current page from URL + const pageMatch = window.location.pathname.match(/\/p\/(\d+)/); + if (pageMatch) infiniteState.currentPage = parseInt(pageMatch[1]); + + // Extract context (tag/user/mime) from URL + const getContext = () => { + const ctx = {}; + const tagMatch = window.location.pathname.match(/\/tag\/([^/]+)/); + if (tagMatch) ctx.tag = decodeURIComponent(tagMatch[1]); + const userMatch = window.location.pathname.match(/\/user\/([^/]+)/); + if (userMatch) ctx.user = decodeURIComponent(userMatch[1]); + const mimeMatch = window.location.pathname.match(/\/(image|audio|video)(?:\/|$)/); + if (mimeMatch) ctx.mime = mimeMatch[1]; + return ctx; + }; + + // Build URL path for history + const buildUrl = (page) => { + const ctx = getContext(); + let path = '/'; + if (ctx.tag) path += `tag/${ctx.tag}/`; + if (ctx.user) path += `user/${ctx.user}/`; + if (ctx.mime) path += `${ctx.mime}/`; + if (page > 1) path += `p/${page}`; + return path.replace(/\/$/, '') || '/'; + }; + + // Fetch and append more items + const loadMoreItems = async () => { + if (infiniteState.loading || !infiniteState.hasMore) return; + + infiniteState.loading = true; + const foot = document.querySelector("div#footbar"); + if (foot) { + foot.innerHTML = 'Loading...'; + foot.style.color = 'var(--footbar-color)'; + } + + const nextPage = infiniteState.currentPage + 1; + const ctx = getContext(); + + const params = new URLSearchParams(); + params.append('page', nextPage); + if (ctx.tag) params.append('tag', ctx.tag); + if (ctx.user) params.append('user', ctx.user); + if (ctx.mime) params.append('mime', ctx.mime); + + try { + const response = await fetch(`/ajax/items?${params.toString()}`); + const data = await response.json(); + + if (data.success && data.html) { + // Append new thumbnails + postsContainer.insertAdjacentHTML('beforeend', data.html); + + // Update state + infiniteState.currentPage = data.currentPage; + infiniteState.hasMore = data.hasMore; + + // Update URL + history.replaceState({}, '', buildUrl(infiniteState.currentPage)); + + // Update pagination display if exists + const paginationLinks = document.querySelectorAll('.pagination .pagination-int-item, .pagination .btn'); + paginationLinks.forEach(el => { + if (el.textContent.trim() == infiniteState.currentPage) { + el.classList.add('disabled'); + } + }); + } else { + infiniteState.hasMore = false; + } + } catch (err) { + console.error('Infinite scroll fetch error:', err); + } finally { + infiniteState.loading = false; + if (foot) { + foot.innerHTML = infiniteState.hasMore ? '▼' : '—'; + foot.style.color = 'transparent'; + } + } + }; + + // Scroll detection + let tts = 0; + const scroll_treshold = 1; + document.addEventListener("wheel", e => { if (!document.querySelector('#main')) return; - if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down - if (elem = document.querySelector(".pagination > .next:not(.disabled)")) { + if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { + // Scrolling down at bottom + if (infiniteState.hasMore && !infiniteState.loading) { if (tts < scroll_treshold) { const foot = document.querySelector("div#footbar"); if (foot) { @@ -351,43 +457,25 @@ window.requestAnimFrame = (function () { foot.style.color = "var(--footbar-color)"; } tts++; + } else { + loadMoreItems(); + tts = 0; } - else - changePage(elem); } - } - else if (window.scrollY <= 0 && e.deltaY < 0) { // up - if (elem = document.querySelector(".pagination > .prev:not(.disabled)")) { - if (tts < scroll_treshold) { - const nav = document.querySelector("nav.navbar"); - if (nav) { - nav.style.boxShadow = "0px 2px 0px var(--loading-indicator-color)"; - nav.style.transition = ".2s ease-in-out"; - } - tts++; - } - else - changePage(elem); - } - } - else { + } else if (window.scrollY <= 0 && e.deltaY < 0) { + // Scrolling up at top - could load previous page if needed (optional) + tts = 0; + } else { tts = 0; const foot = document.querySelector("div#footbar"); if (foot) { foot.style.boxShadow = "unset"; foot.style.color = "transparent"; } - const nav = document.querySelector("nav.navbar"); - if (nav) nav.style.boxShadow = "unset"; } }); } - - const rmatch = /\/p\/(\d+?)/; - if (document.referrer.match(rmatch) && document.location.href.match(rmatch)) - if (document.location.href.match(rmatch) < document.referrer.match(rmatch)) - document.body.scrollTop = document.body.scrollHeight; - // + // // const swipeRT = { diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index a933050..cddbdbe 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -64,5 +64,58 @@ export default (router, tpl) => { }); }); + // Infinite scroll endpoint for index thumbnails + router.get(/\/ajax\/items/, async (req, res) => { + let query = {}; + if (typeof req.url === 'string') { + const parsedUrl = url.parse(req.url, true); + query = parsedUrl.query; + } else { + query = req.url.qs || {}; + } + + const page = parseInt(query.page) || 1; + + const data = await f0cklib.getf0cks({ + page: page, + tag: query.tag || null, + user: query.user || null, + mime: query.mime || null, + mode: req.session.mode, + session: !!req.session, + fav: false + }); + + if (!data.success) { + return res.reply({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + success: false, + html: '', + hasMore: false + }) + }); + } + + // Render just the thumbnail items + const itemsHtml = tpl.render('snippets/items-grid', { + items: data.items, + link: data.link + }); + + const hasMore = data.pagination.next !== null; + + return res.reply({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + success: true, + html: itemsHtml, + hasMore: hasMore, + nextPage: data.pagination.next, + currentPage: data.pagination.page + }) + }); + }); + return router; }; diff --git a/views/snippets/items-grid.html b/views/snippets/items-grid.html new file mode 100644 index 0000000..9c69cfd --- /dev/null +++ b/views/snippets/items-grid.html @@ -0,0 +1,7 @@ +@each(items as item) + +

+
+@endeach \ No newline at end of file