apply prettier formatting
Some checks failed
renovate / renovate (push) Has been cancelled

This commit is contained in:
2025-06-08 16:45:36 -05:00
parent 67f12ecf72
commit 51041f6ae9
32 changed files with 3303 additions and 2509 deletions

View File

@@ -1,97 +1,144 @@
---
import BlogPost from '../../layouts/BlogPost.astro';
import directus from "../../../lib/directus"
import { readItems } from "@directus/sdk";
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", {
fields: ['*'],
}));
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
})
);
const sortedEntries = [...posts].sort(
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
);
return sortedEntries.map((post, index) => {
return {
params: { slug: post.slug },
props: {
props: {
post,
nextPost: index > 0 ? sortedEntries[index - 1] : null,
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
},
};
});
}
const { post, nextPost, prevPost } = Astro.props;
---
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}>
<!-- Main Content - Enhanced with better typography and spacing -->
<div class="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:text-zinc-800 dark:prose-a:text-zinc-300 prose-a:font-medium prose-a:underline-offset-4 hover:prose-a:text-zinc-600 dark:hover:prose-a:text-zinc-100 prose-img:rounded-xl sm:prose-base prose-sm">
<div set:html={post.content} />
</div>
<BlogPost
slug={post.slug}
title={post.title}
description={post.description}
content={post.content}
image={post.image}
image_alt={post.image_alt}
published_date={post.published_date}
updated_date={post.updated_date}
tags={post.tags}
>
<!-- Main Content - Enhanced with better typography and spacing -->
<div
class="prose prose-sm prose-zinc max-w-none dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100"
>
<div set:html={post.content} />
</div>
<!-- Next/Previous Navigation - Improved responsive design -->
<div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12">
{prevPost && (
<a
href={`/blog/${prevPost.slug}`}
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
<!-- Next/Previous Navigation - Improved responsive design -->
<div
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2"
>
{
prevPost && (
<a
href={`/blog/${prevPost.slug}`}
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6"
>
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1">
<path d="m15 18-6-6 6-6"></path>
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
>
<path d="m15 18-6-6 6-6" />
</svg>
Previous Article
</span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{prevPost.title}
</h3>
</a>
)}
{nextPost && (
<a
href={`/blog/${nextPost.slug}`}
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 md:text-right overflow-hidden"
)
}
{
nextPost && (
<a
href={`/blog/${nextPost.slug}`}
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right"
>
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end">
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
Next Article
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1">
<path d="m9 18 6-6-6-6"></path>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
>
<path d="m9 18 6-6-6-6" />
</svg>
</span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{nextPost.title}
</h3>
</a>
)}
</div>
)
}
</div>
</BlogPost>
<script>
// Removing TOC-related functions
// Add copy buttons to code blocks
function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach(block => {
codeBlocks.forEach((block) => {
// Skip if already processed by either method
if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) {
if (
block.classList.contains('code-block-processed') ||
block.classList.contains('enhanced')
) {
return;
}
block.classList.add('code-block-processed');
// Create wrapper if not already wrapped
let wrapper;
if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) {
if (
block.parentNode.classList.contains('relative') &&
block.parentNode.classList.contains('group')
) {
wrapper = block.parentNode;
} else {
wrapper = document.createElement('div');
@@ -99,28 +146,29 @@ const { post, nextPost, prevPost } = Astro.props;
block.parentNode.insertBefore(wrapper, block);
wrapper.appendChild(block);
}
// Add copy button if not already present
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
const copyButton = document.createElement('button');
copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
copyButton.className =
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
`;
copyButton.addEventListener('click', () => {
const code = block.querySelector('code').innerText;
navigator.clipboard.writeText(code);
// Show copied feedback
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
`;
setTimeout(() => {
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
@@ -129,39 +177,39 @@ const { post, nextPost, prevPost } = Astro.props;
`;
}, 2000);
});
wrapper.appendChild(copyButton);
}
});
}
// Handle SPA transitions for blog post navigation
function setupSPATransitions() {
// Handle prev/next navigation links
document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
@@ -173,13 +221,13 @@ const { post, nextPost, prevPost } = Astro.props;
});
});
}
// Main initialization function
function initializeBlogPost() {
// Initialize remaining components
initializeCodeCopyButtons();
setupSPATransitions();
// Scroll to hash if present in URL
if (window.location.hash) {
setTimeout(() => {
@@ -190,100 +238,105 @@ const { post, nextPost, prevPost } = Astro.props;
}, 100);
}
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', initializeBlogPost);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', initializeBlogPost);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', initializeBlogPost);
</script>
<style>
/* Removing TOC-related styles */
/* Language badge styling */
.language-badge {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
text-transform: lowercase;
letter-spacing: 0.05em;
}
/* Extra small screens */
@media (min-width: 480px) {
.xs\:inline {
display: inline;
}
.xs\:hidden {
display: none;
}
}
/* Enhanced typography for blog content - Responsive adjustments */
.prose {
@apply text-zinc-800 dark:text-zinc-200;
}
.prose h1, .prose h2, .prose h3, .prose h4 {
@apply text-zinc-900 dark:text-zinc-100 font-semibold;
.prose h1,
.prose h2,
.prose h3,
.prose h4 {
@apply font-semibold text-zinc-900 dark:text-zinc-100;
}
.prose h1 {
@apply text-2xl sm:text-3xl md:text-4xl;
}
.prose h2 {
@apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800;
@apply mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
}
.prose h3 {
@apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3;
@apply mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
}
.prose p {
@apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base;
@apply mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
}
.prose a {
@apply text-zinc-800 dark:text-zinc-300 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-100 hover:decoration-zinc-600 dark:hover:decoration-zinc-400 transition-colors;
@apply font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
}
.prose blockquote {
@apply border-l-4 border-zinc-300 dark:border-zinc-700 pl-4 italic text-zinc-700 dark:text-zinc-300 my-4 sm:my-6;
@apply my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
}
.prose code {
@apply bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-800 dark:text-zinc-200 text-sm font-medium;
@apply rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
}
.prose pre {
@apply bg-[#1e293b] dark:bg-[#1e293b] text-zinc-200 p-3 sm:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm my-4 sm:my-6 shadow-md !important;
@apply my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
}
.prose pre code {
@apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
}
.prose img {
@apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto;
@apply mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
}
.prose ul, .prose ol {
@apply my-4 sm:my-6 pl-5 sm:pl-6;
.prose ul,
.prose ol {
@apply my-4 pl-5 sm:my-6 sm:pl-6;
}
.prose li {
@apply mb-1 sm:mb-2 text-sm sm:text-base;
@apply mb-1 text-sm sm:mb-2 sm:text-base;
}
.prose hr {
@apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800;
@apply my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
}
/* Line clamp for truncating text */
.line-clamp-2 {
display: -webkit-box;
@@ -291,4 +344,4 @@ const { post, nextPost, prevPost } = Astro.props;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</style>

View File

@@ -1,19 +1,17 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from "../../../lib/directus"
import { readItems } from "@directus/sdk";
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
const posts = await directus.request(
readItems("posts", {
readItems('posts', {
fields: ['*'],
sort: ["-published_date"],
sort: ['-published_date'],
})
);
const sortedPosts = posts.sort(
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
);
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
// Group posts by year for timeline effect
const postsByYear = sortedPosts.reduce((acc, post) => {
@@ -29,176 +27,206 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
const totalPosts = sortedPosts.length;
// Get unique tags for search suggestions
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
---
<BaseLayout title="Blog">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16">
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
<!-- Header with search -->
<div class="relative mb-12 sm:mb-20">
<!-- Decorative elements -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
<div
class="animate-blob absolute -left-10 -top-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-72 sm:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-72 sm:w-72"
>
</div>
<div class="relative text-center">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4">
<h1
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl"
>
Blog
</h1>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto">
<p
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:mb-10 sm:text-base"
>
Thoughts, ideas, and explorations on technology and selfhosting.
</p>
</div>
</div>
<!-- Grid layout for mobile experience -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8">
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
<!-- Featured post (if exists) -->
{sortedPosts.length > 0 && (
<div class="md:col-span-12 mb-8 sm:mb-12">
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8">
<div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8">
{sortedPosts[0].image && (
<div class="w-full md:w-1/2 h-60 sm:h-80 md:h-96 overflow-hidden mx-auto md:mx-0 max-w-full sm:max-w-md">
<img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`}
alt={sortedPosts[0].title}
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
loading="eager"
/>
</div>
)}
<div class="flex-1 flex flex-col justify-center">
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-2 mb-3 justify-center md:justify-start">
<span class="font-medium uppercase tracking-wider">Featured</span>
<span class="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span>
{sortedPosts[0].published_date && (
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
{sortedPosts[0].published_date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
)}
</div>
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
<a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0">
{sortedPosts[0].title}
</a>
</h2>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 sm:mb-6 line-clamp-3 text-center md:text-left">
{sortedPosts[0].description}
</p>
<!-- Improved mobile layout for featured post metadata -->
<div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">
{sortedPosts[0].tags && (
<div class="flex flex-wrap gap-2 justify-center md:justify-start">
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
{tag}
</span>
))}
</div>
)}
</div>
</div>
</div>
</article>
</div>
)}
<!-- Improved sidebar for mobile -->
<div class="md:col-span-3 relative">
<div class="md:sticky md:top-24 space-y-4 mb-8 md:mb-0">
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-4 uppercase tracking-wider text-center md:text-left">Archive</h3>
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
<div class="flex md:flex-col overflow-x-auto md:overflow-visible pb-4 md:pb-0 hide-scrollbar">
{years.map((year, index) => (
<a
href={`#year-${year}`}
class={`flex items-center py-2 md:py-3 px-4 md:px-0 mr-3 md:mr-0 border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors md:w-full whitespace-nowrap md:whitespace-normal rounded-full md:rounded-none ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
>
<span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span>
<span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span>
</a>
))}
</div>
</div>
</div>
<!-- Improved post grid for mobile -->
<div class="md:col-span-9">
{years.map((year) => (
<div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16">
<h2 class="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 border-b border-zinc-200 dark:border-zinc-800 pb-3 sm:pb-4 text-center md:text-left">
{year}
</h2>
<div class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}>
{postsByYear[year].map((post, index) => (
<article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md">
{post.image && (
<div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg">
<img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
alt={post.title}
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
loading="lazy"
/>
</div>
)}
<div class="flex-1 flex flex-col">
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center md:justify-start flex-wrap">
{post.pubDate && (
<time datetime={post.published_date.toLocaleString()} class="flex items-center">
{post.published_date.toLocaleString('en-US', {
month: 'short',
day: 'numeric'
})}
</time>
)}
</div>
<h3 class="text-lg sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title}
</a>
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left">
{post.description}
</p>
{post.tags && (
<div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start">
{post.tags.slice(0, 2).map((tag) => (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
{
sortedPosts.length > 0 && (
<div class="mb-8 sm:mb-12 md:col-span-12">
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 dark:border-zinc-800 sm:pb-8">
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
{sortedPosts[0].image && (
<div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
alt={sortedPosts[0].title}
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="eager"
/>
</div>
)}
<div class="flex flex-1 flex-col justify-center">
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 sm:text-sm md:justify-start">
<span class="font-medium uppercase tracking-wider">Featured</span>
<span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
{sortedPosts[0].published_date && (
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
{sortedPosts[0].published_date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
)}
</div>
<h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-4 sm:text-3xl md:text-left">
<a
href={`/blog/${sortedPosts[0].slug}/`}
class="before:absolute before:inset-0"
>
{sortedPosts[0].title}
</a>
</h2>
<p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mb-6 sm:text-base md:text-left">
{sortedPosts[0].description}
</p>
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
{sortedPosts[0].tags && (
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag}
</span>
))}
{post.tags.length > 2 && (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
+{post.tags.length - 2}
</span>
)}
</div>
)}
</div>
</article>
))}
</div>
</div>
</div>
</article>
</div>
))}
)
}
<!-- Improved sidebar for mobile -->
<div class="relative md:col-span-3">
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
<h3
class="mb-4 text-center text-lg font-medium uppercase tracking-wider text-zinc-900 dark:text-zinc-100 md:text-left"
>
Archive
</h3>
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
<div
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
>
{
years.map((year, index) => (
<a
href={`#year-${year}`}
class={`mr-3 flex items-center whitespace-nowrap rounded-full border-b border-zinc-100 px-4 py-2 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900 md:mr-0 md:w-full md:whitespace-normal md:rounded-none md:px-0 md:py-3 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
>
<span class="text-base font-medium text-zinc-900 dark:text-zinc-100 md:text-lg">
{year}
</span>
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 md:ml-auto md:text-sm">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span>
</a>
))
}
</div>
</div>
</div>
<!-- Improved post grid for mobile -->
<div class="md:col-span-9">
{
years.map((year) => (
<div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 dark:border-zinc-800 dark:text-zinc-100 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left">
{year}
</h2>
<div
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
>
{postsByYear[year].map((post, index) => (
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
{post.image && (
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title}
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="lazy"
/>
</div>
)}
<div class="flex flex-1 flex-col">
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start">
{post.pubDate && (
<time
datetime={post.published_date.toLocaleString()}
class="flex items-center"
>
{post.published_date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
})}
</time>
)}
</div>
<h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-xl md:text-left">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title}
</a>
</h3>
<p class="mb-4 line-clamp-2 flex-grow text-center text-sm text-zinc-600 dark:text-zinc-400 md:text-left">
{post.description}
</p>
{post.tags && (
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
{post.tags.slice(0, 2).map((tag) => (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag}
</span>
))}
{post.tags.length > 2 && (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
+{post.tags.length - 2}
</span>
)}
</div>
)}
</div>
</article>
))}
</div>
</div>
))
}
</div>
</div>
</div>
@@ -209,13 +237,14 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
.animate-blob {
animation: blob-bounce 8s infinite ease;
}
.animation-delay-2000 {
animation-delay: 2s;
}
@keyframes blob-bounce {
0%, 100% {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
@@ -228,36 +257,37 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
transform: translate(-5%, 5%) scale(0.95);
}
}
/* Search container hover effect */
.search-container:hover .search-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
/* Input focus animation */
input:focus + div .search-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Line clamp for descriptions */
.line-clamp-2 {
display: -webkit-box;
@@ -265,17 +295,18 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Improved touch targets for mobile */
@media (max-width: 640px) {
a, button {
a,
button {
min-height: 44px;
display: flex;
align-items: center;
@@ -287,7 +318,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
// Script không thay đổi - giữ nguyên chức năng
document.addEventListener('DOMContentLoaded', () => {
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
// Show button when scrolled down
const toggleBackToTopButton = () => {
@@ -299,50 +330,50 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton(); // Initial check
}
// Add smooth scrolling to year links
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 100,
behavior: 'smooth'
behavior: 'smooth',
});
// Update URL hash without jumping
history.pushState(null, null, targetId);
}
});
});
// Add touch support for hover effects
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const articles = document.querySelectorAll('article');
articles.forEach(article => {
articles.forEach((article) => {
article.addEventListener('touchstart', () => {
article.classList.add('is-touched');
});
article.addEventListener('touchend', () => {
setTimeout(() => {
article.classList.remove('is-touched');
@@ -351,34 +382,34 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
});
}
});
// SPA transition handling
function setupSPATransitions() {
// Handle all blog post links for SPA transitions
document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
@@ -389,19 +420,19 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
}
});
});
// Handle year anchor links specially
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.setAttribute('data-spa-internal', 'true');
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script>
</script>