Compare commits
	
		
			1 Commits
		
	
	
		
			0.10.0
			...
			412915ad81
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 412915ad81 | 
| @@ -1,11 +1,11 @@ | ||||
| name: process-repository | ||||
| name: process-issues | ||||
| 
 | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "@daily" | ||||
|     - cron: '@daily' | ||||
| 
 | ||||
| jobs: | ||||
|   process-repository: | ||||
|   process-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout Python Script | ||||
| @@ -14,27 +14,22 @@ jobs: | ||||
|           repository: alexlebens/workflow-scripts | ||||
|           ref: main | ||||
|           token: ${{ secrets.BOT_TOKEN }} | ||||
|           path: workflow-scripts | ||||
|           path: scripts | ||||
| 
 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5 | ||||
|         with: | ||||
|           python-version: "3.13" | ||||
|           python-version: '3.13' | ||||
| 
 | ||||
|       - name: Install dependencies | ||||
|         run: pip install requests immutabledict | ||||
|         run: pip install requests | ||||
| 
 | ||||
|       - name: Run Script | ||||
|         env: | ||||
|           INSTANCE_URL: ${{ vars.INSTANCE_URL }} | ||||
|           OWNER: ${{ gitea.owner }} | ||||
|           REPOSITORY: ${{ gitea.repository }} | ||||
|           TOKEN: ${{ secrets.BOT_TOKEN }} | ||||
|           LOG_LEVEL: DEBUG | ||||
|           ISSUE_STALE_DAYS: 3 | ||||
|           ISSUE_STALE_TAG: 23 | ||||
|           ISSUE_EXCLUDE_TAG: 17 | ||||
|           PULL_REQUEST_STALE_DAYS: 3 | ||||
|           PULL_REQUEST_STALE_TAG: 23 | ||||
|           PULL_REQUEST_REQUIRED_TAG: 22 | ||||
|         run: python ./workflow-scripts/process-repository.py | ||||
|           STALE_DAYS: 3 | ||||
|           STALE_TAG: 'stale' | ||||
|           EXCLUDE_TAG: 'renovate' | ||||
|         run: python ./scripts/scripts/process-issues.py | ||||
							
								
								
									
										35
									
								
								.gitea/workflows/process-pull-requests.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.gitea/workflows/process-pull-requests.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| name: process-pull-requests | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '@daily' | ||||
|  | ||||
| jobs: | ||||
|   process-pull-requests: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout Python Script | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           repository: alexlebens/workflow-scripts | ||||
|           ref: main | ||||
|           token: ${{ secrets.BOT_TOKEN }} | ||||
|           path: scripts | ||||
|  | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5 | ||||
|         with: | ||||
|           python-version: '3.13' | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: pip install requests | ||||
|  | ||||
|       - name: Run Script | ||||
|         env: | ||||
|           INSTANCE_URL: ${{ vars.INSTANCE_URL }} | ||||
|           REPOSITORY: ${{ gitea.repository }} | ||||
|           TOKEN: ${{ secrets.BOT_TOKEN }} | ||||
|           STALE_DAYS: 3 | ||||
|           STALE_TAG: 'stale' | ||||
|           REQUIRED_TAG: 'automerge' | ||||
|         run: python ./scripts/scripts/process-pull-requests.py | ||||
| @@ -24,7 +24,7 @@ jobs: | ||||
|       - name: Set up Node.js | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 22.17.x | ||||
|           node-version: 22.16.x | ||||
|           cache: pnpm | ||||
|  | ||||
|       - name: Install Dependencies | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| ARG REGISTRY=docker.io | ||||
| 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" | ||||
|  | ||||
| ENV PNPM_HOME="/pnpm" | ||||
|   | ||||
| @@ -2,8 +2,6 @@ | ||||
|  | ||||
| Copyright (c) 2025 Lê Vĩnh Khang | ||||
|  | ||||
| Copyright (c) 2025 Alex Lebens | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
|   | ||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "site-profile", | ||||
|   "type": "module", | ||||
|   "version": "0.10.0", | ||||
|   "version": "0.8.12", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "astro dev", | ||||
| @@ -17,10 +17,10 @@ | ||||
|     "@astrojs/node": "^9.2.2", | ||||
|     "@astrojs/react": "^4.3.0", | ||||
|     "@astrojs/rss": "^4.0.12", | ||||
|     "@directus/sdk": "^20.0.0", | ||||
|     "@directus/sdk": "^19.1.0", | ||||
|     "@tailwindcss/postcss": "^4.1.8", | ||||
|     "@tailwindcss/vite": "^4.1.8", | ||||
|     "astro": "^5.10.1", | ||||
|     "astro": "^5.10.0", | ||||
|     "framer-motion": "^12.16.0", | ||||
|     "react": "^19.1.0", | ||||
|     "react-dom": "^19.1.0", | ||||
| @@ -31,13 +31,13 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/typography": "^0.5.16", | ||||
|     "@typescript-eslint/parser": "8.36.0", | ||||
|     "eslint": "9.31.0", | ||||
|     "@typescript-eslint/parser": "8.34.1", | ||||
|     "eslint": "9.29.0", | ||||
|     "eslint-config-prettier": "10.1.5", | ||||
|     "eslint-plugin-astro": "1.3.1", | ||||
|     "prettier": "^3.5.3", | ||||
|     "prettier-plugin-astro": "^0.14.1", | ||||
|     "prettier-plugin-tailwindcss": "^0.6.12", | ||||
|     "typescript-eslint": "8.36.0" | ||||
|     "typescript-eslint": "8.34.1" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1025
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1025
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -16,7 +16,7 @@ | ||||
|                 "npm" | ||||
|             ], | ||||
|             "addLabels": [ | ||||
|                 "dependency" | ||||
|                 "automerge" | ||||
|             ], | ||||
|             "automerge": false, | ||||
|             "minimumReleaseAge": "1 days" | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| --- | ||||
|  | ||||
| // Background.astro - Dot pattern and ambient glow background with smooth theme transitions | ||||
| --- | ||||
|  | ||||
| <div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden"> | ||||
| @@ -29,19 +29,24 @@ | ||||
|  | ||||
| <script> | ||||
|   // Theme transition script | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const themeToggle = document.querySelector('[data-theme-toggle]'); | ||||
|     const overlay = document.getElementById('theme-transition-overlay'); | ||||
|  | ||||
|     if (themeToggle && overlay) { | ||||
|       themeToggle.addEventListener('click', () => { | ||||
|         // Add transitioning class to optimize performance | ||||
|         document.documentElement.classList.add('theme-transitioning'); | ||||
|  | ||||
|         // Fade in overlay | ||||
|         overlay.style.opacity = '0.15'; | ||||
|         overlay.style.transition = 'opacity 0.3s ease'; | ||||
|  | ||||
|         setTimeout(() => { | ||||
|           // Fade out overlay | ||||
|           overlay.style.opacity = '0'; | ||||
|  | ||||
|           // Remove transitioning class after animation completes | ||||
|           setTimeout(() => { | ||||
|             document.documentElement.classList.remove('theme-transitioning'); | ||||
|           }, 700); | ||||
|   | ||||
| @@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links')); | ||||
| const currentYear = new Date().getFullYear(); | ||||
|  | ||||
| const navLinks = [ | ||||
|   { text: 'Home', href: '/' }, | ||||
|   { text: 'About', href: '/about' }, | ||||
|   { text: 'Blog', href: '/blog' }, | ||||
|   { text: 'Topics', href: '/topics' }, | ||||
|   { text: 'About', href: '/about' }, | ||||
|   { text: 'RSS', href: '/rss.xml' }, | ||||
| ]; | ||||
|  | ||||
| const socialLinks = [ | ||||
| @@ -35,7 +35,6 @@ const socialLinks = [ | ||||
|  | ||||
| <footer | ||||
|   class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800" | ||||
|   transition:animate="none" | ||||
| > | ||||
|   <div class="pointer-events-none absolute inset-0 overflow-hidden"> | ||||
|     <div | ||||
| @@ -54,6 +53,7 @@ const socialLinks = [ | ||||
|  | ||||
|   <div class="relative px-4 pt-16 pb-12 sm:px-6"> | ||||
|     <div class="mx-auto max-w-4xl"> | ||||
|       <!-- Main footer content --> | ||||
|       <div class="grid grid-cols-1 gap-10 md:grid-cols-12"> | ||||
|         <!-- Brand section --> | ||||
|         <div class="col-span-1 md:col-span-3"> | ||||
| @@ -64,9 +64,8 @@ const socialLinks = [ | ||||
|               > | ||||
|                 <span | ||||
|                   class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900" | ||||
|                   >{global.initals}</span | ||||
|                 > | ||||
|                   {global.initals} | ||||
|                 </span> | ||||
|                 <div | ||||
|                   class="absolute inset-0 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> | ||||
|               <span | ||||
|                 class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" | ||||
|                 >Blog</span | ||||
|               > | ||||
|                 Blog | ||||
|               </span> | ||||
|             </div> | ||||
|           </a> | ||||
|  | ||||
| @@ -148,8 +146,8 @@ const socialLinks = [ | ||||
|  | ||||
|           <div class="flex items-center space-x-2"> | ||||
|             <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400" | ||||
|               >Built with | ||||
|             </span> | ||||
|               >Built with</span | ||||
|             > | ||||
|             <a | ||||
|               href="https://astro.build" | ||||
|               target="_blank" | ||||
| @@ -176,8 +174,7 @@ const socialLinks = [ | ||||
|                 Astro | ||||
|                 <span | ||||
|                   class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full" | ||||
|                 > | ||||
|                 </span> | ||||
|                 ></span> | ||||
|               </span> | ||||
|             </a> | ||||
|           </div> | ||||
|   | ||||
| @@ -20,7 +20,6 @@ const currentPath = pathname.slice(1); | ||||
|  | ||||
| <header | ||||
|   class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900" | ||||
|   transition:animate="none" | ||||
| > | ||||
|   <div class="mx-auto flex max-w-3xl items-center justify-between px-4"> | ||||
|     <!-- Logo --> | ||||
| @@ -122,7 +121,7 @@ const currentPath = pathname.slice(1); | ||||
|  | ||||
| <script> | ||||
|   // Mobile menu toggle with animations | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const mobileMenuButton = document.getElementById('mobile-menu-button'); | ||||
|     const closeMenuButton = document.getElementById('close-menu-button'); | ||||
|     const mobileMenu = document.getElementById('mobile-menu'); | ||||
|   | ||||
| @@ -29,12 +29,10 @@ const encodedUrl = encodeURIComponent(url); | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         class="h-4 w-4" | ||||
|       > | ||||
|         <path | ||||
|         ><path | ||||
|           d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" | ||||
|         ></path></svg | ||||
|       > | ||||
|         </path> | ||||
|       </svg> | ||||
|     </a> | ||||
|     <a | ||||
|       href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} | ||||
| @@ -52,9 +50,8 @@ const encodedUrl = encodeURIComponent(url); | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         class="h-4 w-4" | ||||
|         ><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg | ||||
|       > | ||||
|         <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path> | ||||
|       </svg> | ||||
|     </a> | ||||
|     <a | ||||
|       href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} | ||||
| @@ -72,12 +69,10 @@ const encodedUrl = encodeURIComponent(url); | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         class="h-4 w-4" | ||||
|         ><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" | ||||
|         ></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2" | ||||
|         ></circle></svg | ||||
|       > | ||||
|         <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"> | ||||
|         </path> | ||||
|         <rect x="2" y="9" width="4" height="12"></rect> | ||||
|         <circle cx="4" cy="4" r="2"></circle> | ||||
|       </svg> | ||||
|     </a> | ||||
|     <button | ||||
|       id="copy-link-button" | ||||
| @@ -94,10 +89,9 @@ const encodedUrl = encodeURIComponent(url); | ||||
|         stroke-linecap="round" | ||||
|         stroke-linejoin="round" | ||||
|         class="h-4 w-4" | ||||
|         ><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path | ||||
|           d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg | ||||
|       > | ||||
|         <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"> </path> | ||||
|         <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"> </path> | ||||
|       </svg> | ||||
|       <span | ||||
|         id="copy-tooltip" | ||||
|         class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700" | ||||
| @@ -107,3 +101,75 @@ const encodedUrl = encodeURIComponent(url); | ||||
|     </button> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|   // Function to handle copy link button | ||||
|   function setupCopyLinkButton() { | ||||
|     const copyButtons = document.querySelectorAll('#copy-link-button'); | ||||
|  | ||||
|     copyButtons.forEach((button) => { | ||||
|       button.addEventListener('click', () => { | ||||
|         // Get the current URL | ||||
|         const url = window.location.href; | ||||
|  | ||||
|         // Copy to clipboard | ||||
|         navigator.clipboard | ||||
|           .writeText(url) | ||||
|           .then(() => { | ||||
|             // Show tooltip | ||||
|             const tooltip = button.querySelector('#copy-tooltip'); | ||||
|             if (tooltip) { | ||||
|               tooltip.classList.add('opacity-100'); | ||||
|  | ||||
|               // Hide tooltip after 2 seconds | ||||
|               setTimeout(() => { | ||||
|                 tooltip.classList.remove('opacity-100'); | ||||
|               }, 2000); | ||||
|             } | ||||
|           }) | ||||
|           .catch((err) => { | ||||
|             console.error('Failed to copy: ', err); | ||||
|           }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Set up the copy link button when the DOM is loaded | ||||
|   document.addEventListener('DOMContentLoaded', setupCopyLinkButton); | ||||
|  | ||||
|   // Also set up when the page content is updated via SPA navigation | ||||
|   document.addEventListener('astro:page-load', setupCopyLinkButton); | ||||
|  | ||||
|   // For compatibility with the custom page transition system | ||||
|   document.addEventListener('page-transition-complete', setupCopyLinkButton); | ||||
|  | ||||
|   // Handle SPA transitions for share links | ||||
|   function setupSpaTransitions() { | ||||
|     // Get all share links | ||||
|     const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]'); | ||||
|  | ||||
|     // Make sure external share links don't trigger page transitions | ||||
|     shareLinks.forEach((link) => { | ||||
|       link.setAttribute('data-spa-external', 'true'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Initialize SPA transitions | ||||
|   document.addEventListener('DOMContentLoaded', setupSpaTransitions); | ||||
|   document.addEventListener('astro:page-load', setupSpaTransitions); | ||||
|   document.addEventListener('page-transition-complete', setupSpaTransitions); | ||||
|  | ||||
|   // Dispatch custom event when share action is completed | ||||
|   function notifyShareComplete() { | ||||
|     document.dispatchEvent(new CustomEvent('share-action-complete')); | ||||
|   } | ||||
|  | ||||
|   // Add analytics tracking for share actions if needed | ||||
|   function trackShareAction(platform) { | ||||
|     // You can implement analytics tracking here | ||||
|     console.log(`Shared on ${platform}`); | ||||
|  | ||||
|     // Notify other components that share action is complete | ||||
|     notifyShareComplete(); | ||||
|   } | ||||
| </script> | ||||
|   | ||||
| @@ -47,25 +47,24 @@ | ||||
|   ></span> | ||||
| </button> | ||||
|  | ||||
| <script is:inline> | ||||
|   // Use a function to persist theme when using SPA transitions | ||||
|   // https://docs.astro.build/en/guides/view-transitions/#script-re-execution | ||||
|   function applyTheme() { | ||||
|     localStorage.theme === 'dark' | ||||
|       ? document.documentElement.classList.add('dark') | ||||
|       : document.documentElement.classList.remove('dark'); | ||||
|   } | ||||
|  | ||||
|   document.addEventListener('astro:after-swap', applyTheme); | ||||
|  | ||||
|   applyTheme(); | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
|   // Use a function to handle theme toggle to ensure it can be called from anywhere | ||||
|   function setupThemeToggle() { | ||||
|     const themeToggles = document.querySelectorAll('[data-theme-toggle]'); | ||||
|  | ||||
|     // Check for dark mode preference at the system level | ||||
|     const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||||
|  | ||||
|     // Check for saved theme preference or use the system preference | ||||
|     const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light'); | ||||
|  | ||||
|     // Apply the theme on initial load | ||||
|     if (currentTheme === 'dark') { | ||||
|       document.documentElement.classList.add('dark'); | ||||
|     } else { | ||||
|       document.documentElement.classList.remove('dark'); | ||||
|     } | ||||
|  | ||||
|     // Create theme switch overlay element if it doesn't exist | ||||
|     if (!document.querySelector('.theme-switch-overlay')) { | ||||
|       const overlay = document.createElement('div'); | ||||
| @@ -185,7 +184,7 @@ | ||||
|   } | ||||
|  | ||||
|   // Run setup on load | ||||
|   document.addEventListener('astro:page-load', setupThemeToggle); | ||||
|   document.addEventListener('DOMContentLoaded', setupThemeToggle); | ||||
|  | ||||
|   // Also run on page visibility change to ensure theme is consistent | ||||
|   document.addEventListener('visibilitychange', () => { | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/layouts/Base.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/layouts/Base.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| --- | ||||
| import Layout from './Layout.astro'; | ||||
|  | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
|  | ||||
| export interface Props { | ||||
|   title: string; | ||||
|   description?: string; | ||||
| } | ||||
| --- | ||||
|  | ||||
| <Layout title={global.title} description={global.title}> | ||||
|   <slot /> | ||||
| </Layout> | ||||
| @@ -15,3 +15,45 @@ export interface Props { | ||||
| <Layout title={global.title} description={global.title}> | ||||
|   <slot /> | ||||
| </Layout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const themeToggle = document.getElementById('theme-toggle'); | ||||
|  | ||||
|     if (themeToggle) { | ||||
|       themeToggle.addEventListener('click', () => { | ||||
|         document.documentElement.classList.add('theme-switching'); | ||||
|  | ||||
|         const rippleElements = document.querySelectorAll('.theme-ripple'); | ||||
|         rippleElements.forEach((el) => { | ||||
|           el.classList.add('ripple-active'); | ||||
|           setTimeout(() => { | ||||
|             el.classList.remove('ripple-active'); | ||||
|           }, 600); | ||||
|         }); | ||||
|  | ||||
|         const event = new CustomEvent('themeChange', { | ||||
|           detail: { | ||||
|             theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light', | ||||
|           }, | ||||
|         }); | ||||
|         document.dispatchEvent(event); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|           document.documentElement.classList.remove('theme-switching'); | ||||
|         }, 600); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const socialLinks = document.querySelectorAll('.social-link'); | ||||
|     socialLinks.forEach((link) => { | ||||
|       link.addEventListener('mouseenter', () => { | ||||
|         link.classList.add('hover-active'); | ||||
|       }); | ||||
|  | ||||
|       link.addEventListener('mouseleave', () => { | ||||
|         link.classList.remove('hover-active'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| </script> | ||||
|   | ||||
| @@ -70,6 +70,7 @@ try { | ||||
|     <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800"> | ||||
|       <div class="flex flex-col items-center justify-between gap-6 sm:flex-row"> | ||||
|         <ShareButtons url={canonicalURL.toString()} title={post.title} /> | ||||
|         <!-- Convert URL to string --> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
| @@ -85,8 +86,286 @@ try { | ||||
|   <slot name="after-article" /> | ||||
| </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> | ||||
|   /* Hero image styling */ | ||||
|   /* Enhanced hero image styling */ | ||||
|   article img:first-of-type { | ||||
|     border-radius: 1rem; | ||||
|     box-shadow: | ||||
| @@ -98,4 +377,22 @@ try { | ||||
|   article img:first-of-type:hover { | ||||
|     transform: scale(1.01); | ||||
|   } | ||||
|  | ||||
|   /* Article entrance animation */ | ||||
|   .article-entering { | ||||
|     animation: article-fade-in 0.8s ease-out forwards; | ||||
|   } | ||||
|  | ||||
|   @keyframes article-fade-in { | ||||
|     from { | ||||
|       opacity: 0; | ||||
|       transform: translateY(10px); | ||||
|     } | ||||
|     to { | ||||
|       opacity: 1; | ||||
|       transform: translateY(0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Rest of the styles remain unchanged... */ | ||||
| </style> | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| --- | ||||
| import { ClientRouter } from 'astro:transitions'; | ||||
|  | ||||
| import Navigation from '../components/Navigation.astro'; | ||||
| import Footer from '../components/Footer.astro'; | ||||
| import Background from '../components/Background.astro'; | ||||
|  | ||||
| import '../styles/global.css'; | ||||
|  | ||||
| interface Props { | ||||
| @@ -30,30 +27,19 @@ const { title, description } = Astro.props; | ||||
|       href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" | ||||
|       rel="stylesheet" | ||||
|     /> | ||||
|     <!-- Load theme early to prevent flashes between light and dark modes --> | ||||
|     <script is:inline> | ||||
|       const theme = (() => { | ||||
|         if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) { | ||||
|           return localStorage.getItem('theme'); | ||||
|         } | ||||
|         if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||||
|           return 'dark'; | ||||
|         } | ||||
|         return 'light'; | ||||
|       })(); | ||||
|  | ||||
|       if (theme === 'light') { | ||||
|         document.documentElement.classList.remove('dark'); | ||||
|       } else { | ||||
|         document.documentElement.classList.add('dark'); | ||||
|       } | ||||
|       window.localStorage.setItem('theme', theme); | ||||
|     </script> | ||||
|     <ClientRouter /> | ||||
|   </head> | ||||
|   <body | ||||
|     class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100" | ||||
|   > | ||||
|     <!-- Page transition overlay - for smooth transitions between pages --> | ||||
|     <div | ||||
|       id="page-transition" | ||||
|       class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900" | ||||
|     > | ||||
|       <div class="transition-spinner"></div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Background component with dot pattern and ambient glow --> | ||||
|     <Background /> | ||||
|  | ||||
|     <div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6"> | ||||
| @@ -63,10 +49,262 @@ const { title, description } = Astro.props; | ||||
|       </main> | ||||
|     </div> | ||||
|     <Footer /> | ||||
|  | ||||
|     <script> | ||||
|       // SPA transition system with history API | ||||
|       document.addEventListener('DOMContentLoaded', () => { | ||||
|         const pageTransition = document.getElementById('page-transition'); | ||||
|         const mainContent = document.querySelector('main'); | ||||
|  | ||||
|         // Initialize content with entrance animation | ||||
|         if (mainContent) { | ||||
|           mainContent.classList.add('content-entering'); | ||||
|           setTimeout(() => { | ||||
|             mainContent.classList.remove('content-entering'); | ||||
|           }, 800); | ||||
|         } | ||||
|  | ||||
|         // Function to load content via fetch | ||||
|         async function loadContent(url) { | ||||
|           try { | ||||
|             // Show transition overlay | ||||
|             if (pageTransition) { | ||||
|               pageTransition.classList.remove('opacity-0', 'pointer-events-none'); | ||||
|               pageTransition.classList.add('opacity-100'); | ||||
|             } | ||||
|  | ||||
|             // Fade out current content | ||||
|             if (mainContent) { | ||||
|               mainContent.style.opacity = '0'; | ||||
|               mainContent.style.transform = 'translateY(10px)'; | ||||
|             } | ||||
|  | ||||
|             // Fetch the new page content | ||||
|             const response = await fetch(url); | ||||
|             if (!response.ok) throw new Error(`Failed to fetch ${url}`); | ||||
|             const html = await response.text(); | ||||
|  | ||||
|             // Create a temporary element to parse the HTML | ||||
|             const parser = new DOMParser(); | ||||
|             const doc = parser.parseFromString(html, 'text/html'); | ||||
|  | ||||
|             // Extract the main content | ||||
|             const newContent = doc.querySelector('main'); | ||||
|             if (!newContent) throw new Error('Could not find main content in the fetched page'); | ||||
|  | ||||
|             // Extract the title | ||||
|             const newTitle = doc.querySelector('title'); | ||||
|             if (newTitle) { | ||||
|               document.title = newTitle.textContent; | ||||
|             } | ||||
|  | ||||
|             // Extract meta description | ||||
|             const newDescription = doc.querySelector('meta[name="description"]'); | ||||
|             if (newDescription) { | ||||
|               const currentDescription = document.querySelector('meta[name="description"]'); | ||||
|               if (currentDescription) { | ||||
|                 currentDescription.setAttribute( | ||||
|                   'content', | ||||
|                   newDescription.getAttribute('content') || '' | ||||
|                 ); | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             // Wait a bit for transition effect | ||||
|             await new Promise((resolve) => setTimeout(resolve, 300)); | ||||
|  | ||||
|             // Replace the content | ||||
|             if (mainContent && newContent) { | ||||
|               mainContent.innerHTML = newContent.innerHTML; | ||||
|  | ||||
|               // Run scripts in the new content | ||||
|               Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => { | ||||
|                 const newScript = document.createElement('script'); | ||||
|                 Array.from(oldScript.attributes).forEach((attr) => { | ||||
|                   newScript.setAttribute(attr.name, attr.value); | ||||
|                 }); | ||||
|                 newScript.textContent = oldScript.textContent; | ||||
|                 if (oldScript.parentNode) { | ||||
|                   mainContent.appendChild(newScript); | ||||
|                 } | ||||
|               }); | ||||
|             } | ||||
|  | ||||
|             // Fade in new content with animation | ||||
|             if (mainContent) { | ||||
|               mainContent.style.opacity = '0'; | ||||
|               mainContent.style.transform = 'translateY(10px)'; | ||||
|  | ||||
|               setTimeout(() => { | ||||
|                 mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; | ||||
|                 mainContent.style.opacity = '1'; | ||||
|                 mainContent.style.transform = 'translateY(0)'; | ||||
|  | ||||
|                 // Add entrance animation class | ||||
|                 mainContent.classList.add('content-entering'); | ||||
|                 setTimeout(() => { | ||||
|                   mainContent.classList.remove('content-entering'); | ||||
|                 }, 800); | ||||
|               }, 50); | ||||
|             } | ||||
|  | ||||
|             // Hide transition overlay | ||||
|             if (pageTransition) { | ||||
|               setTimeout(() => { | ||||
|                 pageTransition.classList.add('opacity-0', 'pointer-events-none'); | ||||
|                 pageTransition.classList.remove('opacity-100'); | ||||
|               }, 200); | ||||
|             } | ||||
|  | ||||
|             // Dispatch custom event for content loaded | ||||
|             document.dispatchEvent( | ||||
|               new CustomEvent('spa-content-loaded', { | ||||
|                 detail: { url }, | ||||
|               }) | ||||
|             ); | ||||
|  | ||||
|             // Scroll to top or to saved position | ||||
|             window.scrollTo(0, 0); | ||||
|  | ||||
|             // Re-attach event listeners to new content | ||||
|             attachLinkListeners(); | ||||
|           } catch (error) { | ||||
|             console.error('Error loading content:', error); | ||||
|  | ||||
|             // Fallback to traditional navigation on error | ||||
|             window.location.href = url; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Function to attach event listeners to all links | ||||
|         function attachLinkListeners() { | ||||
|           document.querySelectorAll('a').forEach((link) => { | ||||
|             // Skip links that are already handled, anchor links, external links, or have special attributes | ||||
|             if ( | ||||
|               link.hasAttribute('data-spa-handled') || | ||||
|               !link.href.startsWith(window.location.origin) || | ||||
|               link.href.includes('#') || | ||||
|               link.hasAttribute('target') || | ||||
|               link.hasAttribute('download') || | ||||
|               link.getAttribute('rel') === 'external' || | ||||
|               link.getAttribute('rel') === 'nofollow' | ||||
|             ) { | ||||
|               return; | ||||
|             } | ||||
|  | ||||
|             // Mark as handled to avoid duplicate listeners | ||||
|             link.setAttribute('data-spa-handled', 'true'); | ||||
|  | ||||
|             link.addEventListener('click', (e) => { | ||||
|               // Don't handle if modifier keys are pressed (for opening in new tab, etc.) | ||||
|               if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               e.preventDefault(); | ||||
|               const targetHref = link.href; | ||||
|  | ||||
|               // Don't transition if clicking the current page | ||||
|               if (targetHref === window.location.href) { | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               // Update browser history | ||||
|               window.history.pushState({ path: targetHref }, '', targetHref); | ||||
|  | ||||
|               // Load the new content | ||||
|               loadContent(targetHref); | ||||
|             }); | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         // Initial attachment of link listeners | ||||
|         attachLinkListeners(); | ||||
|  | ||||
|         // Handle browser back/forward navigation | ||||
|         window.addEventListener('popstate', (e) => { | ||||
|           if (e.state && e.state.path) { | ||||
|             loadContent(e.state.path); | ||||
|           } else { | ||||
|             loadContent(window.location.href); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         // Check RSS feed availability | ||||
|         const checkAndGenerateRSS = async () => { | ||||
|           try { | ||||
|             const response = await fetch('/rss.xml'); | ||||
|             if (!response.ok) { | ||||
|               console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.'); | ||||
|             } | ||||
|           } catch (error) { | ||||
|             console.warn('Could not check RSS feed status.'); | ||||
|           } | ||||
|         }; | ||||
|  | ||||
|         // Check RSS feed availability | ||||
|         checkAndGenerateRSS(); | ||||
|       }); | ||||
|  | ||||
|       // Theme handling with transition effects | ||||
|       function setupThemeHandling() { | ||||
|         // Apply theme from localStorage or system preference | ||||
|         const theme = localStorage.getItem('theme'); | ||||
|         if ( | ||||
|           theme === 'dark' || | ||||
|           (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches) | ||||
|         ) { | ||||
|           document.documentElement.classList.add('dark'); | ||||
|         } else { | ||||
|           document.documentElement.classList.remove('dark'); | ||||
|         } | ||||
|  | ||||
|         // Listen for theme changes | ||||
|         document.addEventListener('themeChanged', () => { | ||||
|           // Add transition class to body | ||||
|           document.body.classList.add('theme-transitioning'); | ||||
|  | ||||
|           // Remove class after transition completes | ||||
|           setTimeout(() => { | ||||
|             document.body.classList.remove('theme-transitioning'); | ||||
|           }, 500); | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       // Initialize theme handling | ||||
|       document.addEventListener('DOMContentLoaded', setupThemeHandling); | ||||
|     </script> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| <style> | ||||
|   /* Page transition effects */ | ||||
|   #page-transition { | ||||
|     transition: opacity 0.3s ease; | ||||
|     backdrop-filter: blur-sm(4px); | ||||
|   } | ||||
|  | ||||
|   /* Transition spinner animation */ | ||||
|   .transition-spinner { | ||||
|     width: 30px; | ||||
|     height: 30px; | ||||
|     border: 2px solid rgba(0, 0, 0, 0.1); | ||||
|     border-radius: 50%; | ||||
|     border-top-color: #3b82f6; | ||||
|     animation: spin 0.7s linear infinite; | ||||
|   } | ||||
|  | ||||
|   :global(.dark) .transition-spinner { | ||||
|     border-color: rgba(255, 255, 255, 0.1); | ||||
|     border-top-color: #60a5fa; | ||||
|   } | ||||
|  | ||||
|   @keyframes spin { | ||||
|     to { | ||||
|       transform: rotate(360deg); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Content entrance animation */ | ||||
|   main { | ||||
|     opacity: 1; | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/layouts/TransitionLayout.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/layouts/TransitionLayout.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| --- | ||||
| import { ClientRouter } from 'astro:transitions'; | ||||
| import BaseLayout from './BaseLayout.astro'; | ||||
|  | ||||
| const { title, description } = Astro.props; | ||||
| --- | ||||
|  | ||||
| <BaseLayout title={title} description={description}> | ||||
|   <ClientRouter fallback="swap" /> | ||||
|  | ||||
|   <div transition:animate="slide"> | ||||
|     <slot /> | ||||
|   </div> | ||||
| </BaseLayout> | ||||
|  | ||||
| <style> | ||||
|   /* Custom transition styles */ | ||||
|   ::view-transition-old(root) { | ||||
|     animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left; | ||||
|   } | ||||
|  | ||||
|   ::view-transition-new(root) { | ||||
|     animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right; | ||||
|   } | ||||
| </style> | ||||
| @@ -5,7 +5,6 @@ import Layout from '../layouts/Layout.astro'; | ||||
| <Layout title="404 - Page Not Found"> | ||||
|   <div | ||||
|     class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|     <!-- Animated background elements --> | ||||
|     <div class="absolute inset-0 overflow-hidden"> | ||||
| @@ -49,8 +48,7 @@ import Layout from '../layouts/Layout.astro'; | ||||
|         > | ||||
|           <span | ||||
|             class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100" | ||||
|           > | ||||
|           </span> | ||||
|           ></span> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             fill="none" | ||||
| @@ -63,8 +61,7 @@ import Layout from '../layouts/Layout.astro'; | ||||
|               stroke-linecap="round" | ||||
|               stroke-linejoin="round" | ||||
|               d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" | ||||
|             > | ||||
|             </path> | ||||
|             ></path> | ||||
|           </svg> | ||||
|           <span class="relative z-10 font-medium">Return Home</span> | ||||
|         </a> | ||||
| @@ -84,9 +81,7 @@ import Layout from '../layouts/Layout.astro'; | ||||
|             <path | ||||
|               stroke-linecap="round" | ||||
|               stroke-linejoin="round" | ||||
|               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" | ||||
|             > | ||||
|             </path> | ||||
|               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> | ||||
|           </svg> | ||||
|           <span class="font-medium">Go Back</span> | ||||
|         </button> | ||||
| @@ -132,6 +127,66 @@ import Layout from '../layouts/Layout.astro'; | ||||
|     const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; | ||||
|     funFactElement.textContent = randomFact; | ||||
|   } | ||||
|  | ||||
|   // Handle SPA transitions for 404 page | ||||
|   function setupSPATransitions() { | ||||
|     // Handle all internal links for SPA transitions | ||||
|     document.querySelectorAll('a[href^="/"]').forEach((link) => { | ||||
|       // Skip links that are anchor links, external links, or already processed | ||||
|       if ( | ||||
|         link.getAttribute('href').includes('#') || | ||||
|         link.getAttribute('target') === '_blank' || | ||||
|         link.hasAttribute('data-spa-handled') | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Mark as handled to avoid duplicate listeners | ||||
|       link.setAttribute('data-spa-handled', 'true'); | ||||
|  | ||||
|       link.addEventListener('click', (e) => { | ||||
|         // Don't handle if modifier keys are pressed (for opening in new tab, etc.) | ||||
|         if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         e.preventDefault(); | ||||
|         const targetHref = link.getAttribute('href'); | ||||
|  | ||||
|         // Trigger page transition animation | ||||
|         const pageTransition = document.getElementById('page-transition'); | ||||
|         if (pageTransition) { | ||||
|           pageTransition.classList.remove('opacity-0'); | ||||
|           pageTransition.classList.add('opacity-100'); | ||||
|  | ||||
|           // Navigate after transition effect | ||||
|           setTimeout(() => { | ||||
|             window.location.href = targetHref; | ||||
|           }, 300); | ||||
|         } else { | ||||
|           // Fallback if transition element doesn't exist | ||||
|           window.location.href = targetHref; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     // Re-initialize back button after SPA navigation | ||||
|     const backButton = document.getElementById('back-button'); | ||||
|     if (backButton) { | ||||
|       backButton.addEventListener('click', () => { | ||||
|         window.history.back(); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Initialize on first load | ||||
|   document.addEventListener('DOMContentLoaded', setupSPATransitions); | ||||
|  | ||||
|   // Re-initialize when content changes via Astro's view transitions | ||||
|   document.addEventListener('astro:page-load', setupSPATransitions); | ||||
|  | ||||
|   // For compatibility with custom transition system | ||||
|   document.addEventListener('page-transition-complete', setupSPATransitions); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| --- | ||||
| import BaseLayout from '../layouts/BaseLayout.astro'; | ||||
| import DynamicIcon from '../utils/DynamicIcon.tsx'; | ||||
| import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa'; | ||||
| import { SiTypescript, SiAstro } from 'react-icons/si'; | ||||
|  | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton, readItems } from '@directus/sdk'; | ||||
| @@ -16,10 +17,7 @@ const skills = await directus.request( | ||||
| --- | ||||
|  | ||||
| <BaseLayout title="About Me" description={global.description}> | ||||
|   <div | ||||
|     class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|   <div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"> | ||||
|     <!-- Hero Section --> | ||||
|     <div class="relative mb-12 sm:mb-16 md:mb-20"> | ||||
|       <!-- Decorative elements --> | ||||
| @@ -121,16 +119,13 @@ const skills = await directus.request( | ||||
|         <!-- Main slider container --> | ||||
|         <div class="slider-track animate-slide flex"> | ||||
|           { | ||||
|             [...skills, ...skills, ...skills].map((skill, index) => ( | ||||
|               <div | ||||
|                 key={`${skill.title}-${index}`} | ||||
|                 class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600" | ||||
|               > | ||||
|             skills.map((skill, index) => ( | ||||
|               <div class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"> | ||||
|                 <div class="p-4 sm:p-6"> | ||||
|                   <div class="mb-4 flex items-center justify-between sm:mb-6"> | ||||
|                     <div class="flex items-center gap-2 sm:gap-4"> | ||||
|                       <div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200"> | ||||
|                         <DynamicIcon name={skill.icon} /> | ||||
|                         <skill.icon /> | ||||
|                       </div> | ||||
|                       <h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100"> | ||||
|                         {skill.title} | ||||
| @@ -169,6 +164,7 @@ const skills = await directus.request( | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Contact Section --> | ||||
|     <div class="theme-transition-all mx-auto max-w-3xl text-center"> | ||||
|       <h2 | ||||
| @@ -277,7 +273,7 @@ const skills = await directus.request( | ||||
|     z-index: 10; | ||||
|   } | ||||
|  | ||||
|   /* Reduce animation complexity on mobile */ | ||||
|   /* Reduce animation complexity on mobile for better performance */ | ||||
|   @media (max-width: 640px) { | ||||
|     .skill-card { | ||||
|       transition: | ||||
| @@ -337,7 +333,7 @@ const skills = await directus.request( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Touch targets for mobile */ | ||||
|   /* Improved touch targets for mobile */ | ||||
|   @media (max-width: 640px) { | ||||
|     a, | ||||
|     button { | ||||
| @@ -369,7 +365,8 @@ const skills = await directus.request( | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|   // Wait for the DOM to be fully loaded | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const sliderTrack = document.querySelector('.slider-track'); | ||||
|  | ||||
|     // Create seamless infinite scrolling effect | ||||
| @@ -377,6 +374,9 @@ const skills = await directus.request( | ||||
|       const cards = document.querySelectorAll('.skill-card'); | ||||
|       if (!cards.length) return; | ||||
|  | ||||
|       // Clone the first set of cards and append to create seamless loop | ||||
|       const firstSetCount = cards.length / 3; // We have 3 sets in the markup | ||||
|  | ||||
|       // Set proper animation based on screen size | ||||
|       function updateScrollAnimation() { | ||||
|         if (window.innerWidth >= 640) { | ||||
| @@ -461,7 +461,9 @@ const skills = await directus.request( | ||||
|  | ||||
|     // Handle theme transition | ||||
|     document.addEventListener('themeChange', () => { | ||||
|       // Add special effects during theme transition | ||||
|       cards.forEach((card, index) => { | ||||
|         // Add staggered animation delay | ||||
|         setTimeout(() => { | ||||
|           card.classList.add('theme-changing'); | ||||
|           setTimeout(() => { | ||||
| @@ -472,3 +474,104 @@ const skills = await directus.request( | ||||
|     }); | ||||
|   }); | ||||
| </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> | ||||
|  | ||||
| <script> | ||||
|   // Removing TOC-related functions | ||||
|  | ||||
|   // Add copy buttons to code blocks | ||||
|   function initializeCodeCopyButtons() { | ||||
|     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 | ||||
|   function initializeBlogPost() { | ||||
|     // Initialize remaining components | ||||
|     initializeCodeCopyButtons(); | ||||
|     setupSPATransitions(); | ||||
|  | ||||
|     // Scroll to hash if present in URL | ||||
|     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 | ||||
|   document.addEventListener('astro:page-load', initializeBlogPost); | ||||
|  | ||||
|   // For compatibility with custom transition system | ||||
|   document.addEventListener('page-transition-complete', initializeBlogPost); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Removing TOC-related styles */ | ||||
|  | ||||
|   /* Language badge styling */ | ||||
|   .language-badge { | ||||
|     font-family: | ||||
|   | ||||
| @@ -22,11 +22,19 @@ const postsByYear = sortedPosts.reduce((acc, post) => { | ||||
| }, {}); | ||||
|  | ||||
| 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"> | ||||
|   <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"> | ||||
|       <!-- Decorative elements --> | ||||
|       <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" | ||||
|       > | ||||
| @@ -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="mb-8 space-y-4 md:sticky md:top-24 md:mb-0"> | ||||
|           <h3 | ||||
| @@ -148,7 +156,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <!-- Post grid for mobile --> | ||||
|       <!-- Improved post grid for mobile --> | ||||
|       <div class="md:col-span-9"> | ||||
|         { | ||||
|           years.map((year) => ( | ||||
| @@ -160,7 +168,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|               <div | ||||
|                 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"> | ||||
|                     {post.image && ( | ||||
|                       <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; | ||||
|   } | ||||
|  | ||||
|   /* Touch targets for mobile */ | ||||
|   /* Improved touch targets for mobile */ | ||||
|   @media (max-width: 640px) { | ||||
|     a, | ||||
|     button { | ||||
| @@ -307,7 +315,8 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
| </style> | ||||
|  | ||||
| <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'); | ||||
|  | ||||
|     if (backToTopButton) { | ||||
| @@ -332,7 +341,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|  | ||||
|       // Check scroll position | ||||
|       window.addEventListener('scroll', toggleBackToTopButton); | ||||
|       toggleBackToTopButton(); | ||||
|       toggleBackToTopButton(); // Initial check | ||||
|     } | ||||
|  | ||||
|     // 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> | ||||
|   | ||||
| @@ -22,13 +22,10 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
| --- | ||||
|  | ||||
| <Layout title=`Home | ${global.name}`> | ||||
|   <!-- Hero Section with mobile responsiveness --> | ||||
|   <section | ||||
|     class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|   <!-- Hero Section with improved mobile responsiveness --> | ||||
|   <section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"> | ||||
|     <div class="relative mx-auto max-w-2xl"> | ||||
|       <!-- Adjusted blob positions and sizes for mobile appearance --> | ||||
|       <!-- Adjusted blob positions and sizes for better mobile appearance --> | ||||
|       <div | ||||
|         class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50" | ||||
|       > | ||||
| @@ -88,7 +85,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|     </div> | ||||
|   </section> | ||||
|  | ||||
|   <!-- Featured post section --> | ||||
|   <!-- Featured Post Section - Improved for mobile --> | ||||
|   <section | ||||
|     class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800" | ||||
|   > | ||||
| @@ -127,7 +124,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|         </a> | ||||
|       </div> | ||||
|  | ||||
|       <!-- Grid for mobile layout --> | ||||
|       <!-- Improved grid for better mobile layout --> | ||||
|       <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3"> | ||||
|         { | ||||
|           recentPosts.map((post, index) => ( | ||||
| @@ -218,7 +215,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|     </div> | ||||
|   </section> | ||||
|  | ||||
|   <!-- Topics section --> | ||||
|   <!-- Topics/Tags Section - Improved for mobile --> | ||||
|   { | ||||
|     allTags.length > 0 && ( | ||||
|       <section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"> | ||||
| @@ -281,7 +278,8 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|  | ||||
| <script> | ||||
|   // Add hover effect for cards on touch devices | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     // Check if it's a touch device | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|  | ||||
|     if (isTouchDevice) { | ||||
| @@ -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'); | ||||
|     } | ||||
|  | ||||
|     // Viewport height fix for mobile browsers | ||||
|     // Improved viewport height fix for mobile browsers | ||||
|     const setVh = () => { | ||||
|       const vh = window.innerHeight * 0.01; | ||||
|       document.documentElement.style.setProperty('--vh', `${vh}px`); | ||||
| @@ -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', () => { | ||||
|       // Store current scroll position | ||||
|       const scrollPosition = window.scrollY; | ||||
| @@ -479,6 +477,58 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|       animateContent(); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // SPA transition handling for homepage | ||||
|   function setupSPATransitions() { | ||||
|     // Handle all internal links for SPA transitions | ||||
|     document.querySelectorAll('a[href^="/"]').forEach((link) => { | ||||
|       // Skip links that are anchor links, external links, or already processed | ||||
|       if ( | ||||
|         link.getAttribute('href').includes('#') || | ||||
|         link.getAttribute('target') === '_blank' || | ||||
|         link.hasAttribute('data-spa-handled') | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Mark as handled to avoid duplicate listeners | ||||
|       link.setAttribute('data-spa-handled', 'true'); | ||||
|  | ||||
|       link.addEventListener('click', (e) => { | ||||
|         // Don't handle if modifier keys are pressed (for opening in new tab, etc.) | ||||
|         if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         e.preventDefault(); | ||||
|         const targetHref = link.getAttribute('href'); | ||||
|  | ||||
|         // Trigger page transition animation | ||||
|         const pageTransition = document.getElementById('page-transition'); | ||||
|         if (pageTransition) { | ||||
|           pageTransition.classList.remove('opacity-0'); | ||||
|           pageTransition.classList.add('opacity-100'); | ||||
|  | ||||
|           // Navigate after transition effect | ||||
|           setTimeout(() => { | ||||
|             window.location.href = targetHref; | ||||
|           }, 300); | ||||
|         } else { | ||||
|           // Fallback if transition element doesn't exist | ||||
|           window.location.href = targetHref; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Initialize on first load | ||||
|   document.addEventListener('DOMContentLoaded', setupSPATransitions); | ||||
|  | ||||
|   // Re-initialize when content changes via Astro's view transitions | ||||
|   document.addEventListener('astro:page-load', setupSPATransitions); | ||||
|  | ||||
|   // For compatibility with custom transition system | ||||
|   document.addEventListener('page-transition-complete', setupSPATransitions); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| @@ -536,4 +586,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|     opacity: 1 !important; | ||||
|     transform: translateY(0) !important; | ||||
|   } | ||||
|  | ||||
|   /* Rest of your existing styles... */ | ||||
| </style> | ||||
|   | ||||
| @@ -14,6 +14,7 @@ export async function getStaticPaths() { | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   // Get all unique tags | ||||
|   const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))]; | ||||
|  | ||||
|   // Create a path for each tag | ||||
| @@ -40,6 +41,7 @@ const sortedPosts = | ||||
|     : []; | ||||
| console.log(`Sorted posts length: ${sortedPosts.length}`); | ||||
|  | ||||
| const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360); | ||||
| const relatedTags = [ | ||||
|   ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)), | ||||
| ].slice(0, 5); | ||||
| @@ -47,6 +49,7 @@ const relatedTags = [ | ||||
|  | ||||
| <BaseLayout title={`Posts tagged with "${tag}"`}> | ||||
|   <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> | ||||
|     <!-- Header section --> | ||||
|     <div class="relative mb-10 sm:mb-16"> | ||||
|       <div | ||||
|         class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30" | ||||
| @@ -274,7 +277,7 @@ const relatedTags = [ | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Empty state --> | ||||
|     <!-- Empty state với màu zinc --> | ||||
|     { | ||||
|       sortedPosts.length === 0 && ( | ||||
|         <div class="py-12 text-center sm:py-20"> | ||||
| @@ -420,3 +423,98 @@ const relatedTags = [ | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
|   // Handle SPA transitions for tag pages | ||||
|   function setupSPATransitions() { | ||||
|     // Handle all internal links for SPA transitions | ||||
|     document.querySelectorAll('a[href^="/"]').forEach((link) => { | ||||
|       // Skip links that are anchor links, external links, or already processed | ||||
|       if ( | ||||
|         link.getAttribute('href').includes('#') || | ||||
|         link.getAttribute('target') === '_blank' || | ||||
|         link.hasAttribute('data-spa-handled') | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Mark as handled to avoid duplicate listeners | ||||
|       link.setAttribute('data-spa-handled', 'true'); | ||||
|  | ||||
|       link.addEventListener('click', (e) => { | ||||
|         // Don't handle if modifier keys are pressed (for opening in new tab, etc.) | ||||
|         if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         e.preventDefault(); | ||||
|         const targetHref = link.getAttribute('href'); | ||||
|  | ||||
|         // Trigger page transition animation | ||||
|         const pageTransition = document.getElementById('page-transition'); | ||||
|         if (pageTransition) { | ||||
|           pageTransition.classList.remove('opacity-0'); | ||||
|           pageTransition.classList.add('opacity-100'); | ||||
|  | ||||
|           // Navigate after transition effect | ||||
|           setTimeout(() => { | ||||
|             window.location.href = targetHref; | ||||
|           }, 300); | ||||
|         } else { | ||||
|           // Fallback if transition element doesn't exist | ||||
|           window.location.href = targetHref; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     // Initialize animations for tag page | ||||
|     function animateTagContent() { | ||||
|       // Animate header elements | ||||
|       const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description'); | ||||
|       headerElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Animate posts with staggered delay | ||||
|       const articles = document.querySelectorAll('article'); | ||||
|       articles.forEach((article, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             article.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           400 + index * 100 | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Animate related tags | ||||
|       const relatedTags = document.querySelectorAll('.related-tags a'); | ||||
|       relatedTags.forEach((tag, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             tag.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           600 + index * 50 | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Run animations | ||||
|     animateTagContent(); | ||||
|   } | ||||
|  | ||||
|   // Initialize on first load | ||||
|   document.addEventListener('DOMContentLoaded', setupSPATransitions); | ||||
|  | ||||
|   // Re-initialize when content changes via Astro's view transitions | ||||
|   document.addEventListener('astro:page-load', setupSPATransitions); | ||||
|  | ||||
|   // For compatibility with custom transition system | ||||
|   document.addEventListener('page-transition-complete', setupSPATransitions); | ||||
| </script> | ||||
|  | ||||
| <!-- Add this at the end of your page --> | ||||
|   | ||||
| @@ -30,10 +30,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | ||||
| --- | ||||
|  | ||||
| <BaseLayout title="Explore Tags"> | ||||
|   <div | ||||
|     class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|   <div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16"> | ||||
|     <!-- Enhanced header section with animated elements - improved for mobile --> | ||||
|     <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16"> | ||||
|       <div | ||||
|         class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50" | ||||
| @@ -148,7 +146,9 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | ||||
| </BaseLayout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|   // Ultra-reliable responsiveness handling | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     // Fix viewport width issues on mobile | ||||
|     const fixViewportWidth = () => { | ||||
|       // Force the viewport to be exactly the width of the device | ||||
|       const viewport = document.querySelector('meta[name="viewport"]'); | ||||
| @@ -378,6 +378,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | ||||
|     width: 100% !important; | ||||
|   } | ||||
|  | ||||
|   /* Ultra-responsive breakpoints for extreme reliability */ | ||||
|   /* Micro screens (below 240px) */ | ||||
|   @media (max-width: 239px) { | ||||
|     .tag-cloud { | ||||
| @@ -544,7 +545,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | ||||
|     hyphens: auto; | ||||
|   } | ||||
|  | ||||
|   /* Shadow for dark mode */ | ||||
|   /* Improved shadow for dark mode */ | ||||
|   :global(.dark) .tag-cloud { | ||||
|     box-shadow: | ||||
|       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> | ||||
|  | ||||
| <script> | ||||
|   // Handle SPA transitions for tags index page | ||||
|   function setupSPATransitions() { | ||||
|     // Handle all internal links for SPA transitions | ||||
|     document.querySelectorAll('a[href^="/"]').forEach((link) => { | ||||
|       // Skip links that are anchor links, external links, or already processed | ||||
|       if ( | ||||
|         link.getAttribute('href').includes('#') || | ||||
|         link.getAttribute('target') === '_blank' || | ||||
|         link.hasAttribute('data-spa-handled') | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Mark as handled to avoid duplicate listeners | ||||
|       link.setAttribute('data-spa-handled', 'true'); | ||||
|  | ||||
|       link.addEventListener('click', (e) => { | ||||
|         // Don't handle if modifier keys are pressed (for opening in new tab, etc.) | ||||
|         if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         e.preventDefault(); | ||||
|         const targetHref = link.getAttribute('href'); | ||||
|  | ||||
|         // Trigger page transition animation | ||||
|         const pageTransition = document.getElementById('page-transition'); | ||||
|         if (pageTransition) { | ||||
|           pageTransition.classList.remove('opacity-0'); | ||||
|           pageTransition.classList.add('opacity-100'); | ||||
|  | ||||
|           // Navigate after transition effect | ||||
|           setTimeout(() => { | ||||
|             window.location.href = targetHref; | ||||
|           }, 300); | ||||
|         } else { | ||||
|           // Fallback if transition element doesn't exist | ||||
|           window.location.href = targetHref; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     // Add hover effect for tag cards on touch devices | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|  | ||||
|     if (isTouchDevice) { | ||||
|       const tagCards = document.querySelectorAll('.tag-cloud a'); | ||||
|  | ||||
|       tagCards.forEach((card) => { | ||||
|         card.addEventListener('touchstart', () => { | ||||
|           card.classList.add('is-touched'); | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('touchend', () => { | ||||
|           setTimeout(() => { | ||||
|             card.classList.remove('is-touched'); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Animate tag cards with staggered delay | ||||
|     const tagCards = document.querySelectorAll('.tag-cloud a'); | ||||
|     tagCards.forEach((card, index) => { | ||||
|       setTimeout( | ||||
|         () => { | ||||
|           card.classList.add('animate-reveal'); | ||||
|         }, | ||||
|         100 + index * 50 | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Initialize on first load | ||||
|   document.addEventListener('DOMContentLoaded', setupSPATransitions); | ||||
|  | ||||
|   // Re-initialize when content changes via Astro's view transitions | ||||
|   document.addEventListener('astro:page-load', setupSPATransitions); | ||||
|  | ||||
|   // For compatibility with custom transition system | ||||
|   document.addEventListener('page-transition-complete', setupSPATransitions); | ||||
| </script> | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| /* Remove all the complex mobile menu styles and keep only what's necessary */ | ||||
| @import 'tailwindcss'; | ||||
|  | ||||
| /* Dark mode support for Tailwind CSS v4 */ | ||||
| /* https://tailwindcss.com/docs/dark-mode */ | ||||
| @custom-variant dark (&:where(.dark, .dark *)); | ||||
|  | ||||
| @layer base { | ||||
|   :root { | ||||
|     font-family: 'Inter', sans-serif; | ||||
| @@ -15,7 +12,6 @@ | ||||
|   html { | ||||
|     scroll-behavior: smooth; | ||||
|     scroll-padding-top: 5rem; | ||||
|     overflow-y: scroll; | ||||
|   } | ||||
|  | ||||
|   body { | ||||
| @@ -42,7 +38,7 @@ | ||||
|     scroll-padding-top: 4rem; | ||||
|   } | ||||
|  | ||||
|   /* Touch targets on mobile */ | ||||
|   /* Better touch targets on mobile */ | ||||
|   button, | ||||
|   a { | ||||
|     @apply min-h-[44px]; | ||||
| @@ -136,3 +132,20 @@ a.hover:hover, | ||||
| button:hover { | ||||
|   transform: translateY(-2px); | ||||
| }  | ||||
|  | ||||
| /* Smooth page transitions */ | ||||
| .page-transition { | ||||
|   transition: | ||||
|     opacity 0.3s ease, | ||||
|     transform 0.3s ease; | ||||
| } | ||||
|  | ||||
| .page-entering { | ||||
|   opacity: 0; | ||||
|   transform: translateY(10px); | ||||
| } | ||||
|  | ||||
| .page-entered { | ||||
|   opacity: 1; | ||||
|   transform: translateY(0); | ||||
| } | ||||
|   | ||||
| @@ -1,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": { | ||||
|     "lib": ["dom", "dom.iterable", "esnext"], | ||||
|     "allowJs": true, | ||||
|     "allowImportingTsExtensions": true, | ||||
|     "target": "ES6", | ||||
|     "skipLibCheck": true, | ||||
|     "strict": true, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user