Compare commits

..

11 Commits

Author SHA1 Message Date
fcae7676c6 0.11.0 release
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 25s
release-image / release (push) Successful in 1m20s
2025-07-19 22:39:43 -05:00
cc16b5435a update node
All checks were successful
renovate / renovate (push) Successful in 1m16s
test-build / build (push) Successful in 1m26s
2025-07-19 22:37:05 -05:00
27b5e6a36b change layout and animations to be more common with each other 2025-07-19 22:37:05 -05:00
bcb91972a1 Merge pull request 'Update astro monorepo' (#42) from renovate/astro-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 23s
test-build / build (push) Successful in 28s
Reviewed-on: #42
2025-07-20 03:34:37 +00:00
b11666decb Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-20 00:02:37 +00:00
a947a05041 Merge pull request 'Update dependency eslint-config-prettier to v10.1.8' (#43) from renovate/eslint-config-prettier-10.x into main
All checks were successful
renovate / renovate (push) Successful in 47s
test-build / build (push) Successful in 34s
2025-07-20 00:01:54 +00:00
297c573281 Update dependency eslint-config-prettier to v10.1.8
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 25s
2025-07-20 00:01:31 +00:00
9093594973 Update dependency astro to v5.11.2
All checks were successful
test-build / build (push) Successful in 37s
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Successful in 21s
test-build / build (pull_request) Successful in 36s
2025-07-18 00:01:29 +00:00
77ce0a1182 Update dependency framer-motion to v12.23.6
All checks were successful
renovate / renovate (push) Successful in 28s
test-build / build (pull_request) Successful in 29s
test-build / build (push) Successful in 40s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 16:14:52 +00:00
799e6b6090 Merge pull request 'Update typescript-eslint monorepo to v8.37.0' (#38) from renovate/typescript-eslint-monorepo into main
Some checks failed
test-build / build (push) Successful in 26s
process-repository / process-repository (push) Failing after 11s
renovate / renovate (push) Successful in 1m21s
Reviewed-on: #38
2025-07-16 16:14:17 +00:00
735e4b4877 Update typescript-eslint monorepo to v8.37.0
All checks were successful
test-build / build (pull_request) Successful in 27s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 02:59:27 +00:00
18 changed files with 1399 additions and 1547 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22.17.x node-version: 22.17.1
cache: pnpm cache: pnpm
- name: Install Dependencies - name: Install Dependencies

View File

@@ -1,7 +1,7 @@
ARG REGISTRY=docker.io ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base
LABEL version="0.10.0" LABEL version="0.11.0"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"

View File

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.10.0", "version": "0.11.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
@@ -31,13 +31,13 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@typescript-eslint/parser": "8.36.0", "@typescript-eslint/parser": "8.37.0",
"eslint": "9.31.0", "eslint": "9.31.0",
"eslint-config-prettier": "10.1.5", "eslint-config-prettier": "10.1.8",
"eslint-plugin-astro": "1.3.1", "eslint-plugin-astro": "1.3.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.12", "prettier-plugin-tailwindcss": "^0.6.12",
"typescript-eslint": "8.36.0" "typescript-eslint": "8.37.0"
} }
} }

533
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,13 +55,13 @@
/* Grid pattern for dots */ /* Grid pattern for dots */
.bg-grid-pattern { .bg-grid-pattern {
background-size: 24px 24px; background-size: 24px 24px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px); background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1); transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
} }
/* Dark mode version */ /* Dark mode version */
:global(.dark) .bg-grid-pattern { :global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px); background-image: radial-gradient(circle, rgba(255, 255, 255, 0.15) 1px, transparent 1px);
} }
/* Ambient glow animations */ /* Ambient glow animations */

View File

@@ -10,8 +10,8 @@ const currentYear = new Date().getFullYear();
const navLinks = [ const navLinks = [
{ text: 'Home', href: '/' }, { text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' }, { text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' },
{ text: 'About', href: '/about' }, { text: 'About', href: '/about' },
{ text: 'RSS', href: '/rss' },
]; ];
const socialLinks = [ const socialLinks = [
@@ -60,17 +60,14 @@ const socialLinks = [
<a href="/" class="group inline-block"> <a href="/" class="group inline-block">
<div class="flex items-center"> <div class="flex items-center">
<div <div
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400" class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform dark:from-zinc-200 dark:to-zinc-400"
> >
<span <span
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900" class="theme-transition-all text-xl font-bold text-zinc-100 duration-300 dark:text-zinc-900"
> >
{global.initals} {global.initals}
</span> </span>
<div <div class="absolute inset-0"></div>
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
>
</div>
</div> </div>
<span <span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
@@ -129,7 +126,6 @@ const socialLinks = [
> >
<span class="relative inline-block overflow-hidden"> <span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.text}</span> <span class="relative z-10">{link.text}</span>
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</span> </span>
</a> </a>
</li> </li>

View File

@@ -10,7 +10,22 @@ const parsedDate = typeof date === 'string' ? new Date(date) : date;
{ {
parsedDate && ( parsedDate && (
<time datetime={parsedDate.toISOString()}> <time datetime={parsedDate.toISOString()} class="z-10 flex items-center gap-1.5">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
{parsedDate.toLocaleDateString('en-us', { {parsedDate.toLocaleDateString('en-us', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',

View File

@@ -5,12 +5,13 @@ import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global')); const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
const navItems = [ const navItems = [
{ text: 'Home', href: '/' }, { text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' }, { text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' },
{ text: 'About', href: '/about' }, { text: 'About', href: '/about' },
{ text: 'Gitea', href: links.gitea },
{ text: 'RSS', href: 'rss.xml' }, { text: 'RSS', href: 'rss.xml' },
]; ];
@@ -36,8 +37,8 @@ const currentPath = pathname.slice(1);
href={item.href} href={item.href}
class={`text-sm font-medium ${ class={`text-sm font-medium ${
isActive isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
}`} }`}
> >
{item.text} {item.text}
@@ -101,8 +102,8 @@ const currentPath = pathname.slice(1);
href={item.href} href={item.href}
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${ class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
isActive isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' : 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
}`} }`}
style={`transition-delay: ${index * 0.05}s;`} style={`transition-delay: ${index * 0.05}s;`}
> >

View File

@@ -8,16 +8,21 @@ const { tags = [], class: className = '' } = Astro.props;
--- ---
{ {
tags.length > 0 && ( tags && (
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}> <div class={`mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start ${className}`}>
{tags.map((tag) => ( {tags.slice(0, 2).map((postTag) => (
<a <a
href={`/tag/${tag}`} href={`/tags/${postTag}`}
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" class={`inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700`}
> >
{tag} #{postTag}
</a> </a>
))} ))}
{tags.length > 2 && (
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
+{tags.length - 3}
</span>
)}
</div> </div>
) )
} }

View File

@@ -18,7 +18,6 @@ export async function getStaticPaths() {
} }
const post = Astro.props; const post = Astro.props;
const published_date: string = post.published_date.toLocaleString();
let canonicalURL; let canonicalURL;
try { try {
@@ -30,19 +29,34 @@ try {
--- ---
<Layout title={post.title} description={post.description}> <Layout title={post.title} description={post.description}>
<article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl"> <article
<div class="mb-12"> class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl"
transition:animate="slide"
>
<div class="hero-text mb-12">
<h1 <h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100" class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
> >
{post.title} {post.title}
</h1> </h1>
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400"> <p
<FormattedDate date={published_date} /> class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"
>
{post.description}
</p>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<FormattedDate date={post.published_date} />
</div> </div>
<TagList tags={post.tags} class="mt-2" /> <div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<TagList tags={post.tags} />
</div>
</div> </div>
<!-- Hero image --> <!-- Hero image -->
@@ -85,7 +99,60 @@ try {
<slot name="after-article" /> <slot name="after-article" />
</Layout> </Layout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a'
);
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
);
});
};
animateContent();
});
</script>
<style> <style>
/* Content reveal animations */
.hero-text h1,
.hero-text div,
.hero-text ~ div,
.hero-text span,
.hero-text p,
.hero-text + a,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Hero image styling */ /* Hero image styling */
article img:first-of-type { article img:first-of-type {
border-radius: 1rem; border-radius: 1rem;

View File

@@ -7,22 +7,6 @@ import Layout from '../layouts/Layout.astro';
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
transition:animate="slide" transition:animate="slide"
> >
<!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden">
<div
class="animate-blob absolute -top-20 -left-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute top-1/2 right-1/4 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40"
>
</div>
</div>
<!-- Main content with animation --> <!-- Main content with animation -->
<div class="relative z-10 mx-auto max-w-xl"> <div class="relative z-10 mx-auto max-w-xl">
<div class="glitch-wrapper"> <div class="glitch-wrapper">
@@ -135,34 +119,6 @@ import Layout from '../layouts/Layout.astro';
</script> </script>
<style> <style>
/* Animation for floating blobs */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
/* Glitch effect for 404 text */ /* Glitch effect for 404 text */
.glitch-wrapper { .glitch-wrapper {
position: relative; position: relative;

View File

@@ -22,20 +22,10 @@ const skills = await directus.request(
> >
<!-- Hero Section --> <!-- Hero Section -->
<div class="relative mb-12 sm:mb-16 md:mb-20"> <div class="relative mb-12 sm:mb-16 md:mb-20">
<!-- Decorative elements -->
<div
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
>
</div>
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12"> <div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
<div class="order-2 text-center md:order-1 md:text-left"> <div class="hero-text order-2 text-center md:order-1 md:text-left">
<h1 <h1
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100" class="theme-transition-color hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
> >
Hello, I'm <span Hello, I'm <span
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100" class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
@@ -44,7 +34,7 @@ const skills = await directus.request(
</h1> </h1>
<p <p
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400" class="theme-transition-color hero-text mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
> >
{about.background} {about.background}
</p> </p>
@@ -67,13 +57,6 @@ const skills = await directus.request(
loading="eager" loading="eager"
/> />
</div> </div>
<!-- Decorative elements -->
<div
class="theme-transition-all absolute -right-4 -bottom-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg sm:-right-6 sm:-bottom-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24 dark:border-zinc-900 dark:bg-zinc-800"
>
<span class="text-2xl sm:text-3xl">👋</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -93,16 +76,22 @@ const skills = await directus.request(
></span> ></span>
</h2> </h2>
<div class="theme-transition-all prose prose-zinc dark:prose-invert max-w-none"> <div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none">
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.experience} {about.experience}
</p> </p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.education} {about.education}
</p> </p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.certifications} {about.certifications}
</p> </p>
</div> </div>
@@ -182,57 +171,159 @@ const skills = await directus.request(
I'm always open to new opportunities and collaborations. If you'd like to work together or I'm always open to new opportunities and collaborations. If you'd like to work together or
just say hello, feel free to reach out. just say hello, feel free to reach out.
</p> </p>
<div class="group">
<a <a
href=`mailto:${global.email}` href=`mailto:${global.email}`
class="hover theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300" class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors group-hover:bg-blue-600 group-hover:text-zinc-100 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:group-hover:bg-blue-600 dark:group-hover:text-zinc-100"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
stroke-width="2" fill="none"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" viewBox="0 0 24 24"
></path> stroke="currentColor"
</svg> >
Say Hello <path
</a> stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Say Hello</span>
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-100 transition-all duration-300 group-hover:w-full"
></span>
</span>
</a>
</div>
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
};
animateContent();
// Create seamless infinite scrolling effect
const sliderTrack = document.querySelector('.slider-track');
function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return;
// Set proper animation based on screen size
function updateScrollAnimation() {
if (window.innerWidth >= 640) {
sliderTrack.style.animation = 'scroll 60s linear infinite';
} else {
sliderTrack.style.animation = 'scroll 40s linear infinite';
}
}
updateScrollAnimation();
window.addEventListener('resize', updateScrollAnimation);
}
setupInfiniteScroll();
// Pause animation on hover/touch
sliderTrack?.addEventListener('mouseenter', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('touchstart', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('mouseleave', () => {
sliderTrack.style.animationPlayState = 'running';
});
sliderTrack?.addEventListener('touchend', () => {
setTimeout(() => {
sliderTrack.style.animationPlayState = 'running';
}, 1000); // Delay resuming animation after touch
});
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card');
if (!isTouchDevice) {
cards.forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const angleX = (y - centerY) / 15;
const angleY = (centerX - x) / 15;
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
// Dynamic shadow based on tilt
const shadowX = (x - centerX) / 25;
const shadowY = (y - centerY) / 25;
card.style.boxShadow = `
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
0 10px 20px rgba(0, 0, 0, 0.05)
`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.boxShadow = '';
});
});
} else {
// Simpler effects for touch devices
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
}
// Handle theme transition
document.addEventListener('themeChange', () => {
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('theme-changing');
setTimeout(() => {
card.classList.remove('theme-changing');
}, 600);
}, index * 50);
});
});
});
</script>
<style> <style>
/* Blob animation */
.animate-blob {
animation: blob-bounce 8s infinite ease;
}
.animation-delay-2000 {
animation-delay: 2s;
}
@keyframes blob-bounce {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(5%, 5%) scale(1.05);
}
50% {
transform: translate(0, 10%) scale(1);
}
75% {
transform: translate(-5%, 5%) scale(0.95);
}
}
/* Tech Stack Slider */ /* Tech Stack Slider */
.slider-track { .slider-track {
width: fit-content; width: fit-content;
@@ -352,6 +443,23 @@ const skills = await directus.request(
} }
} }
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Theme transition effect */ /* Theme transition effect */
:global(.theme-switching) .theme-transition-element { :global(.theme-switching) .theme-transition-element {
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1); animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
@@ -367,108 +475,3 @@ const skills = await directus.request(
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
} }
</style> </style>
<script>
document.addEventListener('astro:page-load', () => {
const sliderTrack = document.querySelector('.slider-track');
// Create seamless infinite scrolling effect
function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return;
// Set proper animation based on screen size
function updateScrollAnimation() {
if (window.innerWidth >= 640) {
sliderTrack.style.animation = 'scroll 60s linear infinite';
} else {
sliderTrack.style.animation = 'scroll 40s linear infinite';
}
}
updateScrollAnimation();
window.addEventListener('resize', updateScrollAnimation);
}
setupInfiniteScroll();
// Pause animation on hover/touch
sliderTrack?.addEventListener('mouseenter', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('touchstart', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('mouseleave', () => {
sliderTrack.style.animationPlayState = 'running';
});
sliderTrack?.addEventListener('touchend', () => {
setTimeout(() => {
sliderTrack.style.animationPlayState = 'running';
}, 1000); // Delay resuming animation after touch
});
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card');
if (!isTouchDevice) {
cards.forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const angleX = (y - centerY) / 15;
const angleY = (centerX - x) / 15;
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
// Dynamic shadow based on tilt
const shadowX = (x - centerX) / 25;
const shadowY = (y - centerY) / 25;
card.style.boxShadow = `
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
0 10px 20px rgba(0, 0, 0, 0.05)
`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.boxShadow = '';
});
});
} else {
// Simpler effects for touch devices
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
}
// Handle theme transition
document.addEventListener('themeChange', () => {
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('theme-changing');
setTimeout(() => {
card.classList.remove('theme-changing');
}, 600);
}, index * 50);
});
});
});
</script>

View File

@@ -41,14 +41,14 @@ const { post, nextPost, prevPost } = Astro.props;
updated_date={post.updated_date} updated_date={post.updated_date}
tags={post.tags} tags={post.tags}
> >
<!-- Main Content - Enhanced with better typography and spacing --> <!-- Main Content -->
<div <div
class="prose prose-sm prose-zinc 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 max-w-none" class="hero-text prose prose-sm prose-zinc 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 max-w-none"
> >
<div set:html={post.content} /> <div set:html={post.content} />
</div> </div>
<!-- Next/Previous Navigation - Improved responsive design --> <!-- Next/Previous Navigation -->
<div <div
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800" class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800"
> >
@@ -116,6 +116,63 @@ const { post, nextPost, prevPost } = Astro.props;
</BlogPost> </BlogPost>
<script> <script>
document.addEventListener('astro:page-load', () => {
// Show button when scrolled down
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
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
);
});
};
animateContent();
});
// Add copy buttons to code blocks // Add copy buttons to code blocks
function initializeCodeCopyButtons() { function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre'); const codeBlocks = document.querySelectorAll('pre');
@@ -201,6 +258,24 @@ const { post, nextPost, prevPost } = Astro.props;
</script> </script>
<style> <style>
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Language badge styling */ /* Language badge styling */
.language-badge { .language-badge {
font-family: font-family:

View File

@@ -1,5 +1,7 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import TagList from '../../components/TagList.astro';
import directus from '../../../lib/directus'; import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk'; import { readItems } from '@directus/sdk';
@@ -11,81 +13,57 @@ const posts = await directus.request(
}) })
); );
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
// Group posts by year for timeline effect // Group posts by year for timeline effect
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
const postsByYear = sortedPosts.reduce((acc, post) => { const postsByYear = sortedPosts.reduce((acc, post) => {
const year = new Date(post.published_date).getFullYear(); const year = new Date(post.published_date).getFullYear();
if (!acc[year]) acc[year] = []; if (!acc[year]) acc[year] = [];
acc[year].push(post); acc[year].push(post);
return acc; return acc;
}, {}); }, {});
const years = Object.keys(postsByYear).sort((a, b) => b - a); const years = Object.keys(postsByYear).sort((a, b) => b - a);
--- ---
<BaseLayout title="Blog"> <BaseLayout title="Blog">
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide"> <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide">
<div class="relative mb-12 sm:mb-20"> <div class="relative mb-12 sm:mb-20">
<div <div class="hero-text relative text-center">
class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
>
</div>
<div class="relative text-center">
<h1 <h1
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100" class="hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100"
> >
Blog Blog
</h1> </h1>
<p <p
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400" class="hero-text mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400"
> >
Thoughts, ideas, and explorations on technology and selfhosting. Thoughts, ideas, and explorations on technology and selfhosting.
</p> </p>
</div> </div>
</div> </div>
<!-- Grid layout for mobile experience --> <!-- Featured post -->
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12"> <div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
<!-- Featured post (if exists) -->
{ {
sortedPosts.length > 0 && ( sortedPosts.length > 0 && (
<div class="mb-8 sm:mb-12 md:col-span-12"> <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 sm:pb-8 dark:border-zinc-800"> <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row"> <div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{sortedPosts[0].image && ( {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"> <div class="z-10 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 <img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}?width=500`}
alt={sortedPosts[0].title} alt={sortedPosts[0].image_alt}
class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0" class="h-full w-full object-cover"
loading="eager" loading="eager"
/> />
</div> </div>
)} )}
<div class="flex flex-1 flex-col justify-center"> <div class="z-10 flex-1">
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 sm:text-sm md:justify-start dark:text-zinc-400"> <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
<span class="font-medium tracking-wider uppercase">Featured</span>
<span class="h-px w-6 bg-zinc-300 sm:w-8 dark:bg-zinc-700" />
{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 sm:mb-4 sm:text-3xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300">
<a <a
href={`/blog/${sortedPosts[0].slug}/`} href={`/blog/${sortedPosts[0].slug}/`}
class="before:absolute before:inset-0" class="before:absolute before:inset-0"
@@ -94,23 +72,44 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
</a> </a>
</h2> </h2>
<p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 sm:mb-6 sm:text-base md:text-left dark:text-zinc-400"> <p class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{sortedPosts[0].description} {sortedPosts[0].description}
</p> </p>
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start"> <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
{sortedPosts[0].tags && ( <FormattedDate date={sortedPosts[0].published_date} />
<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 tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400">
{tag}
</span>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
<div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
<TagList tags={sortedPosts[0].tags} />
<div class="mx-auto sm:mr-0 sm:ml-auto">
<a
href={`/blog/${sortedPosts[0].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="relative z-10">Read article</span>
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</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"
>
<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>
</div>
</div>
</article> </article>
</div> </div>
) )
@@ -122,10 +121,9 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
<h3 <h3
class="mb-4 text-center text-lg font-medium tracking-wider text-zinc-900 uppercase md:text-left dark:text-zinc-100" class="mb-4 text-center text-lg font-medium tracking-wider text-zinc-900 uppercase md:text-left dark:text-zinc-100"
> >
Archive History
</h3> </h3>
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
<div <div
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0" class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
> >
@@ -133,12 +131,12 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
years.map((year, index) => ( years.map((year, index) => (
<a <a
href={`#year-${year}`} href={`#year-${year}`}
class={`hover mr-3 flex items-center rounded-full border-b border-zinc-100 px-4 py-2 whitespace-nowrap transition-colors hover:bg-zinc-50 md:mr-0 md:w-full md:rounded-none md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-900 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`} class={`mr-3 flex items-center rounded-xl border border-zinc-300 bg-white/50 px-4 py-2 whitespace-nowrap transition-all duration-300 hover:bg-zinc-50 sm:rounded-2xl md:mr-0 md:w-full md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-800/70 ${index === 0 ? 'bg-white/50 dark:bg-zinc-900/50' : ''}`}
> >
<span class="text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100"> <span class="ml-3 text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100">
{year} {year}
</span> </span>
<span class="ml-2 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400"> <span class="mr-3 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''} {postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span> </span>
</a> </a>
@@ -148,7 +146,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
</div> </div>
</div> </div>
<!-- Post grid for mobile --> <!-- Post grid -->
<div class="md:col-span-9"> <div class="md:col-span-9">
{ {
years.map((year) => ( years.map((year) => (
@@ -161,13 +159,15 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`} 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) => ( {postsByYear[year].map((post) => (
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0"> <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
{post.image && ( {post.image && (
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56"> <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
<img <img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title} alt={post.title}
class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0" class="h-full w-full object-cover"
loading="lazy" loading="lazy"
/> />
</div> </div>
@@ -188,30 +188,48 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
)} )}
</div> </div>
<h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300"> <h3 class="z-10 mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title} {post.title}
</a> </a>
</h3> </h3>
<p class="mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400"> <p class="z-10 mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400">
{post.description} {post.description}
</p> </p>
{post.tags && ( <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start"> <FormattedDate date={post.published_date} />
{post.tags.slice(0, 2).map((tag) => ( </div>
<span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400">
{tag} <div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
<TagList tags={post.tags} />
<div class="mx-auto sm:mr-0 sm:ml-auto">
<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="relative z-10">Read article</span>
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</span> </span>
))} <svg
{post.tags.length > 2 && ( viewBox="0 0 16 16"
<span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400"> fill="none"
+{post.tags.length - 2} aria-hidden="true"
</span> class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
)} >
<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>
</div> </div>
)} </div>
</div> </div>
</article> </article>
))} ))}
@@ -224,30 +242,316 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
</div> </div>
</BaseLayout> </BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Force the viewport to be exactly the width of the device
const fixViewportWidth = () => {
const viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
}
// Fix for horizontal overflow
document.body.style.overflowX = 'hidden';
document.documentElement.style.overflowX = 'hidden';
document.documentElement.style.width = '100%';
document.body.style.width = '100%';
};
fixViewportWidth();
// Show button when scrolled down
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
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
);
});
};
animateContent();
// Add smooth scrolling to year links
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',
});
// Update URL hash without jumping
history.pushState(null, null, targetId);
}
});
});
// Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360;
const isExtremelySmall = width < 280;
const isMicroScreen = width < 240;
// Fix container width to match viewport exactly
const container = document.querySelector('.tag-cloud');
if (container) {
container.style.maxWidth = '100vw';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
// Remove any margins that might cause overflow
container.style.marginLeft = '0';
container.style.marginRight = '0';
}
// Fix grid width
const grid = document.querySelector('.grid');
if (grid) {
grid.style.width = '100%';
grid.style.maxWidth = '100%';
}
tagItems.forEach((item) => {
// Set appropriate classes based on screen size
if (isMicroScreen) {
item.classList.add('micro-screen');
item.classList.remove('extremely-small-screen', 'very-small-screen');
} else if (isExtremelySmall) {
item.classList.add('extremely-small-screen');
item.classList.remove('very-small-screen', 'micro-screen');
} else if (isVerySmall) {
item.classList.add('very-small-screen');
item.classList.remove('extremely-small-screen', 'micro-screen');
} else {
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
}
// Ensure text doesn't overflow on small screens
const tagName = item.querySelector('h3');
const tagCount = item.querySelector('p');
if (tagName) {
// Set title for accessibility
tagName.title = tagName.textContent.trim();
// Adjust text length based on screen size
if (isMicroScreen && tagName.textContent.length > 6) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 6) + '...';
} else if (isExtremelySmall && tagName.textContent.length > 8) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 8) + '...';
} else if (isVerySmall && tagName.textContent.length > 12) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 12) + '...';
} else if (tagName.dataset.fullText) {
tagName.textContent = tagName.dataset.fullText;
delete tagName.dataset.fullText;
}
}
// Set the tag hue for hover effects
const hue = item.style.getPropertyValue('--tag-hue');
item.style.setProperty('--hover-hue', hue);
});
};
adjustTagItems();
// Run on resize with optimized debounce
let resizeTimer;
const handleResize = () => {
if (resizeTimer) {
window.cancelAnimationFrame(resizeTimer);
}
resizeTimer = window.requestAnimationFrame(() => {
fixViewportWidth();
adjustTagItems();
});
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// Ensure layout is recalculated after page is fully loaded
window.addEventListener('load', () => {
fixViewportWidth();
adjustTagItems();
// Force recalculation after images and fonts are loaded
setTimeout(() => {
fixViewportWidth();
adjustTagItems();
}, 500);
});
// Add touch support for hover effects
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const articles = document.querySelectorAll('article');
articles.forEach((article) => {
article.addEventListener('touchstart', () => {
article.classList.add('is-touched');
});
article.addEventListener('touchend', () => {
setTimeout(() => {
article.classList.remove('is-touched');
}, 300);
});
});
}
// Add touch support for mobile devices
const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach((item) => {
item.addEventListener(
'touchstart',
() => {
item.classList.add('touch-active');
},
{ passive: true }
);
item.addEventListener(
'touchend',
() => {
setTimeout(() => {
item.classList.remove('touch-active');
}, 150);
},
{ passive: true }
);
// Cancel active state if touch moves away
item.addEventListener(
'touchmove',
(e) => {
const touch = e.touches[0];
const rect = item.getBoundingClientRect();
if (
touch.clientX < rect.left ||
touch.clientX > rect.right ||
touch.clientY < rect.top ||
touch.clientY > rect.bottom
) {
item.classList.remove('touch-active');
}
},
{ passive: true }
);
});
};
addTouchSupport();
// Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues
const setVh = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVh();
window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', () => {
// Wait for orientation change to complete
setTimeout(() => {
setVh();
fixViewportWidth();
}, 100);
});
}
});
</script>
<style> <style>
/* Blob animation */ /* Content reveal animations */
.animate-blob { .hero-text h1,
animation: blob-bounce 8s infinite ease; .hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
} }
.animation-delay-2000 { .animate-reveal {
animation-delay: 2s; opacity: 1 !important;
} transform: translateY(0) !important;
@keyframes blob-bounce {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(5%, 5%) scale(1.05);
}
50% {
transform: translate(0, 10%) scale(1);
}
75% {
transform: translate(-5%, 5%) scale(0.95);
}
} }
/* Search container hover effect */ /* Search container hover effect */
@@ -265,11 +569,196 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
} }
} }
/* Shadow for dark mode */
:global(.dark) .tag-cloud {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Input focus animation */ /* Input focus animation */
input:focus + div .search-pulse { input:focus + div .search-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
/* Base styles */
.tag-cloud {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d;
perspective: 1000px;
transition: all var(--theme-transition);
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Fix for horizontal overflow */
:global(html),
:global(body) {
overflow-x: hidden;
width: 100%;
max-width: 100%;
}
:global(.max-w-6xl) {
max-width: 100% !important;
width: 100% !important;
}
/* Micro screens (below 240px) */
@media (max-width: 239px) {
.tag-cloud {
padding: 0.5rem !important;
margin: 0 !important;
border-radius: 0.25rem !important;
width: 100% !important;
}
.tag-cloud h2 {
font-size: 0.875rem !important;
margin-bottom: 0.375rem !important;
}
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
gap: 0.375rem !important;
width: 100% !important;
}
.micro-screen .flex {
padding: 0.25rem !important;
}
.micro-screen h3 {
font-size: 0.625rem !important;
}
.micro-screen p {
font-size: 0.5rem !important;
}
}
/* Extra extra extra small screens (240px-279px) */
@media (min-width: 240px) and (max-width: 279px) {
.xxxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxxs\:w-6 {
width: 1.5rem;
}
.xxxs\:h-6 {
height: 1.5rem;
}
.xxxs\:text-xs {
font-size: 0.75rem;
}
.xxxs\:gap-2 {
gap: 0.5rem;
}
.xxxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra extra small screens (280px-359px) */
@media (min-width: 280px) {
.xxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxs\:w-6 {
width: 1.5rem;
}
.xxs\:h-6 {
height: 1.5rem;
}
.xxs\:text-xs {
font-size: 0.75rem;
}
.xxs\:gap-2 {
gap: 0.5rem;
}
.xxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra small screens (360px-639px) */
@media (min-width: 360px) {
.xs\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xs\:w-7 {
width: 1.75rem;
}
.xs\:h-7 {
height: 1.75rem;
}
.xs\:text-xs {
font-size: 0.75rem;
}
.xs\:text-sm {
font-size: 0.875rem;
}
.xs\:gap-2 {
gap: 0.5rem;
}
.xs\:text-\[10px\] {
font-size: 10px;
}
}
/* Hide scrollbar but keep functionality */ /* Hide scrollbar but keep functionality */
.hide-scrollbar { .hide-scrollbar {
-ms-overflow-style: none; -ms-overflow-style: none;
@@ -295,6 +784,38 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
overflow: hidden; overflow: hidden;
} }
/* Prevent layout shifts */
.grow {
grow: 1;
}
.min-w-0 {
min-width: 0;
}
/* Ensure container doesn't overflow */
.overflow-hidden {
overflow: hidden;
}
/* Ensure text doesn't overflow on small screens */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* Ensure proper word breaking for long tag names */
.break-words {
word-break: break-word;
overflow-wrap: break-word;
}
.hyphens-auto {
hyphens: auto;
}
/* Touch targets for mobile */ /* Touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, a,
@@ -304,73 +825,21 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
align-items: center; align-items: center;
} }
} }
.touch-active {
transform: scale(0.97) !important;
opacity: 0.9;
transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
}
/* Fix for iOS Safari notch */
@supports (padding: max(0px)) {
.tag-cloud {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
}
}
</style> </style>
<script>
document.addEventListener('astro:page-load', () => {
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
// Show button when scrolled down
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth scrolling to year links
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',
});
// 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) => {
article.addEventListener('touchstart', () => {
article.classList.add('is-touched');
});
article.addEventListener('touchend', () => {
setTimeout(() => {
article.classList.remove('is-touched');
}, 300);
});
});
}
});
</script>

View File

@@ -1,6 +1,7 @@
--- ---
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import FormattedDate from '../components/FormattedDate.astro'; import FormattedDate from '../components/FormattedDate.astro';
import TagList from '../components/TagList.astro';
import directus from '../../lib/directus'; import directus from '../../lib/directus';
import { readItems, readSingleton } from '@directus/sdk'; import { readItems, readSingleton } from '@directus/sdk';
@@ -22,22 +23,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
--- ---
<Layout title=`Home | ${global.name}`> <Layout title=`Home | ${global.name}`>
<!-- Hero Section with mobile responsiveness -->
<section <section
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20" class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
transition:animate="slide" transition:animate="slide"
> >
<div class="relative mx-auto max-w-2xl"> <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"> <div class="relative text-center sm:text-left">
<h1 <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" 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"
@@ -63,7 +53,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
> >
<a <a
href="/about" 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" 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-400 dark:hover:text-zinc-100"
> >
<span>More about me</span> <span>More about me</span>
<svg <svg
@@ -79,9 +69,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
stroke-linejoin="round" stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </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> </a>
</div> </div>
</div> </div>
@@ -103,7 +90,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</h2> </h2>
<a <a
href="/blog" 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" 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-400 dark:hover:text-zinc-100"
> >
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
View all posts View all posts
@@ -121,9 +108,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </svg>
</span> </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> </a>
</div> </div>
@@ -132,14 +116,14 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
{ {
recentPosts.map((post, index) => ( 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"> <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" /> <div class="theme-transition-all absolute -inset-x-4 -inset-y-6 z-0 border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 sm:-inset-x-6 sm:rounded-2xl dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70" />
{post.image && ( {post.image && (
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg"> <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
<img <img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title} alt={post.title}
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover"
loading={index === 0 ? 'eager' : 'lazy'} loading={index === 0 ? 'eager' : 'lazy'}
width="400" width="400"
height="225" height="225"
@@ -147,12 +131,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</div> </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"> <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 <a
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
@@ -163,45 +141,29 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</a> </a>
</h3> </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"> <p class="z-10 mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{post.description} {post.description}
</p> </p>
{post.tags && post.tags.length > 0 && ( <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start"> <FormattedDate date={post.published_date} />
{post.tags.slice(0, 3).map((tag) => ( </div>
<a
href={`/topics/${tag}`} <TagList tags={post.tags} class="z-10" />
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 <a
href={`/blog/${post.slug}`} 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" 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="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full"> <span class="relative z-10">Read article</span>
Read article <span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</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> </span>
<svg <svg
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="none" fill="none"
aria-hidden="true" aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
> >
<path <path
d="M6.75 5.75 9.25 8l-2.5 2.25" d="M6.75 5.75 9.25 8l-2.5 2.25"
@@ -224,16 +186,16 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(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"> <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="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"> <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 Popular Tags
</h2> </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"> <div class="hover-3d 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) => { {allTags.map((tag) => {
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length; const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
return ( return (
<a <a
href={`/topics/${tag}`} href={`/tags/${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" class="theme-transition-all flex min-h-[80px] flex-col rounded-xl border border-zinc-300 bg-white/50 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:bg-zinc-900/50 dark:hover:bg-zinc-800/70"
> >
<div class="mb-2 flex items-start justify-between"> <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"> <span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
@@ -250,29 +212,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
); );
})} })}
</div> </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> </div>
</section> </section>
) )
@@ -447,37 +386,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
}); });
}; };
// Run animations after the loading screen is hidden animateContent();
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> </script>
@@ -505,13 +414,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
transition: color var(--theme-transition-duration) var(--theme-transition-timing); 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 */ /* Remove the forced transition disabling which causes flickering */
:global(.theme-switching), :global(.theme-switching),
:global(.theme-switching *) { :global(.theme-switching *) {

View File

@@ -48,18 +48,9 @@ const relatedTags = [
<BaseLayout title={`Posts tagged with "${tag}"`}> <BaseLayout title={`Posts tagged with "${tag}"`}>
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
<div class="relative mb-10 sm:mb-16"> <div class="relative mb-10 sm:mb-16">
<div
class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl sm:h-48 sm:w-48 dark:bg-zinc-900/20"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<a <a
href="/tags" href="/blog#topics"
class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
> >
<svg <svg
@@ -73,9 +64,11 @@ const relatedTags = [
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
>
</path>
</svg> </svg>
<span>Back to all topics</span> <span>Back to blog</span>
<span <span
class="block h-0.5 max-w-0 bg-zinc-300 transition-all duration-300 group-hover:max-w-full dark:bg-zinc-700" class="block h-0.5 max-w-0 bg-zinc-300 transition-all duration-300 group-hover:max-w-full dark:bg-zinc-700"
></span> ></span>
@@ -99,8 +92,9 @@ const relatedTags = [
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
></path> >
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"></path> </path>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"> </path>
</svg> </svg>
</div> </div>
@@ -112,7 +106,7 @@ const relatedTags = [
<span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700" <span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
></span> ></span>
<span <span
class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100" class="animate-expand absolute -bottom-1 left-0 h-1 w-full bg-zinc-900 opacity-70 dark:bg-zinc-100"
></span> ></span>
</span> </span>
</h1> </h1>
@@ -133,14 +127,14 @@ const relatedTags = [
<!-- Related tags section --> <!-- Related tags section -->
{ {
relatedTags.length > 0 && ( relatedTags.length > 0 && (
<div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12"> <div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100"> <h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
Related topics Related topics
</h2> </h2>
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start"> <div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
{relatedTags.map((relatedTag) => ( {relatedTags.map((relatedTag) => (
<a <a
href={`/topics/${relatedTag}`} href={`/tags/${relatedTag}`}
class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
> >
#{relatedTag} #{relatedTag}
@@ -153,55 +147,31 @@ const relatedTags = [
<!-- Posts list --> <!-- Posts list -->
<div class="relative"> <div class="relative">
<div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"> <div
class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"
>
</div> </div>
<div class="relative space-y-6 sm:space-y-8"> <div class="relative space-y-6 sm:space-y-8">
{ {
sortedPosts.map((post) => ( sortedPosts.map((post) => (
<article class="hover-card group relative mx-auto flex max-w-2xl flex-col rounded-2xl border border-zinc-200 p-5 transition-all duration-300 hover:bg-zinc-50/80 hover:shadow-md sm:mx-0 sm:p-8 dark:border-zinc-800 dark:hover:bg-zinc-900/50"> <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 opacity-0 transition-opacity duration-500 group-hover:opacity-100 dark:from-zinc-900/0 dark:to-zinc-800/0" /> <div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6"> <div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{post.image && ( {post.image && (
<div class="mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl shadow-xs transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56"> <div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl sm:mx-0 sm:w-56">
<img <img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
alt={post.image_alt} alt={post.image_alt}
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover"
loading="lazy" loading="lazy"
/> />
</div> </div>
)} )}
<div class="flex-1"> <div class="z-10 flex-1">
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
{post.published_date && (
<time
datetime={post.published_date.toLocaleString()}
class="flex items-center gap-1.5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
<FormattedDate date={post.published_date} />
</time>
)}
</div>
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title} {post.title}
</a> </a>
@@ -210,15 +180,19 @@ const relatedTags = [
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"> <p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{post.description} {post.description}
</p> </p>
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<FormattedDate date={post.published_date} />
</div>
</div> </div>
</div> </div>
<div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800"> <div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
{post.tags && post.tags.length > 0 && ( {post.tags && post.tags.length > 0 && (
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start"> <div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
{post.tags.slice(0, 3).map((postTag) => ( {post.tags.slice(0, 3).map((postTag) => (
<a <a
href={`/topics/${postTag}`} href={`/blog/${postTag}`}
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${ class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
postTag === tag postTag === tag
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100' ? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
@@ -238,31 +212,24 @@ const relatedTags = [
<div class="mx-auto sm:mr-0 sm:ml-auto"> <div class="mx-auto sm:mr-0 sm:ml-auto">
<a <a
href={`/blog/${post.slug}/`} href={`/blog/${post.slug}`}
class="inline-flex items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100" 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"
aria-hidden="true"
tabindex="-1"
> >
<span class="relative inline-block overflow-hidden"> <span class="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full"> <span class="relative z-10">Read article</span>
Read article <span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</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> </span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
fill="none" fill="none"
viewBox="0 0 24 24" aria-hidden="true"
stroke-width="1.5" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
stroke="currentColor"
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
> >
<path <path
d="M6.75 5.75 9.25 8l-2.5 2.25"
stroke-width="1.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/> />
</svg> </svg>
</a> </a>
@@ -324,6 +291,61 @@ const relatedTags = [
</div> </div>
</BaseLayout> </BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
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
);
});
};
animateContent();
// Add hover effect for cards on touch devices
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');
}
});
</script>
<style> <style>
/* Grid pattern background */ /* Grid pattern background */
.bg-grid-pattern { .bg-grid-pattern {
@@ -351,7 +373,7 @@ const relatedTags = [
width: 0; width: 0;
} }
to { to {
width: 50%; width: 100%;
} }
} }
@@ -359,43 +381,22 @@ const relatedTags = [
animation: expand 1s ease-out forwards; animation: expand 1s ease-out forwards;
} }
/* Blob animation */ /* Content reveal animations */
.animate-blob { .hero-text h1,
animation: blob 7s infinite; .hero-text span,
} .hero-text p,
.hero-text ~ div,
.animation-delay-2000 { article.group {
animation-delay: 2s; opacity: 0;
} transform: translateY(20px);
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(20px, -20px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
/* Hover card effect */
.hover-card {
transform: translateY(0);
transition: transition:
transform 0.3s ease, opacity 0.8s ease,
box-shadow 0.3s ease, transform 0.8s ease;
background-color 0.3s ease;
} }
@media (hover: hover) { .animate-reveal {
.hover-card:hover { opacity: 1 !important;
transform: translateY(-2px); transform: translateY(0) !important;
}
} }
/* Line clamp for descriptions */ /* Line clamp for descriptions */

View File

@@ -1,629 +0,0 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
// Count posts for each tag and create tag objects with additional data
const tagObjects = tags.map((tag) => {
const count = posts.filter((post) => post.tags?.includes(tag)).length;
// Generate a consistent but random-looking hue for each tag
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
return {
name: tag,
count,
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
hue,
};
});
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
---
<BaseLayout title="Explore Tags">
<div
class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16"
transition:animate="slide"
>
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
<div
class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-16 -bottom-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
>
</div>
<div
class="animate-blob animation-delay-4000 theme-transition-bg absolute top-8 right-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl sm:h-32 sm:w-32 md:h-40 md:w-40 dark:bg-zinc-700/20"
>
</div>
<h1
class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl dark:text-zinc-100"
>
<span class="relative inline-block">
<span class="relative inline-block">
<span
class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-xs dark:from-zinc-800/50 dark:to-zinc-700/50"
></span>
<span class="relative">Explore</span>
</span>
{' '}
<span class="relative inline-block">
Topics
<span
class="animate-underline theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-full origin-left transform bg-gradient-to-r from-zinc-400 to-zinc-600 sm:-bottom-2 sm:h-1 dark:from-zinc-600 dark:to-zinc-400"
></span>
</span>
</span>
</h1>
<p
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 sm:text-base md:text-lg lg:text-xl dark:text-zinc-400"
>
Discover content organized by your interests
</p>
</div>
{
tags.length === 0 ? (
<div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
<div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24 dark:bg-zinc-800">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="theme-transition-color h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 md:h-12 md:w-12 dark:text-zinc-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
</svg>
</div>
<p class="theme-transition-color text-lg font-medium text-zinc-800 sm:text-xl md:text-2xl dark:text-zinc-200">
No tags found yet.
</p>
<p class="theme-transition-color mt-2 text-xs text-zinc-500 sm:text-sm md:text-base dark:text-zinc-500">
Check back later for categorized content.
</p>
</div>
) : (
<div class="flex w-full justify-center">
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-xs sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8 dark:border-zinc-800 dark:bg-zinc-900/50">
<div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
<div class="theme-transition-bg absolute -top-8 -right-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
<div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
<h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl dark:text-zinc-100">
Popular Topics
</h2>
<div class="xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 xxxs:gap-2 xxs:gap-2 xs:gap-2 grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 sm:gap-3 md:grid-cols-4 md:gap-4 lg:grid-cols-5">
{sortedTags.map((tag) => (
<a
href={`/topics/${tag.name}`}
class="theme-transition-element theme-ripple group relative min-w-0 grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl dark:border-zinc-800 dark:hover:border-zinc-700"
style={`--tag-hue: ${tag.hue};`}
>
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-xs transition-all duration-300 sm:h-8 sm:w-8 md:h-10 md:w-10 dark:bg-zinc-800 dark:text-zinc-300">
<span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
#
</span>
</div>
<div class="min-w-0 flex-1 overflow-hidden">
<h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate text-[10px] font-bold break-words hyphens-auto text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-sm md:text-base dark:text-zinc-100 dark:group-hover:text-zinc-300">
{tag.name}
</h3>
<p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 sm:text-xs md:text-xs dark:text-zinc-400">
{tag.count} article{tag.count !== 1 ? 's' : ''}
</p>
</div>
</div>
</a>
))}
</div>
</div>
</div>
)
}
</div>
</BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
const fixViewportWidth = () => {
// Force the viewport to be exactly the width of the device
const viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
}
// Fix for horizontal overflow
document.body.style.overflowX = 'hidden';
document.documentElement.style.overflowX = 'hidden';
document.documentElement.style.width = '100%';
document.body.style.width = '100%';
};
fixViewportWidth();
// Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360;
const isExtremelySmall = width < 280;
const isMicroScreen = width < 240;
// Fix container width to match viewport exactly
const container = document.querySelector('.tag-cloud');
if (container) {
container.style.maxWidth = '100vw';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
// Remove any margins that might cause overflow
container.style.marginLeft = '0';
container.style.marginRight = '0';
}
// Fix grid width
const grid = document.querySelector('.grid');
if (grid) {
grid.style.width = '100%';
grid.style.maxWidth = '100%';
}
tagItems.forEach((item) => {
// Set appropriate classes based on screen size
if (isMicroScreen) {
item.classList.add('micro-screen');
item.classList.remove('extremely-small-screen', 'very-small-screen');
} else if (isExtremelySmall) {
item.classList.add('extremely-small-screen');
item.classList.remove('very-small-screen', 'micro-screen');
} else if (isVerySmall) {
item.classList.add('very-small-screen');
item.classList.remove('extremely-small-screen', 'micro-screen');
} else {
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
}
// Ensure text doesn't overflow on small screens
const tagName = item.querySelector('h3');
const tagCount = item.querySelector('p');
if (tagName) {
// Set title for accessibility
tagName.title = tagName.textContent.trim();
// Adjust text length based on screen size
if (isMicroScreen && tagName.textContent.length > 6) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 6) + '...';
} else if (isExtremelySmall && tagName.textContent.length > 8) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 8) + '...';
} else if (isVerySmall && tagName.textContent.length > 12) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 12) + '...';
} else if (tagName.dataset.fullText) {
tagName.textContent = tagName.dataset.fullText;
delete tagName.dataset.fullText;
}
}
// Set the tag hue for hover effects
const hue = item.style.getPropertyValue('--tag-hue');
item.style.setProperty('--hover-hue', hue);
});
};
// Run on load
adjustTagItems();
// Run on resize with optimized debounce
let resizeTimer;
const handleResize = () => {
if (resizeTimer) {
window.cancelAnimationFrame(resizeTimer);
}
resizeTimer = window.requestAnimationFrame(() => {
fixViewportWidth();
adjustTagItems();
});
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// Ensure layout is recalculated after page is fully loaded
window.addEventListener('load', () => {
fixViewportWidth();
adjustTagItems();
// Force recalculation after images and fonts are loaded
setTimeout(() => {
fixViewportWidth();
adjustTagItems();
}, 500);
});
// Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues
const setVh = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVh();
window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', () => {
// Wait for orientation change to complete
setTimeout(() => {
setVh();
fixViewportWidth();
}, 100);
});
}
// Add touch support for mobile devices
const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach((item) => {
item.addEventListener(
'touchstart',
() => {
item.classList.add('touch-active');
},
{ passive: true }
);
item.addEventListener(
'touchend',
() => {
setTimeout(() => {
item.classList.remove('touch-active');
}, 150);
},
{ passive: true }
);
// Cancel active state if touch moves away
item.addEventListener(
'touchmove',
(e) => {
const touch = e.touches[0];
const rect = item.getBoundingClientRect();
if (
touch.clientX < rect.left ||
touch.clientX > rect.right ||
touch.clientY < rect.top ||
touch.clientY > rect.bottom
) {
item.classList.remove('touch-active');
}
},
{ passive: true }
);
});
};
addTouchSupport();
});
</script>
<style>
/* Base styles */
.tag-cloud {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d;
perspective: 1000px;
transition: all var(--theme-transition);
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Fix for horizontal overflow */
:global(html),
:global(body) {
overflow-x: hidden;
width: 100%;
max-width: 100%;
}
:global(.max-w-6xl) {
max-width: 100% !important;
width: 100% !important;
}
/* Micro screens (below 240px) */
@media (max-width: 239px) {
.tag-cloud {
padding: 0.5rem !important;
margin: 0 !important;
border-radius: 0.25rem !important;
width: 100% !important;
}
.tag-cloud h2 {
font-size: 0.875rem !important;
margin-bottom: 0.375rem !important;
}
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
gap: 0.375rem !important;
width: 100% !important;
}
.micro-screen .flex {
padding: 0.25rem !important;
}
.micro-screen h3 {
font-size: 0.625rem !important;
}
.micro-screen p {
font-size: 0.5rem !important;
}
}
/* Extra extra extra small screens (240px-279px) */
@media (min-width: 240px) and (max-width: 279px) {
.xxxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxxs\:w-6 {
width: 1.5rem;
}
.xxxs\:h-6 {
height: 1.5rem;
}
.xxxs\:text-xs {
font-size: 0.75rem;
}
.xxxs\:gap-2 {
gap: 0.5rem;
}
.xxxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra extra small screens (280px-359px) */
@media (min-width: 280px) {
.xxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxs\:w-6 {
width: 1.5rem;
}
.xxs\:h-6 {
height: 1.5rem;
}
.xxs\:text-xs {
font-size: 0.75rem;
}
.xxs\:gap-2 {
gap: 0.5rem;
}
.xxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra small screens (360px-639px) */
@media (min-width: 360px) {
.xs\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xs\:w-7 {
width: 1.75rem;
}
.xs\:h-7 {
height: 1.75rem;
}
.xs\:text-xs {
font-size: 0.75rem;
}
.xs\:text-sm {
font-size: 0.875rem;
}
.xs\:gap-2 {
gap: 0.5rem;
}
.xs\:text-\[10px\] {
font-size: 10px;
}
}
/* Ensure text doesn't overflow on small screens */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* Ensure proper word breaking for long tag names */
.break-words {
word-break: break-word;
overflow-wrap: break-word;
}
.hyphens-auto {
hyphens: auto;
}
/* Shadow for dark mode */
:global(.dark) .tag-cloud {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Prevent layout shifts */
.grow {
grow: 1;
}
.min-w-0 {
min-width: 0;
}
/* Ensure container doesn't overflow */
.overflow-hidden {
overflow: hidden;
}
/* Touch support for mobile */
.touch-active {
transform: scale(0.97) !important;
opacity: 0.9;
transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
}
/* Animation for blob */
@keyframes blob {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(10px, -10px) scale(1.05);
}
50% {
transform: translate(0, 20px) scale(0.95);
}
75% {
transform: translate(-10px, -10px) scale(1.05);
}
}
.animate-blob {
animation: blob 20s infinite ease-in-out;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
/* Animation for underline */
@keyframes underline {
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
.animate-underline {
animation: underline 1.5s ease-out forwards;
}
/* Fix for iOS Safari notch */
@supports (padding: max(0px)) {
.tag-cloud {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
}
}
</style>

View File

@@ -23,19 +23,7 @@ const iconSets = {
si: SiIcons, si: SiIcons,
}; };
const DynamicIcon = ({ const DynamicIcon = ({ name, set = 'fa' }: { name: string; set: string }) => {
name,
set = 'fa',
size = 20,
color = 'currentColor',
className = '',
}: {
name: string;
set: string;
size: number;
color: string;
className: string;
}) => {
let IconComponent = FaIcons.FaAlignCenter; let IconComponent = FaIcons.FaAlignCenter;
if (name.startsWith('Fa')) { if (name.startsWith('Fa')) {
@@ -46,7 +34,7 @@ const DynamicIcon = ({
IconComponent = iconSets[set][name]; IconComponent = iconSets[set][name];
} }
return <IconComponent size={size} color={color} className={className} />; return <IconComponent />;
}; };
export default DynamicIcon; export default DynamicIcon;