Compare commits
	
		
			1 Commits
		
	
	
		
			0.10.0
			...
			0b7d7d0d05
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0b7d7d0d05 | 
| @@ -1,11 +1,11 @@ | |||||||
| name: process-repository | name: process-issues | ||||||
| 
 | 
 | ||||||
| on: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: "@daily" |     - cron: '@daily' | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   process-repository: |   process-issues: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout Python Script |       - name: Checkout Python Script | ||||||
| @@ -14,27 +14,22 @@ jobs: | |||||||
|           repository: alexlebens/workflow-scripts |           repository: alexlebens/workflow-scripts | ||||||
|           ref: main |           ref: main | ||||||
|           token: ${{ secrets.BOT_TOKEN }} |           token: ${{ secrets.BOT_TOKEN }} | ||||||
|           path: workflow-scripts |           path: scripts | ||||||
| 
 | 
 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.13" |           python-version: '3.13' | ||||||
| 
 | 
 | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pip install requests immutabledict |         run: pip install requests | ||||||
| 
 | 
 | ||||||
|       - name: Run Script |       - name: Run Script | ||||||
|         env: |         env: | ||||||
|           INSTANCE_URL: ${{ vars.INSTANCE_URL }} |           INSTANCE_URL: ${{ vars.INSTANCE_URL }} | ||||||
|           OWNER: ${{ gitea.owner }} |  | ||||||
|           REPOSITORY: ${{ gitea.repository }} |           REPOSITORY: ${{ gitea.repository }} | ||||||
|           TOKEN: ${{ secrets.BOT_TOKEN }} |           TOKEN: ${{ secrets.BOT_TOKEN }} | ||||||
|           LOG_LEVEL: DEBUG |           STALE_DAYS: 3 | ||||||
|           ISSUE_STALE_DAYS: 3 |           STALE_TAG: 'stale' | ||||||
|           ISSUE_STALE_TAG: 23 |           EXCLUDE_TAG: 'renovate' | ||||||
|           ISSUE_EXCLUDE_TAG: 17 |         run: python ./scripts/scripts/process-issues.py | ||||||
|           PULL_REQUEST_STALE_DAYS: 3 |  | ||||||
|           PULL_REQUEST_STALE_TAG: 23 |  | ||||||
|           PULL_REQUEST_REQUIRED_TAG: 22 |  | ||||||
|         run: python ./workflow-scripts/process-repository.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 |       - name: Set up Node.js | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version: 22.17.x |           node-version: 22.16.x | ||||||
|           cache: pnpm |           cache: pnpm | ||||||
|  |  | ||||||
|       - name: Install Dependencies |       - name: Install Dependencies | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| ARG REGISTRY=docker.io | ARG REGISTRY=docker.io | ||||||
| FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base | FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base | ||||||
|  |  | ||||||
| LABEL version="0.10.0" | LABEL version="0.8.12" | ||||||
| LABEL description="Astro based personal website" | LABEL description="Astro based personal website" | ||||||
|  |  | ||||||
| ENV PNPM_HOME="/pnpm" | ENV PNPM_HOME="/pnpm" | ||||||
|   | |||||||
| @@ -2,8 +2,6 @@ | |||||||
|  |  | ||||||
| Copyright (c) 2025 Lê Vĩnh Khang | 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 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||||
| in the Software without restriction, including without limitation the rights | in the Software without restriction, including without limitation the rights | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "site-profile", |   "name": "site-profile", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "version": "0.10.0", |   "version": "0.8.12", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "astro dev", |     "dev": "astro dev", | ||||||
| @@ -17,10 +17,10 @@ | |||||||
|     "@astrojs/node": "^9.2.2", |     "@astrojs/node": "^9.2.2", | ||||||
|     "@astrojs/react": "^4.3.0", |     "@astrojs/react": "^4.3.0", | ||||||
|     "@astrojs/rss": "^4.0.12", |     "@astrojs/rss": "^4.0.12", | ||||||
|     "@directus/sdk": "^20.0.0", |     "@directus/sdk": "^19.1.0", | ||||||
|     "@tailwindcss/postcss": "^4.1.8", |     "@tailwindcss/postcss": "^4.1.8", | ||||||
|     "@tailwindcss/vite": "^4.1.8", |     "@tailwindcss/vite": "^4.1.8", | ||||||
|     "astro": "^5.10.1", |     "astro": "^5.10.0", | ||||||
|     "framer-motion": "^12.16.0", |     "framer-motion": "^12.16.0", | ||||||
|     "react": "^19.1.0", |     "react": "^19.1.0", | ||||||
|     "react-dom": "^19.1.0", |     "react-dom": "^19.1.0", | ||||||
| @@ -31,13 +31,13 @@ | |||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@tailwindcss/typography": "^0.5.16", |     "@tailwindcss/typography": "^0.5.16", | ||||||
|     "@typescript-eslint/parser": "8.36.0", |     "@typescript-eslint/parser": "8.35.0", | ||||||
|     "eslint": "9.31.0", |     "eslint": "9.29.0", | ||||||
|     "eslint-config-prettier": "10.1.5", |     "eslint-config-prettier": "10.1.5", | ||||||
|     "eslint-plugin-astro": "1.3.1", |     "eslint-plugin-astro": "1.3.1", | ||||||
|     "prettier": "^3.5.3", |     "prettier": "^3.5.3", | ||||||
|     "prettier-plugin-astro": "^0.14.1", |     "prettier-plugin-astro": "^0.14.1", | ||||||
|     "prettier-plugin-tailwindcss": "^0.6.12", |     "prettier-plugin-tailwindcss": "^0.6.12", | ||||||
|     "typescript-eslint": "8.36.0" |     "typescript-eslint": "8.35.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1003
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1003
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -16,7 +16,7 @@ | |||||||
|                 "npm" |                 "npm" | ||||||
|             ], |             ], | ||||||
|             "addLabels": [ |             "addLabels": [ | ||||||
|                 "dependency" |                 "automerge" | ||||||
|             ], |             ], | ||||||
|             "automerge": false, |             "automerge": false, | ||||||
|             "minimumReleaseAge": "1 days" |             "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"> | <div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden"> | ||||||
| @@ -29,19 +29,24 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   // Theme transition script |   // Theme transition script | ||||||
|   document.addEventListener('astro:page-load', () => { |   document.addEventListener('DOMContentLoaded', () => { | ||||||
|     const themeToggle = document.querySelector('[data-theme-toggle]'); |     const themeToggle = document.querySelector('[data-theme-toggle]'); | ||||||
|     const overlay = document.getElementById('theme-transition-overlay'); |     const overlay = document.getElementById('theme-transition-overlay'); | ||||||
|  |  | ||||||
|     if (themeToggle && overlay) { |     if (themeToggle && overlay) { | ||||||
|       themeToggle.addEventListener('click', () => { |       themeToggle.addEventListener('click', () => { | ||||||
|  |         // Add transitioning class to optimize performance | ||||||
|         document.documentElement.classList.add('theme-transitioning'); |         document.documentElement.classList.add('theme-transitioning'); | ||||||
|  |  | ||||||
|  |         // Fade in overlay | ||||||
|         overlay.style.opacity = '0.15'; |         overlay.style.opacity = '0.15'; | ||||||
|         overlay.style.transition = 'opacity 0.3s ease'; |         overlay.style.transition = 'opacity 0.3s ease'; | ||||||
|  |  | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|  |           // Fade out overlay | ||||||
|           overlay.style.opacity = '0'; |           overlay.style.opacity = '0'; | ||||||
|  |  | ||||||
|  |           // Remove transitioning class after animation completes | ||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
|             document.documentElement.classList.remove('theme-transitioning'); |             document.documentElement.classList.remove('theme-transitioning'); | ||||||
|           }, 700); |           }, 700); | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links')); | |||||||
| const currentYear = new Date().getFullYear(); | const currentYear = new Date().getFullYear(); | ||||||
|  |  | ||||||
| const navLinks = [ | const navLinks = [ | ||||||
|   { text: 'Home', href: '/' }, |   { text: 'About', href: '/about' }, | ||||||
|   { text: 'Blog', href: '/blog' }, |   { text: 'Blog', href: '/blog' }, | ||||||
|   { text: 'Topics', href: '/topics' }, |   { text: 'Topics', href: '/topics' }, | ||||||
|   { text: 'About', href: '/about' }, |   { text: 'RSS', href: '/rss.xml' }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const socialLinks = [ | const socialLinks = [ | ||||||
| @@ -35,7 +35,6 @@ const socialLinks = [ | |||||||
|  |  | ||||||
| <footer | <footer | ||||||
|   class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800" |   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 class="pointer-events-none absolute inset-0 overflow-hidden"> | ||||||
|     <div |     <div | ||||||
| @@ -54,6 +53,7 @@ const socialLinks = [ | |||||||
|  |  | ||||||
|   <div class="relative px-4 pt-16 pb-12 sm:px-6"> |   <div class="relative px-4 pt-16 pb-12 sm:px-6"> | ||||||
|     <div class="mx-auto max-w-4xl"> |     <div class="mx-auto max-w-4xl"> | ||||||
|  |       <!-- Main footer content --> | ||||||
|       <div class="grid grid-cols-1 gap-10 md:grid-cols-12"> |       <div class="grid grid-cols-1 gap-10 md:grid-cols-12"> | ||||||
|         <!-- Brand section --> |         <!-- Brand section --> | ||||||
|         <div class="col-span-1 md:col-span-3"> |         <div class="col-span-1 md:col-span-3"> | ||||||
| @@ -64,9 +64,8 @@ const socialLinks = [ | |||||||
|               > |               > | ||||||
|                 <span |                 <span | ||||||
|                   class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900" |                   class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900" | ||||||
|  |                   >{global.initals}</span | ||||||
|                 > |                 > | ||||||
|                   {global.initals} |  | ||||||
|                 </span> |  | ||||||
|                 <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" |                   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" | ||||||
|                 > |                 > | ||||||
| @@ -74,9 +73,8 @@ const socialLinks = [ | |||||||
|               </div> |               </div> | ||||||
|               <span |               <span | ||||||
|                 class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" |                 class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" | ||||||
|  |                 >Blog</span | ||||||
|               > |               > | ||||||
|                 Blog |  | ||||||
|               </span> |  | ||||||
|             </div> |             </div> | ||||||
|           </a> |           </a> | ||||||
|  |  | ||||||
| @@ -148,8 +146,8 @@ const socialLinks = [ | |||||||
|  |  | ||||||
|           <div class="flex items-center space-x-2"> |           <div class="flex items-center space-x-2"> | ||||||
|             <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400" |             <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400" | ||||||
|               >Built with |               >Built with</span | ||||||
|             </span> |             > | ||||||
|             <a |             <a | ||||||
|               href="https://astro.build" |               href="https://astro.build" | ||||||
|               target="_blank" |               target="_blank" | ||||||
| @@ -176,8 +174,7 @@ const socialLinks = [ | |||||||
|                 Astro |                 Astro | ||||||
|                 <span |                 <span | ||||||
|                   class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full" |                   class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full" | ||||||
|                 > |                 ></span> | ||||||
|                 </span> |  | ||||||
|               </span> |               </span> | ||||||
|             </a> |             </a> | ||||||
|           </div> |           </div> | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ const currentPath = pathname.slice(1); | |||||||
|  |  | ||||||
| <header | <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" |   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"> |   <div class="mx-auto flex max-w-3xl items-center justify-between px-4"> | ||||||
|     <!-- Logo --> |     <!-- Logo --> | ||||||
| @@ -122,7 +121,7 @@ const currentPath = pathname.slice(1); | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   // Mobile menu toggle with animations |   // Mobile menu toggle with animations | ||||||
|   document.addEventListener('astro:page-load', () => { |   document.addEventListener('DOMContentLoaded', () => { | ||||||
|     const mobileMenuButton = document.getElementById('mobile-menu-button'); |     const mobileMenuButton = document.getElementById('mobile-menu-button'); | ||||||
|     const closeMenuButton = document.getElementById('close-menu-button'); |     const closeMenuButton = document.getElementById('close-menu-button'); | ||||||
|     const mobileMenu = document.getElementById('mobile-menu'); |     const mobileMenu = document.getElementById('mobile-menu'); | ||||||
|   | |||||||
| @@ -29,12 +29,10 @@ const encodedUrl = encodeURIComponent(url); | |||||||
|         stroke-linecap="round" |         stroke-linecap="round" | ||||||
|         stroke-linejoin="round" |         stroke-linejoin="round" | ||||||
|         class="h-4 w-4" |         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" |           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> | ||||||
|     <a |     <a | ||||||
|       href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} |       href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} | ||||||
| @@ -52,9 +50,8 @@ const encodedUrl = encodeURIComponent(url); | |||||||
|         stroke-linecap="round" |         stroke-linecap="round" | ||||||
|         stroke-linejoin="round" |         stroke-linejoin="round" | ||||||
|         class="h-4 w-4" |         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> | ||||||
|     <a |     <a | ||||||
|       href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} |       href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} | ||||||
| @@ -72,12 +69,10 @@ const encodedUrl = encodeURIComponent(url); | |||||||
|         stroke-linecap="round" |         stroke-linecap="round" | ||||||
|         stroke-linejoin="round" |         stroke-linejoin="round" | ||||||
|         class="h-4 w-4" |         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> |     </a> | ||||||
|     <button |     <button | ||||||
|       id="copy-link-button" |       id="copy-link-button" | ||||||
| @@ -94,10 +89,9 @@ const encodedUrl = encodeURIComponent(url); | |||||||
|         stroke-linecap="round" |         stroke-linecap="round" | ||||||
|         stroke-linejoin="round" |         stroke-linejoin="round" | ||||||
|         class="h-4 w-4" |         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 |       <span | ||||||
|         id="copy-tooltip" |         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" |         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> |     </button> | ||||||
|   </div> |   </div> | ||||||
| </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> | ||||||
|   | |||||||
| @@ -47,25 +47,24 @@ | |||||||
|   ></span> |   ></span> | ||||||
| </button> | </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> | <script> | ||||||
|   // Use a function to handle theme toggle to ensure it can be called from anywhere |   // Use a function to handle theme toggle to ensure it can be called from anywhere | ||||||
|   function setupThemeToggle() { |   function setupThemeToggle() { | ||||||
|     const themeToggles = document.querySelectorAll('[data-theme-toggle]'); |     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 |     // Create theme switch overlay element if it doesn't exist | ||||||
|     if (!document.querySelector('.theme-switch-overlay')) { |     if (!document.querySelector('.theme-switch-overlay')) { | ||||||
|       const overlay = document.createElement('div'); |       const overlay = document.createElement('div'); | ||||||
| @@ -185,7 +184,7 @@ | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Run setup on load |   // 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 |   // Also run on page visibility change to ensure theme is consistent | ||||||
|   document.addEventListener('visibilitychange', () => { |   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}> | <Layout title={global.title} description={global.title}> | ||||||
|   <slot /> |   <slot /> | ||||||
| </Layout> | </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> | ||||||
|   | |||||||
| @@ -70,6 +70,7 @@ try { | |||||||
|     <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800"> |     <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"> |       <div class="flex flex-col items-center justify-between gap-6 sm:flex-row"> | ||||||
|         <ShareButtons url={canonicalURL.toString()} title={post.title} /> |         <ShareButtons url={canonicalURL.toString()} title={post.title} /> | ||||||
|  |         <!-- Convert URL to string --> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| @@ -85,8 +86,286 @@ try { | |||||||
|   <slot name="after-article" /> |   <slot name="after-article" /> | ||||||
| </Layout> | </Layout> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  |   //  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', | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       headings.forEach((heading) => { | ||||||
|  |         heading.classList.add('heading-animated'); | ||||||
|  |         observer.observe(heading); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       return observer; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // 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> | <style> | ||||||
|   /* Hero image styling */ |   /* Enhanced hero image styling */ | ||||||
|   article img:first-of-type { |   article img:first-of-type { | ||||||
|     border-radius: 1rem; |     border-radius: 1rem; | ||||||
|     box-shadow: |     box-shadow: | ||||||
| @@ -98,4 +377,22 @@ try { | |||||||
|   article img:first-of-type:hover { |   article img:first-of-type:hover { | ||||||
|     transform: scale(1.01); |     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> | </style> | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| --- | --- | ||||||
| import { ClientRouter } from 'astro:transitions'; |  | ||||||
|  |  | ||||||
| import Navigation from '../components/Navigation.astro'; | import Navigation from '../components/Navigation.astro'; | ||||||
| import Footer from '../components/Footer.astro'; | import Footer from '../components/Footer.astro'; | ||||||
| import Background from '../components/Background.astro'; | import Background from '../components/Background.astro'; | ||||||
|  |  | ||||||
| import '../styles/global.css'; | import '../styles/global.css'; | ||||||
|  |  | ||||||
| interface Props { | 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" |       href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" | ||||||
|       rel="stylesheet" |       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> |   </head> | ||||||
|   <body |   <body | ||||||
|     class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100" |     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 /> |     <Background /> | ||||||
|  |  | ||||||
|     <div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6"> |     <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> |       </main> | ||||||
|     </div> |     </div> | ||||||
|     <Footer /> |     <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> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
| <style> | <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 */ |   /* Content entrance animation */ | ||||||
|   main { |   main { | ||||||
|     opacity: 1; |     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,7 +5,6 @@ import Layout from '../layouts/Layout.astro'; | |||||||
| <Layout title="404 - Page Not Found"> | <Layout title="404 - Page Not Found"> | ||||||
|   <div |   <div | ||||||
|     class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" |     class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" | ||||||
|     transition:animate="slide" |  | ||||||
|   > |   > | ||||||
|     <!-- Animated background elements --> |     <!-- Animated background elements --> | ||||||
|     <div class="absolute inset-0 overflow-hidden"> |     <div class="absolute inset-0 overflow-hidden"> | ||||||
| @@ -49,8 +48,7 @@ import Layout from '../layouts/Layout.astro'; | |||||||
|         > |         > | ||||||
|           <span |           <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" |             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 |           <svg | ||||||
|             xmlns="http://www.w3.org/2000/svg" |             xmlns="http://www.w3.org/2000/svg" | ||||||
|             fill="none" |             fill="none" | ||||||
| @@ -63,8 +61,7 @@ import Layout from '../layouts/Layout.astro'; | |||||||
|               stroke-linecap="round" |               stroke-linecap="round" | ||||||
|               stroke-linejoin="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" |               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> |           </svg> | ||||||
|           <span class="relative z-10 font-medium">Return Home</span> |           <span class="relative z-10 font-medium">Return Home</span> | ||||||
|         </a> |         </a> | ||||||
| @@ -84,9 +81,7 @@ import Layout from '../layouts/Layout.astro'; | |||||||
|             <path |             <path | ||||||
|               stroke-linecap="round" |               stroke-linecap="round" | ||||||
|               stroke-linejoin="round" |               stroke-linejoin="round" | ||||||
|               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" |               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> | ||||||
|             > |  | ||||||
|             </path> |  | ||||||
|           </svg> |           </svg> | ||||||
|           <span class="font-medium">Go Back</span> |           <span class="font-medium">Go Back</span> | ||||||
|         </button> |         </button> | ||||||
| @@ -132,6 +127,66 @@ import Layout from '../layouts/Layout.astro'; | |||||||
|     const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; |     const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; | ||||||
|     funFactElement.textContent = randomFact; |     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> | </script> | ||||||
|  |  | ||||||
| <style> | <style> | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| --- | --- | ||||||
| import BaseLayout from '../layouts/BaseLayout.astro'; | 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 directus from '../../lib/directus'; | ||||||
| import { readSingleton, readItems } from '@directus/sdk'; | import { readSingleton, readItems } from '@directus/sdk'; | ||||||
| @@ -16,10 +17,7 @@ const skills = await directus.request( | |||||||
| --- | --- | ||||||
|  |  | ||||||
| <BaseLayout title="About Me" description={global.description}> | <BaseLayout title="About Me" description={global.description}> | ||||||
|   <div |   <div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"> | ||||||
|     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" |  | ||||||
|   > |  | ||||||
|     <!-- Hero Section --> |     <!-- Hero Section --> | ||||||
|     <div class="relative mb-12 sm:mb-16 md:mb-20"> |     <div class="relative mb-12 sm:mb-16 md:mb-20"> | ||||||
|       <!-- Decorative elements --> |       <!-- Decorative elements --> | ||||||
| @@ -121,16 +119,13 @@ const skills = await directus.request( | |||||||
|         <!-- Main slider container --> |         <!-- Main slider container --> | ||||||
|         <div class="slider-track animate-slide flex"> |         <div class="slider-track animate-slide flex"> | ||||||
|           { |           { | ||||||
|             [...skills, ...skills, ...skills].map((skill, index) => ( |             skills.map((skill, index) => ( | ||||||
|               <div |               <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"> | ||||||
|                 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" |  | ||||||
|               > |  | ||||||
|                 <div class="p-4 sm:p-6"> |                 <div class="p-4 sm:p-6"> | ||||||
|                   <div class="mb-4 flex items-center justify-between sm:mb-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="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"> |                       <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> |                       </div> | ||||||
|                       <h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100"> |                       <h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100"> | ||||||
|                         {skill.title} |                         {skill.title} | ||||||
| @@ -169,6 +164,7 @@ const skills = await directus.request( | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <!-- Contact Section --> |     <!-- Contact Section --> | ||||||
|     <div class="theme-transition-all mx-auto max-w-3xl text-center"> |     <div class="theme-transition-all mx-auto max-w-3xl text-center"> | ||||||
|       <h2 |       <h2 | ||||||
| @@ -277,7 +273,7 @@ const skills = await directus.request( | |||||||
|     z-index: 10; |     z-index: 10; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Reduce animation complexity on mobile */ |   /* Reduce animation complexity on mobile for better performance */ | ||||||
|   @media (max-width: 640px) { |   @media (max-width: 640px) { | ||||||
|     .skill-card { |     .skill-card { | ||||||
|       transition: |       transition: | ||||||
| @@ -337,7 +333,7 @@ const skills = await directus.request( | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Touch targets for mobile */ |   /* Improved touch targets for mobile */ | ||||||
|   @media (max-width: 640px) { |   @media (max-width: 640px) { | ||||||
|     a, |     a, | ||||||
|     button { |     button { | ||||||
| @@ -369,7 +365,8 @@ const skills = await directus.request( | |||||||
| </style> | </style> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   document.addEventListener('astro:page-load', () => { |   // Wait for the DOM to be fully loaded | ||||||
|  |   document.addEventListener('DOMContentLoaded', () => { | ||||||
|     const sliderTrack = document.querySelector('.slider-track'); |     const sliderTrack = document.querySelector('.slider-track'); | ||||||
|  |  | ||||||
|     // Create seamless infinite scrolling effect |     // Create seamless infinite scrolling effect | ||||||
| @@ -377,6 +374,9 @@ const skills = await directus.request( | |||||||
|       const cards = document.querySelectorAll('.skill-card'); |       const cards = document.querySelectorAll('.skill-card'); | ||||||
|       if (!cards.length) return; |       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 |       // Set proper animation based on screen size | ||||||
|       function updateScrollAnimation() { |       function updateScrollAnimation() { | ||||||
|         if (window.innerWidth >= 640) { |         if (window.innerWidth >= 640) { | ||||||
| @@ -461,7 +461,9 @@ const skills = await directus.request( | |||||||
|  |  | ||||||
|     // Handle theme transition |     // Handle theme transition | ||||||
|     document.addEventListener('themeChange', () => { |     document.addEventListener('themeChange', () => { | ||||||
|  |       // Add special effects during theme transition | ||||||
|       cards.forEach((card, index) => { |       cards.forEach((card, index) => { | ||||||
|  |         // Add staggered animation delay | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|           card.classList.add('theme-changing'); |           card.classList.add('theme-changing'); | ||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
| @@ -472,3 +474,104 @@ const skills = await directus.request( | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 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); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 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 | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Animate sections with staggered delay | ||||||
|  |       const sections = document.querySelectorAll('section'); | ||||||
|  |       sections.forEach((section, index) => { | ||||||
|  |         setTimeout( | ||||||
|  |           () => { | ||||||
|  |             section.classList.add('animate-reveal'); | ||||||
|  |           }, | ||||||
|  |           300 + index * 200 | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Run animations | ||||||
|  |     animateAboutContent(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 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> | ||||||
|   | |||||||
| @@ -116,6 +116,8 @@ const { post, nextPost, prevPost } = Astro.props; | |||||||
| </BlogPost> | </BlogPost> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  |   // Removing TOC-related functions | ||||||
|  |  | ||||||
|   // Add copy buttons to code blocks |   // Add copy buttons to code blocks | ||||||
|   function initializeCodeCopyButtons() { |   function initializeCodeCopyButtons() { | ||||||
|     const codeBlocks = document.querySelectorAll('pre'); |     const codeBlocks = document.querySelectorAll('pre'); | ||||||
| @@ -181,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 |   // Main initialization function | ||||||
|   function initializeBlogPost() { |   function initializeBlogPost() { | ||||||
|  |     // Initialize remaining components | ||||||
|     initializeCodeCopyButtons(); |     initializeCodeCopyButtons(); | ||||||
|  |     setupSPATransitions(); | ||||||
|  |  | ||||||
|     // Scroll to hash if present in URL |     // Scroll to hash if present in URL | ||||||
|     if (window.location.hash) { |     if (window.location.hash) { | ||||||
| @@ -196,11 +239,19 @@ const { post, nextPost, prevPost } = Astro.props; | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Initialize on first load | ||||||
|  |   document.addEventListener('DOMContentLoaded', initializeBlogPost); | ||||||
|  |  | ||||||
|   // Re-initialize when content changes via Astro's view transitions |   // Re-initialize when content changes via Astro's view transitions | ||||||
|   document.addEventListener('astro:page-load', initializeBlogPost); |   document.addEventListener('astro:page-load', initializeBlogPost); | ||||||
|  |  | ||||||
|  |   // For compatibility with custom transition system | ||||||
|  |   document.addEventListener('page-transition-complete', initializeBlogPost); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style> | <style> | ||||||
|  |   /* Removing TOC-related styles */ | ||||||
|  |  | ||||||
|   /* Language badge styling */ |   /* Language badge styling */ | ||||||
|   .language-badge { |   .language-badge { | ||||||
|     font-family: |     font-family: | ||||||
|   | |||||||
| @@ -22,11 +22,19 @@ const postsByYear = sortedPosts.reduce((acc, post) => { | |||||||
| }, {}); | }, {}); | ||||||
|  |  | ||||||
| const years = Object.keys(postsByYear).sort((a, b) => b - a); | const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||||
|  |  | ||||||
|  | // Get total post count | ||||||
|  | const totalPosts = sortedPosts.length; | ||||||
|  |  | ||||||
|  | // Get unique tags for search suggestions | ||||||
|  | const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))]; | ||||||
| --- | --- | ||||||
|  |  | ||||||
| <BaseLayout title="Blog"> | <BaseLayout title="Blog"> | ||||||
|   <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide"> |   <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16"> | ||||||
|  |     <!-- Header with search  --> | ||||||
|     <div class="relative mb-12 sm:mb-20"> |     <div class="relative mb-12 sm:mb-20"> | ||||||
|  |       <!-- Decorative elements --> | ||||||
|       <div |       <div | ||||||
|         class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30" |         class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30" | ||||||
|       > |       > | ||||||
| @@ -116,7 +124,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       <!-- Sidebar for mobile --> |       <!-- Improved sidebar for mobile --> | ||||||
|       <div class="relative md:col-span-3"> |       <div class="relative md:col-span-3"> | ||||||
|         <div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0"> |         <div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0"> | ||||||
|           <h3 |           <h3 | ||||||
| @@ -148,7 +156,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <!-- Post grid for mobile --> |       <!-- Improved post grid for mobile --> | ||||||
|       <div class="md:col-span-9"> |       <div class="md:col-span-9"> | ||||||
|         { |         { | ||||||
|           years.map((year) => ( |           years.map((year) => ( | ||||||
| @@ -160,7 +168,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|               <div |               <div | ||||||
|                 class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`} |                 class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`} | ||||||
|               > |               > | ||||||
|                 {postsByYear[year].map((post) => ( |                 {postsByYear[year].map((post, index) => ( | ||||||
|                   <article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0"> |                   <article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0"> | ||||||
|                     {post.image && ( |                     {post.image && ( | ||||||
|                       <div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56"> |                       <div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56"> | ||||||
| @@ -295,7 +303,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Touch targets for mobile */ |   /* Improved touch targets for mobile */ | ||||||
|   @media (max-width: 640px) { |   @media (max-width: 640px) { | ||||||
|     a, |     a, | ||||||
|     button { |     button { | ||||||
| @@ -307,7 +315,8 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
| </style> | </style> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   document.addEventListener('astro:page-load', () => { |   // Script không thay đổi - giữ nguyên chức năng | ||||||
|  |   document.addEventListener('DOMContentLoaded', () => { | ||||||
|     const backToTopButton = document.getElementById('back-to-top'); |     const backToTopButton = document.getElementById('back-to-top'); | ||||||
|  |  | ||||||
|     if (backToTopButton) { |     if (backToTopButton) { | ||||||
| @@ -332,7 +341,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|  |  | ||||||
|       // Check scroll position |       // Check scroll position | ||||||
|       window.addEventListener('scroll', toggleBackToTopButton); |       window.addEventListener('scroll', toggleBackToTopButton); | ||||||
|       toggleBackToTopButton(); |       toggleBackToTopButton(); // Initial check | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Add smooth scrolling to year links |     // Add smooth scrolling to year links | ||||||
| @@ -373,4 +382,57 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   // SPA transition handling | ||||||
|  |   function setupSPATransitions() { | ||||||
|  |     // Handle all blog post links for SPA transitions | ||||||
|  |     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; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Handle year anchor links specially | ||||||
|  |     document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => { | ||||||
|  |       anchor.setAttribute('data-spa-internal', 'true'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Initialize on first load | ||||||
|  |   document.addEventListener('DOMContentLoaded', setupSPATransitions); | ||||||
|  |  | ||||||
|  |   // Re-initialize when content changes via Astro's view transitions | ||||||
|  |   document.addEventListener('astro:page-load', setupSPATransitions); | ||||||
|  |  | ||||||
|  |   // For compatibility with custom transition system | ||||||
|  |   document.addEventListener('page-transition-complete', setupSPATransitions); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -22,13 +22,10 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
| --- | --- | ||||||
|  |  | ||||||
| <Layout title=`Home | ${global.name}`> | <Layout title=`Home | ${global.name}`> | ||||||
|   <!-- Hero Section with mobile responsiveness --> |   <!-- Hero Section with improved mobile responsiveness --> | ||||||
|   <section |   <section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"> | ||||||
|     class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20" |  | ||||||
|     transition:animate="slide" |  | ||||||
|   > |  | ||||||
|     <div class="relative mx-auto max-w-2xl"> |     <div class="relative mx-auto max-w-2xl"> | ||||||
|       <!-- Adjusted blob positions and sizes for mobile appearance --> |       <!-- Adjusted blob positions and sizes for better mobile appearance --> | ||||||
|       <div |       <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" |         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" | ||||||
|       > |       > | ||||||
| @@ -88,7 +85,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|     </div> |     </div> | ||||||
|   </section> |   </section> | ||||||
|  |  | ||||||
|   <!-- Featured post section --> |   <!-- Featured Post Section - Improved for mobile --> | ||||||
|   <section |   <section | ||||||
|     class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800" |     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" | ||||||
|   > |   > | ||||||
| @@ -127,7 +124,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|         </a> |         </a> | ||||||
|       </div> |       </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"> |       <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) => ( |           recentPosts.map((post, index) => ( | ||||||
| @@ -218,7 +215,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|     </div> |     </div> | ||||||
|   </section> |   </section> | ||||||
|  |  | ||||||
|   <!-- Topics section --> |   <!-- Topics/Tags Section - Improved for mobile --> | ||||||
|   { |   { | ||||||
|     allTags.length > 0 && ( |     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"> |       <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"> | ||||||
| @@ -281,7 +278,8 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   // Add hover effect for cards on touch devices |   // 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; |     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||||
|  |  | ||||||
|     if (isTouchDevice) { |     if (isTouchDevice) { | ||||||
| @@ -299,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'); |       document.documentElement.classList.add('touch-device'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Viewport height fix for mobile browsers |     // Improved viewport height fix for mobile browsers | ||||||
|     const setVh = () => { |     const setVh = () => { | ||||||
|       const vh = window.innerHeight * 0.01; |       const vh = window.innerHeight * 0.01; | ||||||
|       document.documentElement.style.setProperty('--vh', `${vh}px`); |       document.documentElement.style.setProperty('--vh', `${vh}px`); | ||||||
| @@ -341,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', () => { |     document.addEventListener('themeChanged', () => { | ||||||
|       // Store current scroll position |       // Store current scroll position | ||||||
|       const scrollPosition = window.scrollY; |       const scrollPosition = window.scrollY; | ||||||
| @@ -479,6 +477,58 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|       animateContent(); |       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> | </script> | ||||||
|  |  | ||||||
| <style> | <style> | ||||||
| @@ -536,4 +586,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | |||||||
|     opacity: 1 !important; |     opacity: 1 !important; | ||||||
|     transform: translateY(0) !important; |     transform: translateY(0) !important; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /* Rest of your existing styles... */ | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ export async function getStaticPaths() { | |||||||
|     }) |     }) | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   // Get all unique tags | ||||||
|   const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))]; |   const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))]; | ||||||
|  |  | ||||||
|   // Create a path for each tag |   // Create a path for each tag | ||||||
| @@ -40,6 +41,7 @@ const sortedPosts = | |||||||
|     : []; |     : []; | ||||||
| console.log(`Sorted posts length: ${sortedPosts.length}`); | console.log(`Sorted posts length: ${sortedPosts.length}`); | ||||||
|  |  | ||||||
|  | const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360); | ||||||
| const relatedTags = [ | const relatedTags = [ | ||||||
|   ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)), |   ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)), | ||||||
| ].slice(0, 5); | ].slice(0, 5); | ||||||
| @@ -47,6 +49,7 @@ const relatedTags = [ | |||||||
|  |  | ||||||
| <BaseLayout title={`Posts tagged with "${tag}"`}> | <BaseLayout title={`Posts tagged with "${tag}"`}> | ||||||
|   <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> |   <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> | ||||||
|  |     <!-- Header section --> | ||||||
|     <div class="relative mb-10 sm:mb-16"> |     <div class="relative mb-10 sm:mb-16"> | ||||||
|       <div |       <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" |         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" | ||||||
| @@ -274,7 +277,7 @@ const relatedTags = [ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <!-- Empty state --> |     <!-- Empty state với màu zinc --> | ||||||
|     { |     { | ||||||
|       sortedPosts.length === 0 && ( |       sortedPosts.length === 0 && ( | ||||||
|         <div class="py-12 text-center sm:py-20"> |         <div class="py-12 text-center sm:py-20"> | ||||||
| @@ -420,3 +423,98 @@ const relatedTags = [ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </style> | </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 --> | ||||||
|   | |||||||
| @@ -30,10 +30,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | |||||||
| --- | --- | ||||||
|  |  | ||||||
| <BaseLayout title="Explore Tags"> | <BaseLayout title="Explore Tags"> | ||||||
|   <div |   <div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16"> | ||||||
|     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 --> | ||||||
|     transition:animate="slide" |  | ||||||
|   > |  | ||||||
|     <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16"> |     <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16"> | ||||||
|       <div |       <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" |         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" | ||||||
| @@ -148,7 +146,9 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | |||||||
| </BaseLayout> | </BaseLayout> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   document.addEventListener('astro:page-load', () => { |   // Ultra-reliable responsiveness handling | ||||||
|  |   document.addEventListener('DOMContentLoaded', () => { | ||||||
|  |     // Fix viewport width issues on mobile | ||||||
|     const fixViewportWidth = () => { |     const fixViewportWidth = () => { | ||||||
|       // Force the viewport to be exactly the width of the device |       // Force the viewport to be exactly the width of the device | ||||||
|       const viewport = document.querySelector('meta[name="viewport"]'); |       const viewport = document.querySelector('meta[name="viewport"]'); | ||||||
| @@ -378,6 +378,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | |||||||
|     width: 100% !important; |     width: 100% !important; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /* Ultra-responsive breakpoints for extreme reliability */ | ||||||
|   /* Micro screens (below 240px) */ |   /* Micro screens (below 240px) */ | ||||||
|   @media (max-width: 239px) { |   @media (max-width: 239px) { | ||||||
|     .tag-cloud { |     .tag-cloud { | ||||||
| @@ -544,7 +545,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | |||||||
|     hyphens: auto; |     hyphens: auto; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Shadow for dark mode */ |   /* Improved shadow for dark mode */ | ||||||
|   :global(.dark) .tag-cloud { |   :global(.dark) .tag-cloud { | ||||||
|     box-shadow: |     box-shadow: | ||||||
|       0 0 0 1px rgba(255, 255, 255, 0.05), |       0 0 0 1px rgba(255, 255, 255, 0.05), | ||||||
| @@ -627,3 +628,87 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </style> | </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'; | @import 'tailwindcss'; | ||||||
|  |  | ||||||
| /* Dark mode support for Tailwind CSS v4 */ |  | ||||||
| /* https://tailwindcss.com/docs/dark-mode */ |  | ||||||
| @custom-variant dark (&:where(.dark, .dark *)); |  | ||||||
|  |  | ||||||
| @layer base { | @layer base { | ||||||
|   :root { |   :root { | ||||||
|     font-family: 'Inter', sans-serif; |     font-family: 'Inter', sans-serif; | ||||||
| @@ -15,7 +12,6 @@ | |||||||
|   html { |   html { | ||||||
|     scroll-behavior: smooth; |     scroll-behavior: smooth; | ||||||
|     scroll-padding-top: 5rem; |     scroll-padding-top: 5rem; | ||||||
|     overflow-y: scroll; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   body { |   body { | ||||||
| @@ -42,7 +38,7 @@ | |||||||
|     scroll-padding-top: 4rem; |     scroll-padding-top: 4rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Touch targets on mobile */ |   /* Better touch targets on mobile */ | ||||||
|   button, |   button, | ||||||
|   a { |   a { | ||||||
|     @apply min-h-[44px]; |     @apply min-h-[44px]; | ||||||
| @@ -136,3 +132,20 @@ a.hover:hover, | |||||||
| button:hover { | button:hover { | ||||||
|   transform: translateY(-2px); |   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,52 +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', |  | ||||||
|   size = 20, |  | ||||||
|   color = 'currentColor', |  | ||||||
|   className = '', |  | ||||||
| }: { |  | ||||||
|   name: string; |  | ||||||
|   set: string; |  | ||||||
|   size: number; |  | ||||||
|   color: string; |  | ||||||
|   className: 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 size={size} color={color} className={className} />; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default DynamicIcon; |  | ||||||
| @@ -3,7 +3,6 @@ | |||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "lib": ["dom", "dom.iterable", "esnext"], |     "lib": ["dom", "dom.iterable", "esnext"], | ||||||
|     "allowJs": true, |     "allowJs": true, | ||||||
|     "allowImportingTsExtensions": true, |  | ||||||
|     "target": "ES6", |     "target": "ES6", | ||||||
|     "skipLibCheck": true, |     "skipLibCheck": true, | ||||||
|     "strict": true, |     "strict": true, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user