diff --git a/assets/css/tailwind.css b/assets/css/tailwind.css index ed99427..54cede7 100644 --- a/assets/css/tailwind.css +++ b/assets/css/tailwind.css @@ -36,6 +36,8 @@ html[data-theme='light'] { --text-tertiary: var(--color-gray-700); } +[x-cloak] { display: none !important; } + @layer base { body { background-color: var(--bg-primary); @@ -66,6 +68,10 @@ html[data-theme='light'] { } } +.kg-image-card img, .post-content img { + @apply cursor-pointer transition-opacity duration-200 hover:opacity-90; +} + @keyframes fadeInUp { from { opacity: 0; diff --git a/assets/js/main.js b/assets/js/main.js index 7eba9c8..b7b0c02 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,216 +1,35 @@ // Main JavaScript file for Palina theme +// Using HTMX and Alpine.js for enhanced functionality + document.addEventListener('DOMContentLoaded', () => { console.log('Palina theme loaded!'); - const htmlElement = document.documentElement; - // Remove 'hidden' class from html to prevent FOUC - htmlElement.classList.remove('hidden'); - - // Staggered fade-in animation for post grid (initial load) - const initialGridItems = document.querySelectorAll('.post-grid-item'); - initialGridItems.forEach((item, index) => { - item.style.animationDelay = `${index * 100}ms`; - item.classList.add('animate-fadeInUp'); - }); - - // Lightbox for post images - const lightbox = document.getElementById('lightbox'); - if (lightbox) { - const lightboxImage = document.getElementById('lightbox-image'); - const lightboxClose = document.getElementById('lightbox-close'); - - const setupLightboxImage = (image) => { - image.style.cursor = 'pointer'; - image.addEventListener('click', () => { - lightbox.classList.remove('hidden'); - lightbox.classList.add('show'); - lightboxImage.src = image.src; - }); - }; - - // Initial setup for existing images - document.querySelectorAll('.kg-image-card img, .post-content img').forEach(setupLightboxImage); - - - lightboxClose.addEventListener('click', () => { - lightbox.classList.add('hidden'); - lightbox.classList.remove('show'); - lightboxImage.src = ''; + // Staggered fade-in animation for post grid + const animateGridItems = (container = document) => { + const gridItems = container.querySelectorAll('.post-grid-item:not(.animated)'); + gridItems.forEach((item, index) => { + item.style.animationDelay = `${index * 100}ms`; + item.classList.add('animate-fadeInUp'); + item.classList.add('animated'); }); - - lightbox.addEventListener('click', (e) => { - if (e.target.id !== 'lightbox-image') { - lightbox.classList.add('hidden'); - lightbox.classList.remove('show'); - lightboxImage.src = ''; - } - }); - } - - // Theme Switcher - const themeToggle = document.getElementById('theme-toggle'); - const sunIcon = document.getElementById('theme-toggle-sun-icon'); - const moonIcon = document.getElementById('theme-toggle-moon-icon'); - - const setThemeIcons = (theme) => { - if (sunIcon && moonIcon) { - if (theme === 'dark') { - sunIcon.classList.remove('hidden'); - moonIcon.classList.add('hidden'); - } else { - sunIcon.classList.add('hidden'); - moonIcon.classList.remove('hidden'); - } - } }; - const currentTheme = localStorage.getItem('theme'); - if (currentTheme) { - htmlElement.setAttribute('data-theme', currentTheme); - setThemeIcons(currentTheme); - } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { - // Default to light theme if system preference is light - htmlElement.setAttribute('data-theme', 'light'); - setThemeIcons('light'); - } else { - // Default to dark theme if no preference - htmlElement.setAttribute('data-theme', 'dark'); - setThemeIcons('dark'); - } + // Initial animation + animateGridItems(); - if (themeToggle) { - themeToggle.addEventListener('click', () => { - let newTheme = htmlElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; - htmlElement.setAttribute('data-theme', newTheme); - localStorage.setItem('theme', newTheme); - setThemeIcons(newTheme); - }); - } - - // Mobile Menu - const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); - const mobileMenuClose = document.getElementById('mobile-menu-close'); - const mobileMenu = document.getElementById('mobile-menu'); - - if (mobileMenuToggle && mobileMenu && mobileMenuClose) { - mobileMenuToggle.addEventListener('click', () => { - mobileMenu.classList.remove('-translate-x-full'); - mobileMenu.classList.add('translate-x-0'); - }); - - mobileMenuClose.addEventListener('click', () => { - mobileMenu.classList.remove('translate-x-0'); - mobileMenu.classList.add('-translate-x-full'); - }); - - // Close menu if a link is clicked - const mobileNavLinks = mobileMenu.querySelectorAll('a'); - mobileNavLinks.forEach(link => { - link.addEventListener('click', () => { - mobileMenu.classList.remove('translate-x-0'); - mobileMenu.classList.add('-translate-x-full'); - }); - }); - } - - // Infinite Scroll - const postsContainer = document.getElementById('posts-container'); - const loadingSpinner = document.getElementById('loading-spinner'); - - if (postsContainer && loadingSpinner) { - let currentPage = 1; - let isLoading = false; - let hasMorePosts = true; - - // Retrieve API key and URL from global GhostConfig object - if (typeof window.GhostConfig === 'undefined' || !window.GhostConfig.ghostApiKey || !window.GhostConfig.ghostApiUrl) { - console.error('Ghost Content API Key or URL not found in window.GhostConfig. Infinite scroll will not work.'); - return; + // Re-run animation and other setup when HTMX settles new content + document.body.addEventListener('htmx:afterSettle', (event) => { + // If the settled element is the posts-container or contains grid items + if (event.detail.target.id === 'posts-container' || event.detail.target.querySelector('.post-grid-item')) { + animateGridItems(event.detail.target); } - const GHOST_API_KEY = window.GhostConfig.ghostApiKey; - const GHOST_API_URL = window.GhostConfig.ghostApiUrl; + // If it was a full page boost, we might need to reset some things + if (event.detail.boosted) { + animateGridItems(); + } + }); - const fetchPosts = async () => { - if (isLoading || !hasMorePosts) return; - - isLoading = true; - loadingSpinner.classList.remove('hidden'); - - currentPage++; - const postsPerPage = 12; // Matches posts_per_page in package.json config - const url = `${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=${postsPerPage}&page=${currentPage}&include=tags,authors`; - - try { - const response = await fetch(url); - const data = await response.json(); - - const newPosts = data.posts; - const pagination = data.meta.pagination; - - if (newPosts.length > 0) { - newPosts.forEach((post, index) => { - const article = document.createElement('article'); - article.className = 'post-grid-item opacity-0 relative bg-[var(--bg-secondary)] rounded-lg shadow-lg overflow-hidden group'; - - // Basic rendering of a post for infinite scroll. - // This should ideally match the structure in index.hbs - // For simplicity, I'm reconstructing it here. - article.innerHTML = ` - - ${post.feature_image ? ` - ${post.title} - ` : ` -
No Image
- `} -
-

${post.title}

-
-
- `; - postsContainer.appendChild(article); - - // Apply staggered animation to newly added posts - // Calculate delay based on total items - const totalItems = postsContainer.children.length; - article.style.animationDelay = `${(totalItems - newPosts.length + index) * 100}ms`; - article.classList.add('animate-fadeInUp'); - - // If lightbox is present, set up new images for lightbox - if (lightbox) { - article.querySelectorAll('.kg-image-card img, .post-content img').forEach(setupLightboxImage); - } - }); - } else { - hasMorePosts = false; - } - - if (pagination && pagination.next === null) { - hasMorePosts = false; - } - - } catch (error) { - console.error('Error fetching posts:', error); - hasMorePosts = false; // Stop trying to fetch if there's an error - } finally { - isLoading = false; - loadingSpinner.classList.add('hidden'); - } - }; - - const observer = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry.isIntersecting && hasMorePosts && !isLoading) { - fetchPosts(); - } - }); - }, { threshold: 0.1 }); // Trigger when 10% of the spinner is visible - - observer.observe(loadingSpinner); - } + // Handle Lightbox for post images specifically (if needed beyond the global Alpine listener) + // The global listener in default.hbs should handle most cases. }); diff --git a/default.hbs b/default.hbs index c97d20d..84be6f6 100644 --- a/default.hbs +++ b/default.hbs @@ -1,16 +1,39 @@ - + {{meta_title}} + + + + {{ghost_head}} - + -
+
{{> header}}
@@ -18,13 +41,18 @@
{{> footer}} -
- {{> mobile-menu}} + {{> mobile-menu}} - {{ghost_foot}} diff --git a/index.hbs b/index.hbs index 1020397..9e5712c 100644 --- a/index.hbs +++ b/index.hbs @@ -9,32 +9,23 @@ {{#if posts}}
{{#foreach posts}} - + {{> "post-card"}} {{/foreach}} + + {{#if pagination.next}} +
+
+ {{/if}}
- diff --git a/partials/post-card.hbs b/partials/post-card.hbs new file mode 100644 index 0000000..def8387 --- /dev/null +++ b/partials/post-card.hbs @@ -0,0 +1,22 @@ +
+ + {{#if feature_image}} + {{title}} + {{else}} +
No Image
+ {{/if}} +
+

{{title}}

+
+
+