Compare commits
	
		
			1 Commits
		
	
	
		
			0.11.2
			...
			5cd0fec89f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5cd0fec89f | 
| @@ -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.2" | ||||
| 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.2", | ||||
|   "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.35.0", | ||||
|     "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.35.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1034
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1034
									
								
								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