lorenzoiovino.com/src/pages/blog.astro

351 lines
11 KiB
Text

---
import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
import { Image } from "astro:assets";
import Footer from "../components/Footer.astro";
import Navbar from "../components/Navbar.astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import type { TagCount } from "../types/i-blog";
type BlogPost = CollectionEntry<"blog">;
const allPosts: BlogPost[] = await getCollection("blog");
const sortedPosts: BlogPost[] = allPosts
.filter((post: BlogPost) => !post.data.draft)
.sort((a: BlogPost, b: BlogPost) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const allTags: Record<string, number> = sortedPosts.reduce(
(acc: Record<string, number>, post: BlogPost) => {
post.data.tags.forEach((tag: string) => {
acc[tag] = (acc[tag] || 0) + 1;
});
return acc;
},
{} as Record<string, number>,
);
const sortedTags: TagCount[] = Object.entries(allTags)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.map(([tag, count]: [string, number]): TagCount => ({ tag, count }));
const initialPosts: BlogPost[] = sortedPosts.slice(0, 6);
---
<BaseLayout
title="Lorenzo Iovino >> Blog"
description="Blog posts and articles by Lorenzo Iovino"
>
<Navbar />
<div class="min-h-screen pt-16 bg-secondary">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-20">
<div class="mb-16 text-center">
<h1 class="text-white mb-4">Blog</h1>
<p class="text-xl text-white/90 max-w-2xl mx-auto">
Thoughts, experiences, and insights about software engineering, technology, and life
</p>
<div class="h-1 w-20 bg-white rounded-full mx-auto mt-6"></div>
</div>
<div class="mb-12">
<div class="bg-white rounded-2xl shadow-soft-lg overflow-hidden">
<button
id="toggle-tags"
class="w-full p-6 flex items-center justify-between hover:bg-gray-50 transition-colors duration-200"
>
<div class="flex items-center gap-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-secondary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
></path>
</svg>
<h2 class="text-xl font-bold text-gray-900">Filter by Topic</h2>
</div>
<svg
id="chevron-icon"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-400 transition-transform duration-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="tags-container" class="hidden px-6 pb-6">
<div class="flex flex-wrap gap-2">
<a
href="/blog"
class="inline-block px-4 py-2 bg-secondary text-white rounded-lg font-medium transition-all duration-200 hover:bg-secondary/90"
>
All Posts ({sortedPosts.length})
</a>
{
sortedTags.map(({ tag, count }: TagCount) => (
<a
href={`/blog/tag/${tag}`}
class="inline-block px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium transition-all duration-200 hover:bg-secondary hover:text-white"
>
{tag} ({count})
</a>
))
}
</div>
</div>
</div>
</div>
<div id="posts-container" class="space-y-8">
{
initialPosts.map((post: BlogPost) => (
<article class="bg-white rounded-2xl shadow-soft-lg overflow-hidden hover:shadow-soft-lg transition-shadow duration-300">
<div class="block">
{post.data.heroImage && (
<a href={`/blog/${post.slug}`} class="block">
<div class="relative h-64 overflow-hidden">
<Image
src={post.data.heroImage}
alt={post.data.title}
class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
width={1200}
height={630}
quality={70}
format="webp"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
</div>
</a>
)}
<div class="p-8">
<div class="flex items-center gap-3 mb-4 text-sm text-gray-500">
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
{post.data.tags.length > 0 && (
<>
<span>•</span>
<div class="flex gap-2 flex-wrap">
{post.data.tags.slice(0, 3).map((tag: string) => (
<a
href={`/blog/tag/${tag}`}
class="px-2 py-1 bg-secondary/10 text-secondary text-xs rounded-lg font-medium hover:bg-secondary hover:text-white transition-all duration-200"
>
{tag}
</a>
))}
</div>
</>
)}
</div>
<a href={`/blog/${post.slug}`} class="block group">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 mb-3 group-hover:text-secondary transition-colors">
{post.data.title}
</h2>
<p class="text-gray-700 text-lg leading-relaxed mb-4">
{post.data.description}
</p>
<div class="flex items-center text-secondary font-medium">
Read more
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 ml-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</div>
</a>
</div>
</div>
</article>
))
}
</div>
<div id="loading" class="hidden text-center py-8">
<div
class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"
>
</div>
<p class="text-white mt-4">Loading more posts...</p>
</div>
<div id="end-message" class="hidden text-center py-8">
<p class="text-white/80 text-lg">You've reached the end! 🎉</p>
</div>
</div>
</div>
<Footer />
</BaseLayout>
<script define:vars={{ allPosts: sortedPosts }}>
let currentIndex = 6;
const postsPerLoad = 3;
let isLoading = false;
const postsContainer = document.getElementById("posts-container");
const loading = document.getElementById("loading");
const endMessage = document.getElementById("end-message");
function createPostElement(post) {
const article = document.createElement("article");
article.className =
"bg-white rounded-2xl shadow-soft-lg overflow-hidden hover:shadow-soft-lg transition-shadow duration-300";
const heroImageHTML = post.data.heroImage
? `
<a href="/blog/${post.slug}" class="block">
<div class="relative h-64 overflow-hidden">
<img
src="${post.data.heroImage}"
alt="${post.data.title}"
class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
</div>
</a>
`
: "";
const tagsHTML =
post.data.tags.length > 0
? `
<span>•</span>
<div class="flex gap-2 flex-wrap">
${post.data.tags
.slice(0, 3)
.map(
(tag) =>
`<a href="/blog/tag/${tag}" class="px-2 py-1 bg-secondary/10 text-secondary text-xs rounded-lg font-medium hover:bg-secondary hover:text-white transition-all duration-200">${tag}</a>`
)
.join("")}
</div>
`
: "";
const pubDate = new Date(post.data.pubDate);
const formattedDate = pubDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
article.innerHTML = `
<div class="block">
${heroImageHTML}
<div class="p-8">
<div class="flex items-center gap-3 mb-4 text-sm text-gray-500">
<time datetime="${pubDate.toISOString()}">
${formattedDate}
</time>
${tagsHTML}
</div>
<a href="/blog/${post.slug}" class="block group">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 mb-3 group-hover:text-secondary transition-colors">
${post.data.title}
</h2>
<p class="text-gray-700 text-lg leading-relaxed mb-4">
${post.data.description}
</p>
<div class="flex items-center text-secondary font-medium">
Read more
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 ml-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</div>
</a>
</div>
</div>
`;
return article;
}
function loadMorePosts() {
if (isLoading || currentIndex >= allPosts.length) return;
isLoading = true;
loading.classList.remove("hidden");
setTimeout(() => {
const nextPosts = allPosts.slice(currentIndex, currentIndex + postsPerLoad);
nextPosts.forEach((post) => {
const postElement = createPostElement(post);
postsContainer.appendChild(postElement);
});
currentIndex += postsPerLoad;
loading.classList.add("hidden");
isLoading = false;
if (currentIndex >= allPosts.length) {
endMessage.classList.remove("hidden");
}
}, 800);
}
const toggleTagsBtn = document.getElementById("toggle-tags");
const tagsContainer = document.getElementById("tags-container");
const chevronIcon = document.getElementById("chevron-icon");
toggleTagsBtn?.addEventListener("click", () => {
const isHidden = tagsContainer.classList.contains("hidden");
if (isHidden) {
tagsContainer.classList.remove("hidden");
chevronIcon.style.transform = "rotate(180deg)";
} else {
tagsContainer.classList.add("hidden");
chevronIcon.style.transform = "rotate(0deg)";
}
});
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading && currentIndex < allPosts.length) {
loadMorePosts();
}
},
{
rootMargin: "200px",
}
);
if (loading) {
observer.observe(loading);
}
if (endMessage) {
observer.observe(endMessage);
}
</script>