Compare commits
	
		
			1 Commits
		
	
	
		
			0.11.1
			...
			d917adbb77
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						d917adbb77
	
				 | 
					
					
						
@@ -1,11 +1,11 @@
 | 
			
		||||
name: process-repository
 | 
			
		||||
name: process-issues
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: "@daily"
 | 
			
		||||
    - cron: '@daily'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  process-repository:
 | 
			
		||||
  process-issues:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Python Script
 | 
			
		||||
@@ -14,27 +14,22 @@ jobs:
 | 
			
		||||
          repository: alexlebens/workflow-scripts
 | 
			
		||||
          ref: main
 | 
			
		||||
          token: ${{ secrets.BOT_TOKEN }}
 | 
			
		||||
          path: workflow-scripts
 | 
			
		||||
          path: scripts
 | 
			
		||||
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.13"
 | 
			
		||||
          python-version: '3.13'
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install requests immutabledict
 | 
			
		||||
        run: pip install requests
 | 
			
		||||
 | 
			
		||||
      - name: Run Script
 | 
			
		||||
        env:
 | 
			
		||||
          INSTANCE_URL: ${{ vars.INSTANCE_URL }}
 | 
			
		||||
          OWNER: ${{ gitea.owner }}
 | 
			
		||||
          REPOSITORY: ${{ gitea.repository }}
 | 
			
		||||
          TOKEN: ${{ secrets.BOT_TOKEN }}
 | 
			
		||||
          LOG_LEVEL: DEBUG
 | 
			
		||||
          ISSUE_STALE_DAYS: 3
 | 
			
		||||
          ISSUE_STALE_TAG: 23
 | 
			
		||||
          ISSUE_EXCLUDE_TAG: 17
 | 
			
		||||
          PULL_REQUEST_STALE_DAYS: 3
 | 
			
		||||
          PULL_REQUEST_STALE_TAG: 23
 | 
			
		||||
          PULL_REQUEST_REQUIRED_TAG: 22
 | 
			
		||||
        run: python ./workflow-scripts/process-repository.py
 | 
			
		||||
          STALE_DAYS: 3
 | 
			
		||||
          STALE_TAG: 'stale'
 | 
			
		||||
          EXCLUDE_TAG: 'renovate'
 | 
			
		||||
        run: python ./scripts/scripts/process-issues.py
 | 
			
		||||
							
								
								
									
										35
									
								
								.gitea/workflows/process-pull-requests.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.gitea/workflows/process-pull-requests.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
name: process-pull-requests
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '@daily'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  process-pull-requests:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Python Script
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          repository: alexlebens/workflow-scripts
 | 
			
		||||
          ref: main
 | 
			
		||||
          token: ${{ secrets.BOT_TOKEN }}
 | 
			
		||||
          path: scripts
 | 
			
		||||
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.13'
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install requests
 | 
			
		||||
 | 
			
		||||
      - name: Run Script
 | 
			
		||||
        env:
 | 
			
		||||
          INSTANCE_URL: ${{ vars.INSTANCE_URL }}
 | 
			
		||||
          REPOSITORY: ${{ gitea.repository }}
 | 
			
		||||
          TOKEN: ${{ secrets.BOT_TOKEN }}
 | 
			
		||||
          STALE_DAYS: 3
 | 
			
		||||
          STALE_TAG: 'stale'
 | 
			
		||||
          REQUIRED_TAG: 'automerge'
 | 
			
		||||
        run: python ./scripts/scripts/process-pull-requests.py
 | 
			
		||||
@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
      - name: Set up Node.js
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22.17.1
 | 
			
		||||
          node-version: 22.16.x
 | 
			
		||||
          cache: pnpm
 | 
			
		||||
 | 
			
		||||
      - name: Install Dependencies
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
ARG REGISTRY=docker.io
 | 
			
		||||
FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base
 | 
			
		||||
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base
 | 
			
		||||
 | 
			
		||||
LABEL version="0.11.1"
 | 
			
		||||
LABEL version="0.8.12"
 | 
			
		||||
LABEL description="Astro based personal website"
 | 
			
		||||
 | 
			
		||||
ENV PNPM_HOME="/pnpm"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2025 Lê Vĩnh Khang
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2025 Alex Lebens
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "site-profile",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "version": "0.11.1",
 | 
			
		||||
  "version": "0.8.12",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "astro dev",
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
    "@directus/sdk": "^20.0.0",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.8",
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.8",
 | 
			
		||||
    "astro": "^5.10.1",
 | 
			
		||||
    "astro": "^5.10.0",
 | 
			
		||||
    "framer-motion": "^12.16.0",
 | 
			
		||||
    "react": "^19.1.0",
 | 
			
		||||
    "react-dom": "^19.1.0",
 | 
			
		||||
@@ -31,13 +31,13 @@
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tailwindcss/typography": "^0.5.16",
 | 
			
		||||
    "@typescript-eslint/parser": "8.37.0",
 | 
			
		||||
    "eslint": "9.31.0",
 | 
			
		||||
    "eslint-config-prettier": "10.1.8",
 | 
			
		||||
    "@typescript-eslint/parser": "8.34.1",
 | 
			
		||||
    "eslint": "9.29.0",
 | 
			
		||||
    "eslint-config-prettier": "10.1.5",
 | 
			
		||||
    "eslint-plugin-astro": "1.3.1",
 | 
			
		||||
    "prettier": "^3.5.3",
 | 
			
		||||
    "prettier-plugin-astro": "^0.14.1",
 | 
			
		||||
    "prettier-plugin-tailwindcss": "^0.6.12",
 | 
			
		||||
    "typescript-eslint": "8.37.0"
 | 
			
		||||
    "typescript-eslint": "8.34.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1224
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1224
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -16,7 +16,7 @@
 | 
			
		||||
                "npm"
 | 
			
		||||
            ],
 | 
			
		||||
            "addLabels": [
 | 
			
		||||
                "dependency"
 | 
			
		||||
                "automerge"
 | 
			
		||||
            ],
 | 
			
		||||
            "automerge": false,
 | 
			
		||||
            "minimumReleaseAge": "1 days"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
 | 
			
		||||
@@ -29,19 +29,24 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Theme transition script
 | 
			
		||||
  document.addEventListener('astro:page-load', () => {
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const themeToggle = document.querySelector('[data-theme-toggle]');
 | 
			
		||||
    const overlay = document.getElementById('theme-transition-overlay');
 | 
			
		||||
 | 
			
		||||
    if (themeToggle && overlay) {
 | 
			
		||||
      themeToggle.addEventListener('click', () => {
 | 
			
		||||
        // Add transitioning class to optimize performance
 | 
			
		||||
        document.documentElement.classList.add('theme-transitioning');
 | 
			
		||||
 | 
			
		||||
        // Fade in overlay
 | 
			
		||||
        overlay.style.opacity = '0.15';
 | 
			
		||||
        overlay.style.transition = 'opacity 0.3s ease';
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          // Fade out overlay
 | 
			
		||||
          overlay.style.opacity = '0';
 | 
			
		||||
 | 
			
		||||
          // Remove transitioning class after animation completes
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            document.documentElement.classList.remove('theme-transitioning');
 | 
			
		||||
          }, 700);
 | 
			
		||||
@@ -55,13 +60,13 @@
 | 
			
		||||
  /* Grid pattern for dots */
 | 
			
		||||
  .bg-grid-pattern {
 | 
			
		||||
    background-size: 24px 24px;
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
 | 
			
		||||
    transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Dark mode version */
 | 
			
		||||
  :global(.dark) .bg-grid-pattern {
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(255, 255, 255, 0.15) 1px, transparent 1px);
 | 
			
		||||
    background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Ambient glow animations */
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links'));
 | 
			
		||||
const currentYear = new Date().getFullYear();
 | 
			
		||||
 | 
			
		||||
const navLinks = [
 | 
			
		||||
  { text: 'Home', href: '/' },
 | 
			
		||||
  { text: 'Blog', href: '/blog' },
 | 
			
		||||
  { text: 'About', href: '/about' },
 | 
			
		||||
  { text: 'RSS', href: '/rss' },
 | 
			
		||||
  { text: 'Blog', href: '/blog' },
 | 
			
		||||
  { text: 'Topics', href: '/topics' },
 | 
			
		||||
  { text: 'RSS', href: '/rss.xml' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const socialLinks = [
 | 
			
		||||
@@ -35,7 +35,6 @@ const socialLinks = [
 | 
			
		||||
 | 
			
		||||
<footer
 | 
			
		||||
  class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
 | 
			
		||||
  transition:animate="none"
 | 
			
		||||
>
 | 
			
		||||
  <div class="pointer-events-none absolute inset-0 overflow-hidden">
 | 
			
		||||
    <div
 | 
			
		||||
@@ -54,26 +53,28 @@ const socialLinks = [
 | 
			
		||||
 | 
			
		||||
  <div class="relative px-4 pt-16 pb-12 sm:px-6">
 | 
			
		||||
    <div class="mx-auto max-w-4xl">
 | 
			
		||||
      <!-- Main footer content -->
 | 
			
		||||
      <div class="grid grid-cols-1 gap-10 md:grid-cols-12">
 | 
			
		||||
        <!-- Brand section -->
 | 
			
		||||
        <div class="col-span-1 md:col-span-3">
 | 
			
		||||
          <a href="/" class="group inline-block">
 | 
			
		||||
            <div class="flex items-center">
 | 
			
		||||
              <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 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 group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
 | 
			
		||||
              >
 | 
			
		||||
                <span
 | 
			
		||||
                  class="theme-transition-all text-xl font-bold text-zinc-100 duration-300 dark:text-zinc-900"
 | 
			
		||||
                  class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
 | 
			
		||||
                  >{global.initals}</span
 | 
			
		||||
                >
 | 
			
		||||
                  {global.initals}
 | 
			
		||||
                </span>
 | 
			
		||||
                <div class="absolute inset-0"></div>
 | 
			
		||||
                <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>
 | 
			
		||||
              <span
 | 
			
		||||
                class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
 | 
			
		||||
                >Blog</span
 | 
			
		||||
              >
 | 
			
		||||
                Blog
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </a>
 | 
			
		||||
 | 
			
		||||
@@ -126,6 +127,7 @@ const socialLinks = [
 | 
			
		||||
                  >
 | 
			
		||||
                    <span class="relative inline-block overflow-hidden">
 | 
			
		||||
                      <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>
 | 
			
		||||
                  </a>
 | 
			
		||||
                </li>
 | 
			
		||||
@@ -144,8 +146,8 @@ const socialLinks = [
 | 
			
		||||
 | 
			
		||||
          <div class="flex items-center space-x-2">
 | 
			
		||||
            <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
 | 
			
		||||
              >Built with
 | 
			
		||||
            </span>
 | 
			
		||||
              >Built with</span
 | 
			
		||||
            >
 | 
			
		||||
            <a
 | 
			
		||||
              href="https://astro.build"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
@@ -172,8 +174,7 @@ const socialLinks = [
 | 
			
		||||
                Astro
 | 
			
		||||
                <span
 | 
			
		||||
                  class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
 | 
			
		||||
                >
 | 
			
		||||
                </span>
 | 
			
		||||
                ></span>
 | 
			
		||||
              </span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,22 +10,7 @@ const parsedDate = typeof date === 'string' ? new Date(date) : date;
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  parsedDate && (
 | 
			
		||||
    <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>
 | 
			
		||||
    <time datetime={parsedDate.toISOString()}>
 | 
			
		||||
      {parsedDate.toLocaleDateString('en-us', {
 | 
			
		||||
        year: 'numeric',
 | 
			
		||||
        month: 'long',
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,12 @@ import directus from '../../lib/directus';
 | 
			
		||||
import { readSingleton } from '@directus/sdk';
 | 
			
		||||
 | 
			
		||||
const global = await directus.request(readSingleton('global'));
 | 
			
		||||
const links = await directus.request(readSingleton('links'));
 | 
			
		||||
 | 
			
		||||
const navItems = [
 | 
			
		||||
  { text: 'Home', href: '/' },
 | 
			
		||||
  { text: 'Blog', href: '/blog' },
 | 
			
		||||
  { text: 'Topics', href: '/topics' },
 | 
			
		||||
  { text: 'About', href: '/about' },
 | 
			
		||||
  { text: 'Gitea', href: links.gitea },
 | 
			
		||||
  { text: 'RSS', href: 'rss.xml' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +20,6 @@ const currentPath = pathname.slice(1);
 | 
			
		||||
 | 
			
		||||
<header
 | 
			
		||||
  class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
 | 
			
		||||
  transition:animate="none"
 | 
			
		||||
>
 | 
			
		||||
  <div class="mx-auto flex max-w-3xl items-center justify-between px-4">
 | 
			
		||||
    <!-- Logo -->
 | 
			
		||||
@@ -37,8 +35,8 @@ const currentPath = pathname.slice(1);
 | 
			
		||||
              href={item.href}
 | 
			
		||||
              class={`text-sm font-medium ${
 | 
			
		||||
                isActive
 | 
			
		||||
                  ? 'text-zinc-900 dark:text-zinc-100'
 | 
			
		||||
                  : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
 | 
			
		||||
                  ? 'text-zinc-900 dark:text-white'
 | 
			
		||||
                  : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
 | 
			
		||||
              }`}
 | 
			
		||||
            >
 | 
			
		||||
              {item.text}
 | 
			
		||||
@@ -102,8 +100,8 @@ const currentPath = pathname.slice(1);
 | 
			
		||||
            href={item.href}
 | 
			
		||||
            class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
 | 
			
		||||
              isActive
 | 
			
		||||
                ? 'text-zinc-900 dark:text-zinc-100'
 | 
			
		||||
                : 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
 | 
			
		||||
                ? 'text-zinc-900 dark:text-white'
 | 
			
		||||
                : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
 | 
			
		||||
            }`}
 | 
			
		||||
            style={`transition-delay: ${index * 0.05}s;`}
 | 
			
		||||
          >
 | 
			
		||||
@@ -123,7 +121,7 @@ const currentPath = pathname.slice(1);
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Mobile menu toggle with animations
 | 
			
		||||
  document.addEventListener('astro:page-load', () => {
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const mobileMenuButton = document.getElementById('mobile-menu-button');
 | 
			
		||||
    const closeMenuButton = document.getElementById('close-menu-button');
 | 
			
		||||
    const mobileMenu = document.getElementById('mobile-menu');
 | 
			
		||||
 
 | 
			
		||||
@@ -29,12 +29,10 @@ const encodedUrl = encodeURIComponent(url);
 | 
			
		||||
        stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round"
 | 
			
		||||
        class="h-4 w-4"
 | 
			
		||||
      >
 | 
			
		||||
        <path
 | 
			
		||||
        ><path
 | 
			
		||||
          d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
 | 
			
		||||
        >
 | 
			
		||||
        </path>
 | 
			
		||||
      </svg>
 | 
			
		||||
        ></path></svg
 | 
			
		||||
      >
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
 | 
			
		||||
@@ -52,9 +50,8 @@ const encodedUrl = encodeURIComponent(url);
 | 
			
		||||
        stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round"
 | 
			
		||||
        class="h-4 w-4"
 | 
			
		||||
        ><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
 | 
			
		||||
      >
 | 
			
		||||
        <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
 | 
			
		||||
@@ -72,12 +69,10 @@ const encodedUrl = encodeURIComponent(url);
 | 
			
		||||
        stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round"
 | 
			
		||||
        class="h-4 w-4"
 | 
			
		||||
        ><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
 | 
			
		||||
        ></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
 | 
			
		||||
        ></circle></svg
 | 
			
		||||
      >
 | 
			
		||||
        <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z">
 | 
			
		||||
        </path>
 | 
			
		||||
        <rect x="2" y="9" width="4" height="12"></rect>
 | 
			
		||||
        <circle cx="4" cy="4" r="2"></circle>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </a>
 | 
			
		||||
    <button
 | 
			
		||||
      id="copy-link-button"
 | 
			
		||||
@@ -94,10 +89,9 @@ const encodedUrl = encodeURIComponent(url);
 | 
			
		||||
        stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round"
 | 
			
		||||
        class="h-4 w-4"
 | 
			
		||||
        ><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
 | 
			
		||||
          d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
 | 
			
		||||
      >
 | 
			
		||||
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"> </path>
 | 
			
		||||
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"> </path>
 | 
			
		||||
      </svg>
 | 
			
		||||
      <span
 | 
			
		||||
        id="copy-tooltip"
 | 
			
		||||
        class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
 | 
			
		||||
@@ -107,3 +101,75 @@ const encodedUrl = encodeURIComponent(url);
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Function to handle copy link button
 | 
			
		||||
  function setupCopyLinkButton() {
 | 
			
		||||
    const copyButtons = document.querySelectorAll('#copy-link-button');
 | 
			
		||||
 | 
			
		||||
    copyButtons.forEach((button) => {
 | 
			
		||||
      button.addEventListener('click', () => {
 | 
			
		||||
        // Get the current URL
 | 
			
		||||
        const url = window.location.href;
 | 
			
		||||
 | 
			
		||||
        // Copy to clipboard
 | 
			
		||||
        navigator.clipboard
 | 
			
		||||
          .writeText(url)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            // Show tooltip
 | 
			
		||||
            const tooltip = button.querySelector('#copy-tooltip');
 | 
			
		||||
            if (tooltip) {
 | 
			
		||||
              tooltip.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
              // Hide tooltip after 2 seconds
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                tooltip.classList.remove('opacity-100');
 | 
			
		||||
              }, 2000);
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          .catch((err) => {
 | 
			
		||||
            console.error('Failed to copy: ', err);
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set up the copy link button when the DOM is loaded
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
 | 
			
		||||
 | 
			
		||||
  // Also set up when the page content is updated via SPA navigation
 | 
			
		||||
  document.addEventListener('astro:page-load', setupCopyLinkButton);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with the custom page transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupCopyLinkButton);
 | 
			
		||||
 | 
			
		||||
  // Handle SPA transitions for share links
 | 
			
		||||
  function setupSpaTransitions() {
 | 
			
		||||
    // Get all share links
 | 
			
		||||
    const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
 | 
			
		||||
 | 
			
		||||
    // Make sure external share links don't trigger page transitions
 | 
			
		||||
    shareLinks.forEach((link) => {
 | 
			
		||||
      link.setAttribute('data-spa-external', 'true');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize SPA transitions
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSpaTransitions);
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSpaTransitions);
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSpaTransitions);
 | 
			
		||||
 | 
			
		||||
  // Dispatch custom event when share action is completed
 | 
			
		||||
  function notifyShareComplete() {
 | 
			
		||||
    document.dispatchEvent(new CustomEvent('share-action-complete'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add analytics tracking for share actions if needed
 | 
			
		||||
  function trackShareAction(platform) {
 | 
			
		||||
    // You can implement analytics tracking here
 | 
			
		||||
    console.log(`Shared on ${platform}`);
 | 
			
		||||
 | 
			
		||||
    // Notify other components that share action is complete
 | 
			
		||||
    notifyShareComplete();
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,21 +8,16 @@ const { tags = [], class: className = '' } = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  tags && (
 | 
			
		||||
    <div class={`mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start ${className}`}>
 | 
			
		||||
      {tags.slice(0, 2).map((postTag) => (
 | 
			
		||||
  tags.length > 0 && (
 | 
			
		||||
    <div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
 | 
			
		||||
      {tags.map((tag) => (
 | 
			
		||||
        <a
 | 
			
		||||
          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-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700`}
 | 
			
		||||
          href={`/tag/${tag}`}
 | 
			
		||||
          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"
 | 
			
		||||
        >
 | 
			
		||||
          #{postTag}
 | 
			
		||||
          {tag}
 | 
			
		||||
        </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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,25 +47,24 @@
 | 
			
		||||
  ></span>
 | 
			
		||||
</button>
 | 
			
		||||
 | 
			
		||||
<script is:inline>
 | 
			
		||||
  // Use a function to persist theme when using SPA transitions
 | 
			
		||||
  // https://docs.astro.build/en/guides/view-transitions/#script-re-execution
 | 
			
		||||
  function applyTheme() {
 | 
			
		||||
    localStorage.theme === 'dark'
 | 
			
		||||
      ? document.documentElement.classList.add('dark')
 | 
			
		||||
      : document.documentElement.classList.remove('dark');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.addEventListener('astro:after-swap', applyTheme);
 | 
			
		||||
 | 
			
		||||
  applyTheme();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Use a function to handle theme toggle to ensure it can be called from anywhere
 | 
			
		||||
  function setupThemeToggle() {
 | 
			
		||||
    const themeToggles = document.querySelectorAll('[data-theme-toggle]');
 | 
			
		||||
 | 
			
		||||
    // Check for dark mode preference at the system level
 | 
			
		||||
    const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
 | 
			
		||||
 | 
			
		||||
    // Check for saved theme preference or use the system preference
 | 
			
		||||
    const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
 | 
			
		||||
 | 
			
		||||
    // Apply the theme on initial load
 | 
			
		||||
    if (currentTheme === 'dark') {
 | 
			
		||||
      document.documentElement.classList.add('dark');
 | 
			
		||||
    } else {
 | 
			
		||||
      document.documentElement.classList.remove('dark');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create theme switch overlay element if it doesn't exist
 | 
			
		||||
    if (!document.querySelector('.theme-switch-overlay')) {
 | 
			
		||||
      const overlay = document.createElement('div');
 | 
			
		||||
@@ -185,7 +184,7 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Run setup on load
 | 
			
		||||
  document.addEventListener('astro:page-load', setupThemeToggle);
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupThemeToggle);
 | 
			
		||||
 | 
			
		||||
  // Also run on page visibility change to ensure theme is consistent
 | 
			
		||||
  document.addEventListener('visibilitychange', () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/layouts/Base.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/layouts/Base.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
import Layout from './Layout.astro';
 | 
			
		||||
 | 
			
		||||
import directus from '../../lib/directus';
 | 
			
		||||
import { readSingleton } from '@directus/sdk';
 | 
			
		||||
 | 
			
		||||
const global = await directus.request(readSingleton('global'));
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  title: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
}
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<Layout title={global.title} description={global.title}>
 | 
			
		||||
  <slot />
 | 
			
		||||
</Layout>
 | 
			
		||||
@@ -15,3 +15,45 @@ export interface Props {
 | 
			
		||||
<Layout title={global.title} description={global.title}>
 | 
			
		||||
  <slot />
 | 
			
		||||
</Layout>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const themeToggle = document.getElementById('theme-toggle');
 | 
			
		||||
 | 
			
		||||
    if (themeToggle) {
 | 
			
		||||
      themeToggle.addEventListener('click', () => {
 | 
			
		||||
        document.documentElement.classList.add('theme-switching');
 | 
			
		||||
 | 
			
		||||
        const rippleElements = document.querySelectorAll('.theme-ripple');
 | 
			
		||||
        rippleElements.forEach((el) => {
 | 
			
		||||
          el.classList.add('ripple-active');
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            el.classList.remove('ripple-active');
 | 
			
		||||
          }, 600);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const event = new CustomEvent('themeChange', {
 | 
			
		||||
          detail: {
 | 
			
		||||
            theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        document.dispatchEvent(event);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          document.documentElement.classList.remove('theme-switching');
 | 
			
		||||
        }, 600);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const socialLinks = document.querySelectorAll('.social-link');
 | 
			
		||||
    socialLinks.forEach((link) => {
 | 
			
		||||
      link.addEventListener('mouseenter', () => {
 | 
			
		||||
        link.classList.add('hover-active');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('mouseleave', () => {
 | 
			
		||||
        link.classList.remove('hover-active');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ export async function getStaticPaths() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const post = Astro.props;
 | 
			
		||||
const published_date: string = post.published_date.toLocaleString();
 | 
			
		||||
 | 
			
		||||
let canonicalURL;
 | 
			
		||||
try {
 | 
			
		||||
@@ -30,30 +31,18 @@ try {
 | 
			
		||||
 | 
			
		||||
<Layout title={post.title} description={post.description}>
 | 
			
		||||
  <article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl">
 | 
			
		||||
    <div class="hero-text mb-12">
 | 
			
		||||
    <div class="mb-12">
 | 
			
		||||
      <h1
 | 
			
		||||
        class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
 | 
			
		||||
      >
 | 
			
		||||
        {post.title}
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <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"
 | 
			
		||||
      >
 | 
			
		||||
        {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 class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
        <FormattedDate date={published_date} />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <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>
 | 
			
		||||
      <TagList tags={post.tags} class="mt-2" />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Hero image -->
 | 
			
		||||
@@ -81,6 +70,7 @@ try {
 | 
			
		||||
    <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
 | 
			
		||||
      <div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
 | 
			
		||||
        <ShareButtons url={canonicalURL.toString()} title={post.title} />
 | 
			
		||||
        <!-- Convert URL to string -->
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@@ -97,60 +87,285 @@ try {
 | 
			
		||||
</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'
 | 
			
		||||
  //  Blog post SPA transitions
 | 
			
		||||
  function setupBlogPostTransitions() {
 | 
			
		||||
    // Animate article entrance
 | 
			
		||||
    const article = document.querySelector('article');
 | 
			
		||||
    if (article) {
 | 
			
		||||
      article.classList.add('article-entering');
 | 
			
		||||
 | 
			
		||||
      // Remove class after animation completes
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        article.classList.remove('article-entering');
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure consistent code block styling
 | 
			
		||||
    function updateCodeBlockStyles() {
 | 
			
		||||
      document.querySelectorAll('pre').forEach((pre) => {
 | 
			
		||||
        // Force the background color with !important for both light and dark mode
 | 
			
		||||
        pre.setAttribute('style', 'background-color: #1e293b !important');
 | 
			
		||||
 | 
			
		||||
        // Also apply to any nested code elements
 | 
			
		||||
        const codeElements = pre.querySelectorAll('code');
 | 
			
		||||
        codeElements.forEach((code) => {
 | 
			
		||||
          code.setAttribute(
 | 
			
		||||
            'style',
 | 
			
		||||
            'background-color: transparent !important; color: #e5e7eb !important;'
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initial application
 | 
			
		||||
    updateCodeBlockStyles();
 | 
			
		||||
 | 
			
		||||
    // Watch for theme changes
 | 
			
		||||
    const observer = new MutationObserver(() => {
 | 
			
		||||
      updateCodeBlockStyles();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
 | 
			
		||||
 | 
			
		||||
    // Also run on any content changes that might add new code blocks
 | 
			
		||||
    const contentObserver = new MutationObserver((mutations) => {
 | 
			
		||||
      for (const mutation of mutations) {
 | 
			
		||||
        if (mutation.addedNodes.length) {
 | 
			
		||||
          updateCodeBlockStyles();
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    contentObserver.observe(document.body, { childList: true, subtree: true });
 | 
			
		||||
 | 
			
		||||
    // Clean up observers when navigating away
 | 
			
		||||
    document.addEventListener('spa-navigation-start', () => {
 | 
			
		||||
      observer.disconnect();
 | 
			
		||||
      contentObserver.disconnect();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Remove the parallax effect for hero image
 | 
			
		||||
 | 
			
		||||
    // Handle prev/next navigation links
 | 
			
		||||
    const navLinks = document.querySelectorAll('.blog-nav-link');
 | 
			
		||||
    navLinks.forEach((link) => {
 | 
			
		||||
      if (!link.hasAttribute('data-spa-handled')) {
 | 
			
		||||
        link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
        link.addEventListener('mouseenter', () => {
 | 
			
		||||
          link.classList.add('nav-link-hover');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        link.addEventListener('mouseleave', () => {
 | 
			
		||||
          link.classList.remove('nav-link-hover');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Animate headings when they enter the viewport
 | 
			
		||||
    const animateHeadings = () => {
 | 
			
		||||
      const headings = document.querySelectorAll('article h2, article h3');
 | 
			
		||||
 | 
			
		||||
      const observer = new IntersectionObserver(
 | 
			
		||||
        (entries) => {
 | 
			
		||||
          entries.forEach((entry) => {
 | 
			
		||||
            if (entry.isIntersecting) {
 | 
			
		||||
              entry.target.classList.add('heading-visible');
 | 
			
		||||
              observer.unobserve(entry.target);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          threshold: 0.2,
 | 
			
		||||
          rootMargin: '0px 0px -100px 0px',
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      heroElements.forEach((el, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            el.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          100 + index * 150
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      headings.forEach((heading) => {
 | 
			
		||||
        heading.classList.add('heading-animated');
 | 
			
		||||
        observer.observe(heading);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate posts with staggered delay
 | 
			
		||||
      const articles = document.querySelectorAll('article.group');
 | 
			
		||||
      articles.forEach((article, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            article.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          500 + index * 150
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
      return observer;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    animateContent();
 | 
			
		||||
  });
 | 
			
		||||
    // Initialize heading animations
 | 
			
		||||
    const headingObserver = animateHeadings();
 | 
			
		||||
 | 
			
		||||
    // Enhance code blocks with syntax highlighting and copy button
 | 
			
		||||
    function enhanceCodeBlocks() {
 | 
			
		||||
      const codeBlocks = document.querySelectorAll('pre code');
 | 
			
		||||
 | 
			
		||||
      codeBlocks.forEach((codeBlock) => {
 | 
			
		||||
        // Skip if already processed
 | 
			
		||||
        if (codeBlock.parentElement.classList.contains('enhanced')) return;
 | 
			
		||||
 | 
			
		||||
        // Mark as enhanced
 | 
			
		||||
        codeBlock.parentElement.classList.add('enhanced');
 | 
			
		||||
 | 
			
		||||
        // Create copy button
 | 
			
		||||
        const copyButton = document.createElement('button');
 | 
			
		||||
        copyButton.className = 'copy-code-button';
 | 
			
		||||
        copyButton.innerHTML = `
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
            <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
 | 
			
		||||
            <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        // Add copy functionality
 | 
			
		||||
        copyButton.addEventListener('click', () => {
 | 
			
		||||
          const code = codeBlock.textContent;
 | 
			
		||||
          navigator.clipboard.writeText(code);
 | 
			
		||||
 | 
			
		||||
          // Show copied feedback
 | 
			
		||||
          copyButton.innerHTML = `
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
              <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          `;
 | 
			
		||||
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            copyButton.innerHTML = `
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
 | 
			
		||||
                <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
 | 
			
		||||
              </svg>
 | 
			
		||||
            `;
 | 
			
		||||
          }, 2000);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add copy button to pre element
 | 
			
		||||
        codeBlock.parentElement.appendChild(copyButton);
 | 
			
		||||
 | 
			
		||||
        // Fix line numbers implementation
 | 
			
		||||
        const codeText = codeBlock.textContent;
 | 
			
		||||
        const lines = codeText.split('\n');
 | 
			
		||||
 | 
			
		||||
        const lineNumbers = document.createElement('div');
 | 
			
		||||
        lineNumbers.className = 'line-numbers';
 | 
			
		||||
 | 
			
		||||
        // Always include all lines, including empty ones
 | 
			
		||||
        for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
          const lineNumber = document.createElement('span');
 | 
			
		||||
          lineNumber.textContent = i + 1;
 | 
			
		||||
          lineNumbers.appendChild(lineNumber);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        codeBlock.parentElement.classList.add('with-line-numbers');
 | 
			
		||||
        codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
 | 
			
		||||
 | 
			
		||||
        // Fix language label detection and display
 | 
			
		||||
        const className = codeBlock.className;
 | 
			
		||||
        const languageMatch = className.match(/language-(\w+)/);
 | 
			
		||||
 | 
			
		||||
        if (languageMatch && languageMatch[1]) {
 | 
			
		||||
          const language = languageMatch[1];
 | 
			
		||||
 | 
			
		||||
          // Add language label at top right
 | 
			
		||||
          const languageLabel = document.createElement('div');
 | 
			
		||||
          languageLabel.className = 'language-label';
 | 
			
		||||
          languageLabel.textContent = language;
 | 
			
		||||
          codeBlock.parentElement.appendChild(languageLabel);
 | 
			
		||||
 | 
			
		||||
          // Add language badge at bottom right with markdown syntax
 | 
			
		||||
          const languageBadge = document.createElement('div');
 | 
			
		||||
          languageBadge.className = 'language-badge';
 | 
			
		||||
          languageBadge.textContent = `\`\`\`${language}`;
 | 
			
		||||
          languageBadge.style.position = 'absolute';
 | 
			
		||||
          languageBadge.style.bottom = '0.5rem';
 | 
			
		||||
          languageBadge.style.right = '0.5rem';
 | 
			
		||||
          languageBadge.style.fontSize = '0.7rem';
 | 
			
		||||
          languageBadge.style.padding = '0.1rem 0.3rem';
 | 
			
		||||
          languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
 | 
			
		||||
          languageBadge.style.color = '#e5e7eb';
 | 
			
		||||
          languageBadge.style.borderRadius = '0.25rem';
 | 
			
		||||
          languageBadge.style.fontFamily =
 | 
			
		||||
            'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
 | 
			
		||||
          languageBadge.style.zIndex = '10';
 | 
			
		||||
          codeBlock.parentElement.appendChild(languageBadge);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Enhance tables with better styling
 | 
			
		||||
    function enhanceTables() {
 | 
			
		||||
      const tables = document.querySelectorAll('.markdown-content table');
 | 
			
		||||
 | 
			
		||||
      tables.forEach((table) => {
 | 
			
		||||
        if (table.classList.contains('enhanced-table')) return;
 | 
			
		||||
 | 
			
		||||
        table.classList.add('enhanced-table');
 | 
			
		||||
 | 
			
		||||
        // Wrap table in responsive container
 | 
			
		||||
        const wrapper = document.createElement('div');
 | 
			
		||||
        wrapper.className = 'table-container';
 | 
			
		||||
        table.parentNode.insertBefore(wrapper, table);
 | 
			
		||||
        wrapper.appendChild(table);
 | 
			
		||||
 | 
			
		||||
        // Add zebra striping to rows
 | 
			
		||||
        const rows = table.querySelectorAll('tbody tr');
 | 
			
		||||
        rows.forEach((row, index) => {
 | 
			
		||||
          if (index % 2 === 0) {
 | 
			
		||||
            row.classList.add('even-row');
 | 
			
		||||
          } else {
 | 
			
		||||
            row.classList.add('odd-row');
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Enhance blockquotes with icons
 | 
			
		||||
    function enhanceBlockquotes() {
 | 
			
		||||
      const blockquotes = document.querySelectorAll('.markdown-content blockquote');
 | 
			
		||||
 | 
			
		||||
      blockquotes.forEach((blockquote) => {
 | 
			
		||||
        if (blockquote.classList.contains('enhanced-quote')) return;
 | 
			
		||||
 | 
			
		||||
        blockquote.classList.add('enhanced-quote');
 | 
			
		||||
 | 
			
		||||
        // Add quote icon
 | 
			
		||||
        const icon = document.createElement('div');
 | 
			
		||||
        icon.className = 'quote-icon';
 | 
			
		||||
        icon.innerHTML = `
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        blockquote.insertBefore(icon, blockquote.firstChild);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run all enhancements
 | 
			
		||||
    enhanceCodeBlocks();
 | 
			
		||||
    enhanceTables();
 | 
			
		||||
    enhanceBlockquotes();
 | 
			
		||||
 | 
			
		||||
    // Clean up observers when navigating away
 | 
			
		||||
    document.addEventListener('spa-navigation-start', () => {
 | 
			
		||||
      if (headingObserver) {
 | 
			
		||||
        headingObserver.disconnect();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupBlogPostTransitions);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupBlogPostTransitions);
 | 
			
		||||
 | 
			
		||||
  // Also initialize when SPA navigation completes
 | 
			
		||||
  document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<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 */
 | 
			
		||||
  /* Enhanced hero image styling */
 | 
			
		||||
  article img:first-of-type {
 | 
			
		||||
    border-radius: 1rem;
 | 
			
		||||
    box-shadow:
 | 
			
		||||
@@ -162,4 +377,22 @@ try {
 | 
			
		||||
  article img:first-of-type:hover {
 | 
			
		||||
    transform: scale(1.01);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Article entrance animation */
 | 
			
		||||
  .article-entering {
 | 
			
		||||
    animation: article-fade-in 0.8s ease-out forwards;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes article-fade-in {
 | 
			
		||||
    from {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      transform: translateY(10px);
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
      transform: translateY(0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Rest of the styles remain unchanged... */
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
import { ClientRouter } from 'astro:transitions';
 | 
			
		||||
 | 
			
		||||
import Navigation from '../components/Navigation.astro';
 | 
			
		||||
import Footer from '../components/Footer.astro';
 | 
			
		||||
import Background from '../components/Background.astro';
 | 
			
		||||
 | 
			
		||||
import '../styles/global.css';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@@ -30,30 +27,19 @@ const { title, description } = Astro.props;
 | 
			
		||||
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
    />
 | 
			
		||||
    <!-- Load theme early to prevent flashes between light and dark modes -->
 | 
			
		||||
    <script is:inline>
 | 
			
		||||
      const theme = (() => {
 | 
			
		||||
        if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
 | 
			
		||||
          return localStorage.getItem('theme');
 | 
			
		||||
        }
 | 
			
		||||
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
 | 
			
		||||
          return 'dark';
 | 
			
		||||
        }
 | 
			
		||||
        return 'light';
 | 
			
		||||
      })();
 | 
			
		||||
 | 
			
		||||
      if (theme === 'light') {
 | 
			
		||||
        document.documentElement.classList.remove('dark');
 | 
			
		||||
      } else {
 | 
			
		||||
        document.documentElement.classList.add('dark');
 | 
			
		||||
      }
 | 
			
		||||
      window.localStorage.setItem('theme', theme);
 | 
			
		||||
    </script>
 | 
			
		||||
    <ClientRouter />
 | 
			
		||||
  </head>
 | 
			
		||||
  <body
 | 
			
		||||
    class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Page transition overlay - for smooth transitions between pages -->
 | 
			
		||||
    <div
 | 
			
		||||
      id="page-transition"
 | 
			
		||||
      class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="transition-spinner"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Background component with dot pattern and ambient glow -->
 | 
			
		||||
    <Background />
 | 
			
		||||
 | 
			
		||||
    <div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
 | 
			
		||||
@@ -63,10 +49,262 @@ const { title, description } = Astro.props;
 | 
			
		||||
      </main>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Footer />
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
      // SPA transition system with history API
 | 
			
		||||
      document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        const mainContent = document.querySelector('main');
 | 
			
		||||
 | 
			
		||||
        // Initialize content with entrance animation
 | 
			
		||||
        if (mainContent) {
 | 
			
		||||
          mainContent.classList.add('content-entering');
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            mainContent.classList.remove('content-entering');
 | 
			
		||||
          }, 800);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Function to load content via fetch
 | 
			
		||||
        async function loadContent(url) {
 | 
			
		||||
          try {
 | 
			
		||||
            // Show transition overlay
 | 
			
		||||
            if (pageTransition) {
 | 
			
		||||
              pageTransition.classList.remove('opacity-0', 'pointer-events-none');
 | 
			
		||||
              pageTransition.classList.add('opacity-100');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Fade out current content
 | 
			
		||||
            if (mainContent) {
 | 
			
		||||
              mainContent.style.opacity = '0';
 | 
			
		||||
              mainContent.style.transform = 'translateY(10px)';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Fetch the new page content
 | 
			
		||||
            const response = await fetch(url);
 | 
			
		||||
            if (!response.ok) throw new Error(`Failed to fetch ${url}`);
 | 
			
		||||
            const html = await response.text();
 | 
			
		||||
 | 
			
		||||
            // Create a temporary element to parse the HTML
 | 
			
		||||
            const parser = new DOMParser();
 | 
			
		||||
            const doc = parser.parseFromString(html, 'text/html');
 | 
			
		||||
 | 
			
		||||
            // Extract the main content
 | 
			
		||||
            const newContent = doc.querySelector('main');
 | 
			
		||||
            if (!newContent) throw new Error('Could not find main content in the fetched page');
 | 
			
		||||
 | 
			
		||||
            // Extract the title
 | 
			
		||||
            const newTitle = doc.querySelector('title');
 | 
			
		||||
            if (newTitle) {
 | 
			
		||||
              document.title = newTitle.textContent;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Extract meta description
 | 
			
		||||
            const newDescription = doc.querySelector('meta[name="description"]');
 | 
			
		||||
            if (newDescription) {
 | 
			
		||||
              const currentDescription = document.querySelector('meta[name="description"]');
 | 
			
		||||
              if (currentDescription) {
 | 
			
		||||
                currentDescription.setAttribute(
 | 
			
		||||
                  'content',
 | 
			
		||||
                  newDescription.getAttribute('content') || ''
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Wait a bit for transition effect
 | 
			
		||||
            await new Promise((resolve) => setTimeout(resolve, 300));
 | 
			
		||||
 | 
			
		||||
            // Replace the content
 | 
			
		||||
            if (mainContent && newContent) {
 | 
			
		||||
              mainContent.innerHTML = newContent.innerHTML;
 | 
			
		||||
 | 
			
		||||
              // Run scripts in the new content
 | 
			
		||||
              Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
 | 
			
		||||
                const newScript = document.createElement('script');
 | 
			
		||||
                Array.from(oldScript.attributes).forEach((attr) => {
 | 
			
		||||
                  newScript.setAttribute(attr.name, attr.value);
 | 
			
		||||
                });
 | 
			
		||||
                newScript.textContent = oldScript.textContent;
 | 
			
		||||
                if (oldScript.parentNode) {
 | 
			
		||||
                  mainContent.appendChild(newScript);
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Fade in new content with animation
 | 
			
		||||
            if (mainContent) {
 | 
			
		||||
              mainContent.style.opacity = '0';
 | 
			
		||||
              mainContent.style.transform = 'translateY(10px)';
 | 
			
		||||
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
 | 
			
		||||
                mainContent.style.opacity = '1';
 | 
			
		||||
                mainContent.style.transform = 'translateY(0)';
 | 
			
		||||
 | 
			
		||||
                // Add entrance animation class
 | 
			
		||||
                mainContent.classList.add('content-entering');
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                  mainContent.classList.remove('content-entering');
 | 
			
		||||
                }, 800);
 | 
			
		||||
              }, 50);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Hide transition overlay
 | 
			
		||||
            if (pageTransition) {
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                pageTransition.classList.add('opacity-0', 'pointer-events-none');
 | 
			
		||||
                pageTransition.classList.remove('opacity-100');
 | 
			
		||||
              }, 200);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Dispatch custom event for content loaded
 | 
			
		||||
            document.dispatchEvent(
 | 
			
		||||
              new CustomEvent('spa-content-loaded', {
 | 
			
		||||
                detail: { url },
 | 
			
		||||
              })
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Scroll to top or to saved position
 | 
			
		||||
            window.scrollTo(0, 0);
 | 
			
		||||
 | 
			
		||||
            // Re-attach event listeners to new content
 | 
			
		||||
            attachLinkListeners();
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            console.error('Error loading content:', error);
 | 
			
		||||
 | 
			
		||||
            // Fallback to traditional navigation on error
 | 
			
		||||
            window.location.href = url;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Function to attach event listeners to all links
 | 
			
		||||
        function attachLinkListeners() {
 | 
			
		||||
          document.querySelectorAll('a').forEach((link) => {
 | 
			
		||||
            // Skip links that are already handled, anchor links, external links, or have special attributes
 | 
			
		||||
            if (
 | 
			
		||||
              link.hasAttribute('data-spa-handled') ||
 | 
			
		||||
              !link.href.startsWith(window.location.origin) ||
 | 
			
		||||
              link.href.includes('#') ||
 | 
			
		||||
              link.hasAttribute('target') ||
 | 
			
		||||
              link.hasAttribute('download') ||
 | 
			
		||||
              link.getAttribute('rel') === 'external' ||
 | 
			
		||||
              link.getAttribute('rel') === 'nofollow'
 | 
			
		||||
            ) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Mark as handled to avoid duplicate listeners
 | 
			
		||||
            link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
            link.addEventListener('click', (e) => {
 | 
			
		||||
              // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
              if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              e.preventDefault();
 | 
			
		||||
              const targetHref = link.href;
 | 
			
		||||
 | 
			
		||||
              // Don't transition if clicking the current page
 | 
			
		||||
              if (targetHref === window.location.href) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // Update browser history
 | 
			
		||||
              window.history.pushState({ path: targetHref }, '', targetHref);
 | 
			
		||||
 | 
			
		||||
              // Load the new content
 | 
			
		||||
              loadContent(targetHref);
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initial attachment of link listeners
 | 
			
		||||
        attachLinkListeners();
 | 
			
		||||
 | 
			
		||||
        // Handle browser back/forward navigation
 | 
			
		||||
        window.addEventListener('popstate', (e) => {
 | 
			
		||||
          if (e.state && e.state.path) {
 | 
			
		||||
            loadContent(e.state.path);
 | 
			
		||||
          } else {
 | 
			
		||||
            loadContent(window.location.href);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Check RSS feed availability
 | 
			
		||||
        const checkAndGenerateRSS = async () => {
 | 
			
		||||
          try {
 | 
			
		||||
            const response = await fetch('/rss.xml');
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
              console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.');
 | 
			
		||||
            }
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            console.warn('Could not check RSS feed status.');
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Check RSS feed availability
 | 
			
		||||
        checkAndGenerateRSS();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Theme handling with transition effects
 | 
			
		||||
      function setupThemeHandling() {
 | 
			
		||||
        // Apply theme from localStorage or system preference
 | 
			
		||||
        const theme = localStorage.getItem('theme');
 | 
			
		||||
        if (
 | 
			
		||||
          theme === 'dark' ||
 | 
			
		||||
          (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
 | 
			
		||||
        ) {
 | 
			
		||||
          document.documentElement.classList.add('dark');
 | 
			
		||||
        } else {
 | 
			
		||||
          document.documentElement.classList.remove('dark');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for theme changes
 | 
			
		||||
        document.addEventListener('themeChanged', () => {
 | 
			
		||||
          // Add transition class to body
 | 
			
		||||
          document.body.classList.add('theme-transitioning');
 | 
			
		||||
 | 
			
		||||
          // Remove class after transition completes
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            document.body.classList.remove('theme-transitioning');
 | 
			
		||||
          }, 500);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Initialize theme handling
 | 
			
		||||
      document.addEventListener('DOMContentLoaded', setupThemeHandling);
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Page transition effects */
 | 
			
		||||
  #page-transition {
 | 
			
		||||
    transition: opacity 0.3s ease;
 | 
			
		||||
    backdrop-filter: blur-sm(4px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Transition spinner animation */
 | 
			
		||||
  .transition-spinner {
 | 
			
		||||
    width: 30px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
    border: 2px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    border-top-color: #3b82f6;
 | 
			
		||||
    animation: spin 0.7s linear infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :global(.dark) .transition-spinner {
 | 
			
		||||
    border-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
    border-top-color: #60a5fa;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes spin {
 | 
			
		||||
    to {
 | 
			
		||||
      transform: rotate(360deg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Content entrance animation */
 | 
			
		||||
  main {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								src/layouts/TransitionLayout.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/layouts/TransitionLayout.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
---
 | 
			
		||||
import { ClientRouter } from 'astro:transitions';
 | 
			
		||||
import BaseLayout from './BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
const { title, description } = Astro.props;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={title} description={description}>
 | 
			
		||||
  <ClientRouter fallback="swap" />
 | 
			
		||||
 | 
			
		||||
  <div transition:animate="slide">
 | 
			
		||||
    <slot />
 | 
			
		||||
  </div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Custom transition styles */
 | 
			
		||||
  ::view-transition-old(root) {
 | 
			
		||||
    animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ::view-transition-new(root) {
 | 
			
		||||
    animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -5,8 +5,23 @@ import Layout from '../layouts/Layout.astro';
 | 
			
		||||
<Layout title="404 - Page Not Found">
 | 
			
		||||
  <div
 | 
			
		||||
    class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
 | 
			
		||||
    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 -->
 | 
			
		||||
    <div class="relative z-10 mx-auto max-w-xl">
 | 
			
		||||
      <div class="glitch-wrapper">
 | 
			
		||||
@@ -33,8 +48,7 @@ import Layout from '../layouts/Layout.astro';
 | 
			
		||||
        >
 | 
			
		||||
          <span
 | 
			
		||||
            class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
 | 
			
		||||
          >
 | 
			
		||||
          </span>
 | 
			
		||||
          ></span>
 | 
			
		||||
          <svg
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            fill="none"
 | 
			
		||||
@@ -47,8 +61,7 @@ import Layout from '../layouts/Layout.astro';
 | 
			
		||||
              stroke-linecap="round"
 | 
			
		||||
              stroke-linejoin="round"
 | 
			
		||||
              d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
 | 
			
		||||
            >
 | 
			
		||||
            </path>
 | 
			
		||||
            ></path>
 | 
			
		||||
          </svg>
 | 
			
		||||
          <span class="relative z-10 font-medium">Return Home</span>
 | 
			
		||||
        </a>
 | 
			
		||||
@@ -68,9 +81,7 @@ import Layout from '../layouts/Layout.astro';
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="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>
 | 
			
		||||
          <span class="font-medium">Go Back</span>
 | 
			
		||||
        </button>
 | 
			
		||||
@@ -116,9 +127,97 @@ import Layout from '../layouts/Layout.astro';
 | 
			
		||||
    const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
 | 
			
		||||
    funFactElement.textContent = randomFact;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle SPA transitions for 404 page
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all internal links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links, external links, or already processed
 | 
			
		||||
      if (
 | 
			
		||||
        link.getAttribute('href').includes('#') ||
 | 
			
		||||
        link.getAttribute('target') === '_blank' ||
 | 
			
		||||
        link.hasAttribute('data-spa-handled')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Re-initialize back button after SPA navigation
 | 
			
		||||
    const backButton = document.getElementById('back-button');
 | 
			
		||||
    if (backButton) {
 | 
			
		||||
      backButton.addEventListener('click', () => {
 | 
			
		||||
        window.history.back();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<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-wrapper {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
import DynamicIcon from '../utils/DynamicIcon.tsx';
 | 
			
		||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
 | 
			
		||||
import { SiTypescript, SiAstro } from 'react-icons/si';
 | 
			
		||||
 | 
			
		||||
import directus from '../../lib/directus';
 | 
			
		||||
import { readSingleton, readItems } from '@directus/sdk';
 | 
			
		||||
@@ -16,16 +17,23 @@ const skills = await directus.request(
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="About Me" description={global.description}>
 | 
			
		||||
  <div
 | 
			
		||||
    class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"
 | 
			
		||||
    transition:animate="slide"
 | 
			
		||||
  >
 | 
			
		||||
  <div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
 | 
			
		||||
    <!-- Hero Section -->
 | 
			
		||||
    <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="hero-text order-2 text-center md:order-1 md:text-left">
 | 
			
		||||
        <div class="order-2 text-center md:order-1 md:text-left">
 | 
			
		||||
          <h1
 | 
			
		||||
            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"
 | 
			
		||||
            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"
 | 
			
		||||
          >
 | 
			
		||||
            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"
 | 
			
		||||
@@ -34,7 +42,7 @@ const skills = await directus.request(
 | 
			
		||||
          </h1>
 | 
			
		||||
 | 
			
		||||
          <p
 | 
			
		||||
            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"
 | 
			
		||||
            class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
 | 
			
		||||
          >
 | 
			
		||||
            {about.background}
 | 
			
		||||
          </p>
 | 
			
		||||
@@ -57,6 +65,13 @@ const skills = await directus.request(
 | 
			
		||||
              loading="eager"
 | 
			
		||||
            />
 | 
			
		||||
          </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>
 | 
			
		||||
@@ -76,22 +91,16 @@ const skills = await directus.request(
 | 
			
		||||
          ></span>
 | 
			
		||||
        </h2>
 | 
			
		||||
 | 
			
		||||
        <div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none">
 | 
			
		||||
          <p
 | 
			
		||||
            class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
 | 
			
		||||
          >
 | 
			
		||||
        <div class="theme-transition-all 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">
 | 
			
		||||
            {about.experience}
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p
 | 
			
		||||
            class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
 | 
			
		||||
          >
 | 
			
		||||
          <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
 | 
			
		||||
            {about.education}
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p
 | 
			
		||||
            class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
 | 
			
		||||
          >
 | 
			
		||||
          <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
 | 
			
		||||
            {about.certifications}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -110,16 +119,13 @@ const skills = await directus.request(
 | 
			
		||||
        <!-- Main slider container -->
 | 
			
		||||
        <div class="slider-track animate-slide flex">
 | 
			
		||||
          {
 | 
			
		||||
            [...skills, ...skills, ...skills].map((skill, index) => (
 | 
			
		||||
              <div
 | 
			
		||||
                key={`${skill.title}-${index}`}
 | 
			
		||||
                class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"
 | 
			
		||||
              >
 | 
			
		||||
            skills.map((skill, index) => (
 | 
			
		||||
              <div class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600">
 | 
			
		||||
                <div class="p-4 sm:p-6">
 | 
			
		||||
                  <div class="mb-4 flex items-center justify-between sm:mb-6">
 | 
			
		||||
                    <div class="flex items-center gap-2 sm:gap-4">
 | 
			
		||||
                      <div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
 | 
			
		||||
                        <DynamicIcon name={skill.icon} />
 | 
			
		||||
                        <skill.icon />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
 | 
			
		||||
                        {skill.title}
 | 
			
		||||
@@ -158,6 +164,7 @@ const skills = await directus.request(
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Contact Section -->
 | 
			
		||||
    <div class="theme-transition-all mx-auto max-w-3xl text-center">
 | 
			
		||||
      <h2
 | 
			
		||||
@@ -171,62 +178,205 @@ const skills = await directus.request(
 | 
			
		||||
        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.
 | 
			
		||||
      </p>
 | 
			
		||||
      <div class="group">
 | 
			
		||||
        <a
 | 
			
		||||
          href=`mailto:${global.email}`
 | 
			
		||||
          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"
 | 
			
		||||
 | 
			
		||||
      <a
 | 
			
		||||
        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"
 | 
			
		||||
      >
 | 
			
		||||
        <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"
 | 
			
		||||
        >
 | 
			
		||||
          <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
 | 
			
		||||
              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>
 | 
			
		||||
          <path
 | 
			
		||||
            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>
 | 
			
		||||
        Say Hello
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</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
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
<style>
 | 
			
		||||
  /* Blob animation */
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob-bounce 8s infinite ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    animateContent();
 | 
			
		||||
  .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 */
 | 
			
		||||
  .slider-track {
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
    animation: scroll 40s linear infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes scroll {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: translateX(0);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (min-width: 640px) {
 | 
			
		||||
    .slider-track {
 | 
			
		||||
      animation: scroll 60s linear infinite;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes scroll {
 | 
			
		||||
      0% {
 | 
			
		||||
        transform: translateX(0);
 | 
			
		||||
      }
 | 
			
		||||
      100% {
 | 
			
		||||
        transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tech-stack-slider:hover .slider-track {
 | 
			
		||||
    animation-play-state: paused;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card {
 | 
			
		||||
    transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card:hover {
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Reduce animation complexity on mobile for better performance */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    .skill-card {
 | 
			
		||||
      transition:
 | 
			
		||||
        transform 0.3s ease,
 | 
			
		||||
        box-shadow 0.3s ease;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .skill-card:hover {
 | 
			
		||||
      transform: translateY(-5px) !important;
 | 
			
		||||
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card:before {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -10%;
 | 
			
		||||
    left: -10%;
 | 
			
		||||
    width: 120%;
 | 
			
		||||
    height: 120%;
 | 
			
		||||
    background: radial-gradient(
 | 
			
		||||
      circle at center,
 | 
			
		||||
      rgba(255, 255, 255, 0.1) 0%,
 | 
			
		||||
      rgba(255, 255, 255, 0) 70%
 | 
			
		||||
    );
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity 0.5s ease;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card:hover:before {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress-bar-animate {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress-bar-animate:after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: -100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
 | 
			
		||||
    animation: progress-shine 2s infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes progress-shine {
 | 
			
		||||
    0% {
 | 
			
		||||
      left: -100%;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      left: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Improved touch targets for mobile */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    a,
 | 
			
		||||
    button {
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .social-link {
 | 
			
		||||
      min-width: 44px;
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Theme transition effect */
 | 
			
		||||
  :global(.theme-switching) .theme-transition-element {
 | 
			
		||||
    animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Smooth card transition during theme switch */
 | 
			
		||||
  .skill-card.theme-transition-element {
 | 
			
		||||
    transition:
 | 
			
		||||
      background-color var(--theme-transition),
 | 
			
		||||
      border-color var(--theme-transition),
 | 
			
		||||
      color var(--theme-transition),
 | 
			
		||||
      box-shadow var(--theme-transition),
 | 
			
		||||
      transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Wait for the DOM to be fully loaded
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const sliderTrack = document.querySelector('.slider-track');
 | 
			
		||||
 | 
			
		||||
    // Create seamless infinite scrolling effect
 | 
			
		||||
    const sliderTrack = document.querySelector('.slider-track');
 | 
			
		||||
    function setupInfiniteScroll() {
 | 
			
		||||
      const cards = document.querySelectorAll('.skill-card');
 | 
			
		||||
      if (!cards.length) return;
 | 
			
		||||
 | 
			
		||||
      // Clone the first set of cards and append to create seamless loop
 | 
			
		||||
      const firstSetCount = cards.length / 3; // We have 3 sets in the markup
 | 
			
		||||
 | 
			
		||||
      // Set proper animation based on screen size
 | 
			
		||||
      function updateScrollAnimation() {
 | 
			
		||||
        if (window.innerWidth >= 640) {
 | 
			
		||||
@@ -311,7 +461,9 @@ const skills = await directus.request(
 | 
			
		||||
 | 
			
		||||
    // Handle theme transition
 | 
			
		||||
    document.addEventListener('themeChange', () => {
 | 
			
		||||
      // Add special effects during theme transition
 | 
			
		||||
      cards.forEach((card, index) => {
 | 
			
		||||
        // Add staggered animation delay
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          card.classList.add('theme-changing');
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
@@ -323,155 +475,103 @@ const skills = await directus.request(
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Tech Stack Slider */
 | 
			
		||||
  .slider-track {
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
    animation: scroll 40s linear infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes scroll {
 | 
			
		||||
    0% {
 | 
			
		||||
      transform: translateX(0);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (min-width: 640px) {
 | 
			
		||||
    .slider-track {
 | 
			
		||||
      animation: scroll 60s linear infinite;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes scroll {
 | 
			
		||||
      0% {
 | 
			
		||||
        transform: translateX(0);
 | 
			
		||||
<script>
 | 
			
		||||
  // Handle SPA transitions for about page
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all internal links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links, external links, or already processed
 | 
			
		||||
      if (
 | 
			
		||||
        link.getAttribute('href').includes('#') ||
 | 
			
		||||
        link.getAttribute('target') === '_blank' ||
 | 
			
		||||
        link.hasAttribute('data-spa-handled')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      100% {
 | 
			
		||||
        transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Initialize animations for about page
 | 
			
		||||
    function animateAboutContent() {
 | 
			
		||||
      // Animate hero section elements
 | 
			
		||||
      const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
 | 
			
		||||
      heroElements.forEach((el, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            el.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          100 + index * 150
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate profile image
 | 
			
		||||
      const profileImage = document.querySelector('.aspect-square');
 | 
			
		||||
      if (profileImage) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          profileImage.classList.add('animate-reveal');
 | 
			
		||||
        }, 200);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tech-stack-slider:hover .slider-track {
 | 
			
		||||
    animation-play-state: paused;
 | 
			
		||||
  }
 | 
			
		||||
      // Animate skill bars with staggered delay
 | 
			
		||||
      const skillBars = document.querySelectorAll('.skill-bar');
 | 
			
		||||
      skillBars.forEach((bar, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            bar.classList.add('animate-skill');
 | 
			
		||||
          },
 | 
			
		||||
          500 + index * 100
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  .skill-card {
 | 
			
		||||
    transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card:hover {
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Reduce animation complexity on mobile */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    .skill-card {
 | 
			
		||||
      transition:
 | 
			
		||||
        transform 0.3s ease,
 | 
			
		||||
        box-shadow 0.3s ease;
 | 
			
		||||
      // Animate sections with staggered delay
 | 
			
		||||
      const sections = document.querySelectorAll('section');
 | 
			
		||||
      sections.forEach((section, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            section.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          300 + index * 200
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .skill-card:hover {
 | 
			
		||||
      transform: translateY(-5px) !important;
 | 
			
		||||
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
 | 
			
		||||
    }
 | 
			
		||||
    // Run animations
 | 
			
		||||
    animateAboutContent();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .skill-card:before {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -10%;
 | 
			
		||||
    left: -10%;
 | 
			
		||||
    width: 120%;
 | 
			
		||||
    height: 120%;
 | 
			
		||||
    background: radial-gradient(
 | 
			
		||||
      circle at center,
 | 
			
		||||
      rgba(255, 255, 255, 0.1) 0%,
 | 
			
		||||
      rgba(255, 255, 255, 0) 70%
 | 
			
		||||
    );
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity 0.5s ease;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  .skill-card:hover:before {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  .progress-bar-animate {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress-bar-animate:after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: -100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
 | 
			
		||||
    animation: progress-shine 2s infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes progress-shine {
 | 
			
		||||
    0% {
 | 
			
		||||
      left: -100%;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      left: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Touch targets for mobile */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    a,
 | 
			
		||||
    button {
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .social-link {
 | 
			
		||||
      min-width: 44px;
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 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 */
 | 
			
		||||
  :global(.theme-switching) .theme-transition-element {
 | 
			
		||||
    animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Smooth card transition during theme switch */
 | 
			
		||||
  .skill-card.theme-transition-element {
 | 
			
		||||
    transition:
 | 
			
		||||
      background-color var(--theme-transition),
 | 
			
		||||
      border-color var(--theme-transition),
 | 
			
		||||
      color var(--theme-transition),
 | 
			
		||||
      box-shadow var(--theme-transition),
 | 
			
		||||
      transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -41,14 +41,14 @@ const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
  updated_date={post.updated_date}
 | 
			
		||||
  tags={post.tags}
 | 
			
		||||
>
 | 
			
		||||
  <!-- Main Content -->
 | 
			
		||||
  <!-- Main Content - Enhanced with better typography and spacing -->
 | 
			
		||||
  <div
 | 
			
		||||
    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"
 | 
			
		||||
    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"
 | 
			
		||||
  >
 | 
			
		||||
    <div set:html={post.content} />
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <!-- Next/Previous Navigation -->
 | 
			
		||||
  <!-- Next/Previous Navigation - Improved responsive design -->
 | 
			
		||||
  <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"
 | 
			
		||||
  >
 | 
			
		||||
@@ -116,62 +116,7 @@ const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
</BlogPost>
 | 
			
		||||
 | 
			
		||||
<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();
 | 
			
		||||
  });
 | 
			
		||||
  // Removing TOC-related functions
 | 
			
		||||
 | 
			
		||||
  // Add copy buttons to code blocks
 | 
			
		||||
  function initializeCodeCopyButtons() {
 | 
			
		||||
@@ -238,9 +183,50 @@ const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle SPA transitions for blog post navigation
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle prev/next navigation links
 | 
			
		||||
    document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links or already processed
 | 
			
		||||
      if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Main initialization function
 | 
			
		||||
  function initializeBlogPost() {
 | 
			
		||||
    // Initialize remaining components
 | 
			
		||||
    initializeCodeCopyButtons();
 | 
			
		||||
    setupSPATransitions();
 | 
			
		||||
 | 
			
		||||
    // Scroll to hash if present in URL
 | 
			
		||||
    if (window.location.hash) {
 | 
			
		||||
@@ -253,28 +239,18 @@ const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', initializeBlogPost);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', initializeBlogPost);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', initializeBlogPost);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* 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;
 | 
			
		||||
  }
 | 
			
		||||
  /* Removing TOC-related styles */
 | 
			
		||||
 | 
			
		||||
  /* Language badge styling */
 | 
			
		||||
  .language-badge {
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
---
 | 
			
		||||
import Layout from '../layouts/Layout.astro';
 | 
			
		||||
import FormattedDate from '../components/FormattedDate.astro';
 | 
			
		||||
import TagList from '../components/TagList.astro';
 | 
			
		||||
 | 
			
		||||
import directus from '../../lib/directus';
 | 
			
		||||
import { readItems, readSingleton } from '@directus/sdk';
 | 
			
		||||
@@ -23,11 +22,19 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<Layout title=`Home | ${global.name}`>
 | 
			
		||||
  <section
 | 
			
		||||
    class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
 | 
			
		||||
    transition:animate="slide"
 | 
			
		||||
  >
 | 
			
		||||
  <!-- Hero Section with improved mobile responsiveness -->
 | 
			
		||||
  <section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
 | 
			
		||||
    <div class="relative mx-auto max-w-2xl">
 | 
			
		||||
      <!-- Adjusted blob positions and sizes for better mobile appearance -->
 | 
			
		||||
      <div
 | 
			
		||||
        class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50"
 | 
			
		||||
      >
 | 
			
		||||
      </div>
 | 
			
		||||
      <div
 | 
			
		||||
        class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-64 sm:w-64 dark:bg-zinc-800/30"
 | 
			
		||||
      >
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="relative text-center sm:text-left">
 | 
			
		||||
        <h1
 | 
			
		||||
          class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
 | 
			
		||||
@@ -53,7 +60,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
        >
 | 
			
		||||
          <a
 | 
			
		||||
            href="/about"
 | 
			
		||||
            class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-100"
 | 
			
		||||
            class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
 | 
			
		||||
          >
 | 
			
		||||
            <span>More about me</span>
 | 
			
		||||
            <svg
 | 
			
		||||
@@ -69,13 +76,16 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span
 | 
			
		||||
              class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
 | 
			
		||||
            ></span>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
  <!-- Featured post section -->
 | 
			
		||||
  <!-- Featured Post Section - Improved for mobile -->
 | 
			
		||||
  <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"
 | 
			
		||||
  >
 | 
			
		||||
@@ -90,7 +100,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
        </h2>
 | 
			
		||||
        <a
 | 
			
		||||
          href="/blog"
 | 
			
		||||
          class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-400 dark:hover:text-zinc-100"
 | 
			
		||||
          class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-100 dark:hover:text-zinc-300"
 | 
			
		||||
        >
 | 
			
		||||
          <span class="flex items-center gap-1">
 | 
			
		||||
            View all posts
 | 
			
		||||
@@ -108,22 +118,25 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
                d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span
 | 
			
		||||
            class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
 | 
			
		||||
          ></span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Grid for mobile layout -->
 | 
			
		||||
      <!-- Improved grid for better mobile layout -->
 | 
			
		||||
      <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
 | 
			
		||||
        {
 | 
			
		||||
          recentPosts.map((post, index) => (
 | 
			
		||||
            <article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
 | 
			
		||||
              <div class="theme-transition-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" />
 | 
			
		||||
              <div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl dark:bg-zinc-800/50" />
 | 
			
		||||
 | 
			
		||||
              {post.image && (
 | 
			
		||||
                <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
 | 
			
		||||
                    alt={post.title}
 | 
			
		||||
                    class="h-full w-full object-cover"
 | 
			
		||||
                    class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
 | 
			
		||||
                    loading={index === 0 ? 'eager' : 'lazy'}
 | 
			
		||||
                    width="400"
 | 
			
		||||
                    height="225"
 | 
			
		||||
@@ -131,6 +144,12 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 sm:justify-start sm:gap-x-4 dark:text-zinc-400">
 | 
			
		||||
                <time datetime={post.published_date.toLocaleString()} class="font-medium">
 | 
			
		||||
                  <FormattedDate date={post.published_date} />
 | 
			
		||||
                </time>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`/blog/${post.slug}`}
 | 
			
		||||
@@ -141,29 +160,45 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
                </a>
 | 
			
		||||
              </h3>
 | 
			
		||||
 | 
			
		||||
              <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">
 | 
			
		||||
              <p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 sm:mt-3 sm:text-left dark:text-zinc-400">
 | 
			
		||||
                {post.description}
 | 
			
		||||
              </p>
 | 
			
		||||
 | 
			
		||||
              <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>
 | 
			
		||||
 | 
			
		||||
              <TagList tags={post.tags} class="z-10" />
 | 
			
		||||
              {post.tags && post.tags.length > 0 && (
 | 
			
		||||
                <div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
 | 
			
		||||
                  {post.tags.slice(0, 3).map((tag) => (
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={`/topics/${tag}`}
 | 
			
		||||
                      class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 sm:px-3 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
 | 
			
		||||
                    >
 | 
			
		||||
                      #{tag}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  {post.tags.length > 3 && (
 | 
			
		||||
                    <span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
 | 
			
		||||
                      +{post.tags.length - 3} more
 | 
			
		||||
                    </span>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <a
 | 
			
		||||
                href={`/blog/${post.slug}`}
 | 
			
		||||
                class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
 | 
			
		||||
              >
 | 
			
		||||
                <span class="relative inline-block overflow-hidden">
 | 
			
		||||
                  <span class="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 class="block transition-transform duration-300 group-hover:-translate-y-full">
 | 
			
		||||
                    Read article
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
 | 
			
		||||
                    Explore now
 | 
			
		||||
                  </span>
 | 
			
		||||
                </span>
 | 
			
		||||
                <svg
 | 
			
		||||
                  viewBox="0 0 16 16"
 | 
			
		||||
                  fill="none"
 | 
			
		||||
                  aria-hidden="true"
 | 
			
		||||
                  class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
 | 
			
		||||
                  class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
 | 
			
		||||
                >
 | 
			
		||||
                  <path
 | 
			
		||||
                    d="M6.75 5.75 9.25 8l-2.5 2.25"
 | 
			
		||||
@@ -180,22 +215,22 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
  <!-- Topics section -->
 | 
			
		||||
  <!-- Topics/Tags Section - Improved for mobile -->
 | 
			
		||||
  {
 | 
			
		||||
    allTags.length > 0 && (
 | 
			
		||||
      <section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
 | 
			
		||||
        <div class="mx-auto max-w-3xl">
 | 
			
		||||
          <h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
 | 
			
		||||
            Popular Tags
 | 
			
		||||
            Explore Topics
 | 
			
		||||
          </h2>
 | 
			
		||||
 | 
			
		||||
          <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">
 | 
			
		||||
          <div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
 | 
			
		||||
            {allTags.map((tag) => {
 | 
			
		||||
              const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
 | 
			
		||||
              return (
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`/tags/${tag}`}
 | 
			
		||||
                  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"
 | 
			
		||||
                  href={`/topics/${tag}`}
 | 
			
		||||
                  class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/70"
 | 
			
		||||
                >
 | 
			
		||||
                  <div class="mb-2 flex items-start justify-between">
 | 
			
		||||
                    <span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
 | 
			
		||||
@@ -212,6 +247,29 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="mt-6 text-center sm:mt-8">
 | 
			
		||||
            <a
 | 
			
		||||
              href="/tags"
 | 
			
		||||
              class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
 | 
			
		||||
            >
 | 
			
		||||
              <span>View all topics</span>
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                stroke-width="1.5"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
 | 
			
		||||
              >
 | 
			
		||||
                <path
 | 
			
		||||
                  stroke-linecap="round"
 | 
			
		||||
                  stroke-linejoin="round"
 | 
			
		||||
                  d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
 | 
			
		||||
                />
 | 
			
		||||
              </svg>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </section>
 | 
			
		||||
    )
 | 
			
		||||
@@ -220,7 +278,8 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Add hover effect for cards on touch devices
 | 
			
		||||
  document.addEventListener('astro:page-load', () => {
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Check if it's a touch device
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
@@ -238,11 +297,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Disable hover animations on touch devices
 | 
			
		||||
      // Disable hover animations on touch devices for better performance
 | 
			
		||||
      document.documentElement.classList.add('touch-device');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Viewport height fix for mobile browsers
 | 
			
		||||
    // Improved viewport height fix for mobile browsers
 | 
			
		||||
    const setVh = () => {
 | 
			
		||||
      const vh = window.innerHeight * 0.01;
 | 
			
		||||
      document.documentElement.style.setProperty('--vh', `${vh}px`);
 | 
			
		||||
@@ -280,7 +339,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Theme change handler that preserves scroll position and provides smoother transitions
 | 
			
		||||
    // Improved theme change handler that preserves scroll position and provides smoother transitions
 | 
			
		||||
    document.addEventListener('themeChanged', () => {
 | 
			
		||||
      // Store current scroll position
 | 
			
		||||
      const scrollPosition = window.scrollY;
 | 
			
		||||
@@ -386,8 +445,90 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    animateContent();
 | 
			
		||||
    // Run animations after the loading screen is hidden
 | 
			
		||||
    const loadingScreen = document.getElementById('loading-screen');
 | 
			
		||||
    if (loadingScreen) {
 | 
			
		||||
      // Check if loading screen is already hidden (page refresh)
 | 
			
		||||
      if (loadingScreen.style.display === 'none') {
 | 
			
		||||
        animateContent();
 | 
			
		||||
      } else {
 | 
			
		||||
        // Wait for loading screen to hide
 | 
			
		||||
        const observer = new MutationObserver((mutations) => {
 | 
			
		||||
          mutations.forEach((mutation) => {
 | 
			
		||||
            if (
 | 
			
		||||
              mutation.target === loadingScreen &&
 | 
			
		||||
              mutation.type === 'attributes' &&
 | 
			
		||||
              mutation.attributeName === 'style' &&
 | 
			
		||||
              loadingScreen.style.display === 'none'
 | 
			
		||||
            ) {
 | 
			
		||||
              animateContent();
 | 
			
		||||
              observer.disconnect();
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        observer.observe(loadingScreen, { attributes: true });
 | 
			
		||||
 | 
			
		||||
        // Fallback
 | 
			
		||||
        setTimeout(animateContent, 3500);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // If loading screen doesn't exist for some reason
 | 
			
		||||
      animateContent();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // SPA transition handling for homepage
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all internal links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links, external links, or already processed
 | 
			
		||||
      if (
 | 
			
		||||
        link.getAttribute('href').includes('#') ||
 | 
			
		||||
        link.getAttribute('target') === '_blank' ||
 | 
			
		||||
        link.hasAttribute('data-spa-handled')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
@@ -414,6 +555,13 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
    transition: color var(--theme-transition-duration) var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Ensure transitions apply to all theme-related properties */
 | 
			
		||||
  :global(*) {
 | 
			
		||||
    transition-property: background-color, border-color, color, fill, stroke, opacity;
 | 
			
		||||
    transition-duration: var(--theme-transition-duration);
 | 
			
		||||
    transition-timing-function: var(--theme-transition-timing);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Remove the forced transition disabling which causes flickering */
 | 
			
		||||
  :global(.theme-switching),
 | 
			
		||||
  :global(.theme-switching *) {
 | 
			
		||||
@@ -438,4 +586,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
 | 
			
		||||
    opacity: 1 !important;
 | 
			
		||||
    transform: translateY(0) !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Rest of your existing styles... */
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ export async function getStaticPaths() {
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Get all unique tags
 | 
			
		||||
  const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
 | 
			
		||||
 | 
			
		||||
  // Create a path for each tag
 | 
			
		||||
@@ -40,17 +41,28 @@ const sortedPosts =
 | 
			
		||||
    : [];
 | 
			
		||||
console.log(`Sorted posts length: ${sortedPosts.length}`);
 | 
			
		||||
 | 
			
		||||
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
 | 
			
		||||
const relatedTags = [
 | 
			
		||||
  ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
 | 
			
		||||
].slice(0, 5);
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={`Posts tagged with "${tag}"`}>
 | 
			
		||||
  <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16" transition:animate="slide" >
 | 
			
		||||
  <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
 | 
			
		||||
    <!-- Header section -->
 | 
			
		||||
    <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">
 | 
			
		||||
        <a
 | 
			
		||||
          href="/blog#topics"
 | 
			
		||||
          href="/tags"
 | 
			
		||||
          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
 | 
			
		||||
@@ -64,11 +76,9 @@ const relatedTags = [
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="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>
 | 
			
		||||
          <span>Back to blog</span>
 | 
			
		||||
          <span>Back to all topics</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"
 | 
			
		||||
          ></span>
 | 
			
		||||
@@ -92,9 +102,8 @@ const relatedTags = [
 | 
			
		||||
                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>
 | 
			
		||||
              <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>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@@ -106,7 +115,7 @@ const relatedTags = [
 | 
			
		||||
              <span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
 | 
			
		||||
              ></span>
 | 
			
		||||
              <span
 | 
			
		||||
                class="animate-expand absolute -bottom-1 left-0 h-1 w-full bg-zinc-900 opacity-70 dark:bg-zinc-100"
 | 
			
		||||
                class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100"
 | 
			
		||||
              ></span>
 | 
			
		||||
            </span>
 | 
			
		||||
          </h1>
 | 
			
		||||
@@ -127,14 +136,14 @@ const relatedTags = [
 | 
			
		||||
    <!-- Related tags section -->
 | 
			
		||||
    {
 | 
			
		||||
      relatedTags.length > 0 && (
 | 
			
		||||
        <div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
 | 
			
		||||
        <div class="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">
 | 
			
		||||
            Related topics
 | 
			
		||||
          </h2>
 | 
			
		||||
          <div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
 | 
			
		||||
            {relatedTags.map((relatedTag) => (
 | 
			
		||||
              <a
 | 
			
		||||
                href={`/tags/${relatedTag}`}
 | 
			
		||||
                href={`/topics/${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"
 | 
			
		||||
              >
 | 
			
		||||
                #{relatedTag}
 | 
			
		||||
@@ -147,31 +156,55 @@ const relatedTags = [
 | 
			
		||||
 | 
			
		||||
    <!-- Posts list -->
 | 
			
		||||
    <div class="relative">
 | 
			
		||||
      <div
 | 
			
		||||
        class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"
 | 
			
		||||
      >
 | 
			
		||||
      <div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10">
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="relative space-y-6 sm:space-y-8">
 | 
			
		||||
        {
 | 
			
		||||
          sortedPosts.map((post) => (
 | 
			
		||||
            <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" />
 | 
			
		||||
            <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">
 | 
			
		||||
              <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="flex flex-col gap-5 sm:flex-row sm:gap-6">
 | 
			
		||||
                {post.image && (
 | 
			
		||||
                  <div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl sm:mx-0 sm:w-56">
 | 
			
		||||
                  <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">
 | 
			
		||||
                    <img
 | 
			
		||||
                      src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
 | 
			
		||||
                      alt={post.image_alt}
 | 
			
		||||
                      class="h-full w-full object-cover"
 | 
			
		||||
                      class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
 | 
			
		||||
                      loading="lazy"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                <div class="z-10 flex-1">
 | 
			
		||||
                  <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">
 | 
			
		||||
                <div class="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">
 | 
			
		||||
                    {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">
 | 
			
		||||
                      {post.title}
 | 
			
		||||
                    </a>
 | 
			
		||||
@@ -180,19 +213,15 @@ 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">
 | 
			
		||||
                    {post.description}
 | 
			
		||||
                  </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 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">
 | 
			
		||||
              <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">
 | 
			
		||||
                {post.tags && post.tags.length > 0 && (
 | 
			
		||||
                  <div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
 | 
			
		||||
                    {post.tags.slice(0, 3).map((postTag) => (
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={`/blog/${postTag}`}
 | 
			
		||||
                        href={`/topics/${postTag}`}
 | 
			
		||||
                        class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
 | 
			
		||||
                          postTag === tag
 | 
			
		||||
                            ? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
 | 
			
		||||
@@ -212,24 +241,31 @@ const relatedTags = [
 | 
			
		||||
 | 
			
		||||
                <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"
 | 
			
		||||
                    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"
 | 
			
		||||
                    aria-hidden="true"
 | 
			
		||||
                    tabindex="-1"
 | 
			
		||||
                  >
 | 
			
		||||
                    <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 class="block transition-transform duration-300 group-hover:-translate-y-full">
 | 
			
		||||
                        Read article
 | 
			
		||||
                      </span>
 | 
			
		||||
                      <span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
 | 
			
		||||
                        Explore now
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <svg
 | 
			
		||||
                      viewBox="0 0 16 16"
 | 
			
		||||
                      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      aria-hidden="true"
 | 
			
		||||
                      class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
 | 
			
		||||
                      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
 | 
			
		||||
                        d="M6.75 5.75 9.25 8l-2.5 2.25"
 | 
			
		||||
                        stroke-width="1.5"
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                  </a>
 | 
			
		||||
@@ -241,7 +277,7 @@ const relatedTags = [
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Empty state -->
 | 
			
		||||
    <!-- Empty state với màu zinc -->
 | 
			
		||||
    {
 | 
			
		||||
      sortedPosts.length === 0 && (
 | 
			
		||||
        <div class="py-12 text-center sm:py-20">
 | 
			
		||||
@@ -291,61 +327,6 @@ const relatedTags = [
 | 
			
		||||
  </div>
 | 
			
		||||
</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>
 | 
			
		||||
  /* Grid pattern background */
 | 
			
		||||
  .bg-grid-pattern {
 | 
			
		||||
@@ -373,7 +354,7 @@ const relatedTags = [
 | 
			
		||||
      width: 0;
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      width: 50%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -381,22 +362,43 @@ const relatedTags = [
 | 
			
		||||
    animation: expand 1s ease-out forwards;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 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;
 | 
			
		||||
  /* Blob animation */
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob 7s infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animate-reveal {
 | 
			
		||||
    opacity: 1 !important;
 | 
			
		||||
    transform: translateY(0) !important;
 | 
			
		||||
  .animation-delay-2000 {
 | 
			
		||||
    animation-delay: 2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @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:
 | 
			
		||||
      transform 0.3s ease,
 | 
			
		||||
      box-shadow 0.3s ease,
 | 
			
		||||
      background-color 0.3s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (hover: hover) {
 | 
			
		||||
    .hover-card:hover {
 | 
			
		||||
      transform: translateY(-2px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Line clamp for descriptions */
 | 
			
		||||
@@ -421,3 +423,98 @@ const relatedTags = [
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Handle SPA transitions for tag pages
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all internal links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links, external links, or already processed
 | 
			
		||||
      if (
 | 
			
		||||
        link.getAttribute('href').includes('#') ||
 | 
			
		||||
        link.getAttribute('target') === '_blank' ||
 | 
			
		||||
        link.hasAttribute('data-spa-handled')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Initialize animations for tag page
 | 
			
		||||
    function animateTagContent() {
 | 
			
		||||
      // Animate header elements
 | 
			
		||||
      const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
 | 
			
		||||
      headerElements.forEach((el, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            el.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          100 + index * 150
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate posts with staggered delay
 | 
			
		||||
      const articles = document.querySelectorAll('article');
 | 
			
		||||
      articles.forEach((article, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            article.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          400 + index * 100
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Animate related tags
 | 
			
		||||
      const relatedTags = document.querySelectorAll('.related-tags a');
 | 
			
		||||
      relatedTags.forEach((tag, index) => {
 | 
			
		||||
        setTimeout(
 | 
			
		||||
          () => {
 | 
			
		||||
            tag.classList.add('animate-reveal');
 | 
			
		||||
          },
 | 
			
		||||
          600 + index * 50
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run animations
 | 
			
		||||
    animateTagContent();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Add this at the end of your page -->
 | 
			
		||||
							
								
								
									
										714
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										714
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,714 @@
 | 
			
		||||
---
 | 
			
		||||
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">
 | 
			
		||||
    <!-- Enhanced header section with animated elements - improved for mobile -->
 | 
			
		||||
    <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>
 | 
			
		||||
  // Ultra-reliable responsiveness handling
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Fix viewport width issues on mobile
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Ultra-responsive breakpoints for extreme reliability */
 | 
			
		||||
  /* 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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Improved 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>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Handle SPA transitions for tags index page
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all internal links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/"]').forEach((link) => {
 | 
			
		||||
      // Skip links that are anchor links, external links, or already processed
 | 
			
		||||
      if (
 | 
			
		||||
        link.getAttribute('href').includes('#') ||
 | 
			
		||||
        link.getAttribute('target') === '_blank' ||
 | 
			
		||||
        link.hasAttribute('data-spa-handled')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Add hover effect for tag cards on touch devices
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
      const tagCards = document.querySelectorAll('.tag-cloud a');
 | 
			
		||||
 | 
			
		||||
      tagCards.forEach((card) => {
 | 
			
		||||
        card.addEventListener('touchstart', () => {
 | 
			
		||||
          card.classList.add('is-touched');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        card.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            card.classList.remove('is-touched');
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Animate tag cards with staggered delay
 | 
			
		||||
    const tagCards = document.querySelectorAll('.tag-cloud a');
 | 
			
		||||
    tagCards.forEach((card, index) => {
 | 
			
		||||
      setTimeout(
 | 
			
		||||
        () => {
 | 
			
		||||
          card.classList.add('animate-reveal');
 | 
			
		||||
        },
 | 
			
		||||
        100 + index * 50
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
/* Remove all the complex mobile menu styles and keep only what's necessary */
 | 
			
		||||
@import 'tailwindcss';
 | 
			
		||||
 | 
			
		||||
/* Dark mode support for Tailwind CSS v4 */
 | 
			
		||||
/* https://tailwindcss.com/docs/dark-mode */
 | 
			
		||||
@custom-variant dark (&:where(.dark, .dark *));
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    font-family: 'Inter', sans-serif;
 | 
			
		||||
@@ -15,7 +12,6 @@
 | 
			
		||||
  html {
 | 
			
		||||
    scroll-behavior: smooth;
 | 
			
		||||
    scroll-padding-top: 5rem;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  body {
 | 
			
		||||
@@ -42,7 +38,7 @@
 | 
			
		||||
    scroll-padding-top: 4rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Touch targets on mobile */
 | 
			
		||||
  /* Better touch targets on mobile */
 | 
			
		||||
  button,
 | 
			
		||||
  a {
 | 
			
		||||
    @apply min-h-[44px];
 | 
			
		||||
@@ -135,4 +131,21 @@ button {
 | 
			
		||||
a.hover:hover,
 | 
			
		||||
button:hover {
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
} 
 | 
			
		||||
 | 
			
		||||
/* Smooth page transitions */
 | 
			
		||||
.page-transition {
 | 
			
		||||
  transition:
 | 
			
		||||
    opacity 0.3s ease,
 | 
			
		||||
    transform 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-entering {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transform: translateY(10px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-entered {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  transform: translateY(0);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import * as FaIcons from 'react-icons/fa';
 | 
			
		||||
import * as MdIcons from 'react-icons/md';
 | 
			
		||||
import * as AiIcons from 'react-icons/ai';
 | 
			
		||||
import * as GiIcons from 'react-icons/gi';
 | 
			
		||||
import * as IoIcons from 'react-icons/io';
 | 
			
		||||
import * as CiIcons from 'react-icons/ci';
 | 
			
		||||
import * as FiIcons from 'react-icons/fi';
 | 
			
		||||
import * as LuIcons from 'react-icons/lu';
 | 
			
		||||
import * as SiIcons from 'react-icons/si';
 | 
			
		||||
 | 
			
		||||
// Load React Icon library dynamically from attributes in Directus
 | 
			
		||||
 | 
			
		||||
const iconSets = {
 | 
			
		||||
  fa: FaIcons,
 | 
			
		||||
  md: MdIcons,
 | 
			
		||||
  ai: AiIcons,
 | 
			
		||||
  gi: GiIcons,
 | 
			
		||||
  io: IoIcons,
 | 
			
		||||
  ci: CiIcons,
 | 
			
		||||
  fi: FiIcons,
 | 
			
		||||
  lu: LuIcons,
 | 
			
		||||
  si: SiIcons,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DynamicIcon = ({ name, set = 'fa' }: { name: string; set: string }) => {
 | 
			
		||||
  let IconComponent = FaIcons.FaAlignCenter;
 | 
			
		||||
 | 
			
		||||
  if (name.startsWith('Fa')) {
 | 
			
		||||
    IconComponent = iconSets['fa'][name];
 | 
			
		||||
  } else if (name.startsWith('Si')) {
 | 
			
		||||
    IconComponent = iconSets['si'][name];
 | 
			
		||||
  } else {
 | 
			
		||||
    IconComponent = iconSets[set][name];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <IconComponent />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DynamicIcon;
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "lib": ["dom", "dom.iterable", "esnext"],
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "target": "ES6",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user