540 lines
20 KiB
Plaintext
540 lines
20 KiB
Plaintext
---
|
|
import Layout from '../layouts/Layout.astro';
|
|
import FormattedDate from '../components/FormattedDate.astro';
|
|
|
|
import directus from '../../lib/directus';
|
|
import { readItems, readSingleton } from '@directus/sdk';
|
|
|
|
const global = await directus.request(readSingleton('global'));
|
|
|
|
const posts = await directus.request(
|
|
readItems('posts', {
|
|
fields: ['*'],
|
|
sort: ['-published_date'],
|
|
})
|
|
);
|
|
|
|
const recentPosts = posts
|
|
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
|
|
.slice(0, 3);
|
|
|
|
const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
|
|
---
|
|
|
|
<Layout title=`Home | ${global.name}`>
|
|
<!-- Hero Section with mobile responsiveness -->
|
|
<section
|
|
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
|
|
transition:animate="slide"
|
|
>
|
|
<div class="relative mx-auto max-w-2xl">
|
|
<!-- Adjusted blob positions and sizes for mobile appearance -->
|
|
<div
|
|
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50"
|
|
>
|
|
</div>
|
|
<div
|
|
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-64 sm:w-64 dark:bg-zinc-800/30"
|
|
>
|
|
</div>
|
|
|
|
<div class="relative text-center sm:text-left">
|
|
<h1
|
|
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
|
|
>
|
|
<span class="block">Writing on technology,</span>
|
|
<span class="mt-1 block">development, and</span>
|
|
<span class="relative mt-1 block">
|
|
<span class="relative inline-block">
|
|
selfhosting.
|
|
<span
|
|
class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200"
|
|
></span>
|
|
</span>
|
|
</span>
|
|
</h1>
|
|
<p
|
|
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8 dark:text-zinc-400"
|
|
>
|
|
{global.about}
|
|
</p>
|
|
<div
|
|
class="mt-6 flex flex-wrap justify-center gap-3 sm:mt-8 sm:justify-start sm:gap-4 md:mt-10 md:gap-6"
|
|
>
|
|
<a
|
|
href="/about"
|
|
class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
>
|
|
<span>More about me</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
|
</svg>
|
|
<span
|
|
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
|
></span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Featured post section -->
|
|
<section
|
|
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
|
|
>
|
|
<div class="mx-auto max-w-3xl">
|
|
<div
|
|
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
|
|
>
|
|
<h2
|
|
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100"
|
|
>
|
|
Recent Posts
|
|
</h2>
|
|
<a
|
|
href="/blog"
|
|
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
>
|
|
<span class="flex items-center gap-1">
|
|
View all posts
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
|
</svg>
|
|
</span>
|
|
<span
|
|
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
|
></span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Grid for mobile layout -->
|
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
|
{
|
|
recentPosts.map((post, index) => (
|
|
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
|
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl dark:bg-zinc-800/50" />
|
|
|
|
{post.image && (
|
|
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
|
<img
|
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
|
alt={post.title}
|
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
loading={index === 0 ? 'eager' : 'lazy'}
|
|
width="400"
|
|
height="225"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 sm:justify-start sm:gap-x-4 dark:text-zinc-400">
|
|
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
|
<FormattedDate date={post.published_date} />
|
|
</time>
|
|
</div>
|
|
|
|
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
|
<a
|
|
href={`/blog/${post.slug}`}
|
|
class="flex min-h-[44px] items-center justify-center sm:justify-start"
|
|
>
|
|
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
|
|
{post.title}
|
|
</a>
|
|
</h3>
|
|
|
|
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 sm:mt-3 sm:text-left dark:text-zinc-400">
|
|
{post.description}
|
|
</p>
|
|
|
|
{post.tags && post.tags.length > 0 && (
|
|
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
|
|
{post.tags.slice(0, 3).map((tag) => (
|
|
<a
|
|
href={`/topics/${tag}`}
|
|
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 sm:px-3 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
|
>
|
|
#{tag}
|
|
</a>
|
|
))}
|
|
{post.tags.length > 3 && (
|
|
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
|
+{post.tags.length - 3} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<a
|
|
href={`/blog/${post.slug}`}
|
|
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
|
>
|
|
<span class="relative inline-block overflow-hidden">
|
|
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
|
Read article
|
|
</span>
|
|
<span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
|
Explore now
|
|
</span>
|
|
</span>
|
|
<svg
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
aria-hidden="true"
|
|
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
|
|
>
|
|
<path
|
|
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
|
stroke-width="1.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Topics section -->
|
|
{
|
|
allTags.length > 0 && (
|
|
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
|
|
<div class="mx-auto max-w-3xl">
|
|
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
|
|
Explore Topics
|
|
</h2>
|
|
|
|
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
|
{allTags.map((tag) => {
|
|
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
|
return (
|
|
<a
|
|
href={`/topics/${tag}`}
|
|
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/70"
|
|
>
|
|
<div class="mb-2 flex items-start justify-between">
|
|
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
#{tag}
|
|
</span>
|
|
<span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
|
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
|
</span>
|
|
</div>
|
|
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
|
|
Explore articles about {tag}
|
|
</p>
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div class="mt-6 text-center sm:mt-8">
|
|
<a
|
|
href="/tags"
|
|
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
>
|
|
<span>View all topics</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
</Layout>
|
|
|
|
<script>
|
|
// Add hover effect for cards on touch devices
|
|
document.addEventListener('astro:page-load', () => {
|
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
|
|
if (isTouchDevice) {
|
|
const cards = document.querySelectorAll('.hover-3d');
|
|
|
|
cards.forEach((card) => {
|
|
card.addEventListener('touchstart', () => {
|
|
card.classList.add('is-touched');
|
|
});
|
|
|
|
card.addEventListener('touchend', () => {
|
|
setTimeout(() => {
|
|
card.classList.remove('is-touched');
|
|
}, 300);
|
|
});
|
|
});
|
|
|
|
// Disable hover animations on touch devices
|
|
document.documentElement.classList.add('touch-device');
|
|
}
|
|
|
|
// Viewport height fix for mobile browsers
|
|
const setVh = () => {
|
|
const vh = window.innerHeight * 0.01;
|
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
};
|
|
|
|
// Set initial value
|
|
setVh();
|
|
|
|
// Update on resize and scroll to prevent content shifting
|
|
window.addEventListener('resize', setVh);
|
|
|
|
// Use a debounced scroll handler to prevent performance issues
|
|
let scrollTimeout;
|
|
window.addEventListener('scroll', () => {
|
|
if (scrollTimeout) {
|
|
window.cancelAnimationFrame(scrollTimeout);
|
|
}
|
|
|
|
scrollTimeout = window.requestAnimationFrame(() => {
|
|
// Lock width during scroll
|
|
document.body.style.width = '100%';
|
|
document.body.style.overflowX = 'hidden';
|
|
});
|
|
});
|
|
|
|
// Fix for iOS Safari address bar height changes
|
|
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
|
|
// Force the layout to use the initial viewport size
|
|
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
|
|
|
|
// Apply fixed height to sections to prevent resizing
|
|
const sections = document.querySelectorAll('section');
|
|
sections.forEach((section) => {
|
|
section.style.width = '100%';
|
|
});
|
|
}
|
|
|
|
// Theme change handler that preserves scroll position and provides smoother transitions
|
|
document.addEventListener('themeChanged', () => {
|
|
// Store current scroll position
|
|
const scrollPosition = window.scrollY;
|
|
|
|
// Create a temporary overlay for smoother transition
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
inset: 0;
|
|
background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'};
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
|
|
// Fade in overlay
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '0.5';
|
|
|
|
// Update theme-transition elements without forcing reflow of entire page
|
|
requestAnimationFrame(() => {
|
|
document
|
|
.querySelectorAll(
|
|
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
|
|
)
|
|
.forEach((el) => {
|
|
// Apply a subtle animation instead of a hard reset
|
|
el.style.transition = 'all 0.5s ease';
|
|
});
|
|
|
|
// Fade out overlay after transition completes
|
|
setTimeout(() => {
|
|
overlay.style.opacity = '0';
|
|
setTimeout(() => {
|
|
overlay.remove();
|
|
}, 300);
|
|
}, 300);
|
|
});
|
|
});
|
|
|
|
// Restore scroll position (prevents jumping to top)
|
|
if (scrollPosition > 0) {
|
|
setTimeout(() => {
|
|
window.scrollTo({
|
|
top: scrollPosition,
|
|
behavior: 'auto', // Use 'auto' to prevent animation
|
|
});
|
|
}, 10);
|
|
}
|
|
});
|
|
|
|
// Fix theme inconsistency issues by checking theme on visibility change
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
const storedTheme = localStorage.getItem('theme');
|
|
const currentThemeIsDark = document.documentElement.classList.contains('dark');
|
|
|
|
if (storedTheme === 'dark' && !currentThemeIsDark) {
|
|
document.documentElement.classList.add('dark');
|
|
} else if (storedTheme === 'light' && currentThemeIsDark) {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add smooth reveal animations for content after loading
|
|
const animateContent = () => {
|
|
// Animate hero section
|
|
const heroElements = document.querySelectorAll(
|
|
'.hero-text span, .hero-text + p, .hero-text ~ div'
|
|
);
|
|
heroElements.forEach((el, index) => {
|
|
setTimeout(
|
|
() => {
|
|
el.classList.add('animate-reveal');
|
|
},
|
|
100 + index * 150
|
|
);
|
|
});
|
|
|
|
// Animate posts with staggered delay
|
|
const articles = document.querySelectorAll('article.group');
|
|
articles.forEach((article, index) => {
|
|
setTimeout(
|
|
() => {
|
|
article.classList.add('animate-reveal');
|
|
},
|
|
500 + index * 150
|
|
);
|
|
});
|
|
|
|
// Animate topic cards with staggered delay
|
|
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
|
|
topicCards.forEach((card, index) => {
|
|
setTimeout(
|
|
() => {
|
|
card.classList.add('animate-reveal');
|
|
},
|
|
800 + index * 100
|
|
);
|
|
});
|
|
};
|
|
|
|
// Run animations after the loading screen is hidden
|
|
const loadingScreen = document.getElementById('loading-screen');
|
|
if (loadingScreen) {
|
|
// Check if loading screen is already hidden (page refresh)
|
|
if (loadingScreen.style.display === 'none') {
|
|
animateContent();
|
|
} else {
|
|
// Wait for loading screen to hide
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (
|
|
mutation.target === loadingScreen &&
|
|
mutation.type === 'attributes' &&
|
|
mutation.attributeName === 'style' &&
|
|
loadingScreen.style.display === 'none'
|
|
) {
|
|
animateContent();
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(loadingScreen, { attributes: true });
|
|
|
|
// Fallback
|
|
setTimeout(animateContent, 3500);
|
|
}
|
|
} else {
|
|
// If loading screen doesn't exist for some reason
|
|
animateContent();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
/* Fix for theme transition issues */
|
|
:global(:root) {
|
|
--theme-transition-duration: 0.5s;
|
|
--theme-transition-timing: ease;
|
|
}
|
|
|
|
:global(html),
|
|
:global(body) {
|
|
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
}
|
|
|
|
:global(.theme-transition-all) {
|
|
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
|
|
}
|
|
|
|
:global(.theme-transition-bg) {
|
|
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
}
|
|
|
|
:global(.theme-transition-color) {
|
|
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
}
|
|
|
|
/* Ensure transitions apply to all theme-related properties */
|
|
:global(*) {
|
|
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
|
transition-duration: var(--theme-transition-duration);
|
|
transition-timing-function: var(--theme-transition-timing);
|
|
}
|
|
|
|
/* Remove the forced transition disabling which causes flickering */
|
|
:global(.theme-switching),
|
|
:global(.theme-switching *) {
|
|
/* Use a subtle transition instead of none */
|
|
transition-duration: 0.3s !important;
|
|
}
|
|
|
|
/* Content reveal animations */
|
|
.hero-text span,
|
|
.hero-text + p,
|
|
.hero-text ~ div,
|
|
article.group,
|
|
a.group.flex.flex-col {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition:
|
|
opacity 0.8s ease,
|
|
transform 0.8s ease;
|
|
}
|
|
|
|
.animate-reveal {
|
|
opacity: 1 !important;
|
|
transform: translateY(0) !important;
|
|
}
|
|
</style>
|