Compare commits
	
		
			1 Commits
		
	
	
		
			1.1.1
			...
			aa36aca92d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| aa36aca92d | 
| @@ -3,7 +3,7 @@ name: release-image | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - 1.* | ||||
|       - 0.* | ||||
|  | ||||
|   workflow_dispatch: | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ jobs: | ||||
|       - name: Set up Node.js | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 22.17.1 | ||||
|           node-version: 22.17.x | ||||
|           cache: pnpm | ||||
|  | ||||
|       - name: Install Dependencies | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| ARG REGISTRY=docker.io | ||||
| FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base | ||||
|  | ||||
| LABEL version="1.1.1" | ||||
| LABEL version="0.10.0" | ||||
| LABEL description="Astro based personal website" | ||||
|  | ||||
| ENV PNPM_HOME="/pnpm" | ||||
|   | ||||
| @@ -23,8 +23,6 @@ export default defineConfig({ | ||||
|     plugins: [tailwindcss()], | ||||
|   }, | ||||
|  | ||||
|   output: 'server', | ||||
|  | ||||
|   adapter: node({ | ||||
|     mode: 'standalone', | ||||
|   }), | ||||
|   | ||||
| @@ -9,7 +9,6 @@ type Global = { | ||||
|   email: string; | ||||
|   portrait: string; | ||||
|   portrait_alt: string; | ||||
|   logo: string; | ||||
|   about: string; | ||||
| }; | ||||
| 
 | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "site-profile", | ||||
|   "type": "module", | ||||
|   "version": "1.1.1", | ||||
|   "version": "0.10.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "astro dev", | ||||
| @@ -31,13 +31,13 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/typography": "^0.5.16", | ||||
|     "@typescript-eslint/parser": "8.38.0", | ||||
|     "eslint": "9.32.0", | ||||
|     "eslint-config-prettier": "10.1.8", | ||||
|     "@typescript-eslint/parser": "8.37.0", | ||||
|     "eslint": "9.31.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.38.0" | ||||
|     "typescript-eslint": "8.37.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										903
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										903
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 28 KiB | 
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| Before Width: | Height: | Size: 9.9 KiB | 
| @@ -55,13 +55,13 @@ | ||||
|   /* Grid pattern for dots */ | ||||
|   .bg-grid-pattern { | ||||
|     background-size: 24px 24px; | ||||
|     background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px); | ||||
|     background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px); | ||||
|     transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1); | ||||
|   } | ||||
|  | ||||
|   /* Dark mode version */ | ||||
|   :global(.dark) .bg-grid-pattern { | ||||
|     background-image: radial-gradient(circle, rgba(255, 255, 255, 0.15) 1px, transparent 1px); | ||||
|     background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px); | ||||
|   } | ||||
|  | ||||
|   /* Ambient glow animations */ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| --- | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
| @@ -10,8 +10,8 @@ const currentYear = new Date().getFullYear(); | ||||
| const navLinks = [ | ||||
|   { text: 'Home', href: '/' }, | ||||
|   { text: 'Blog', href: '/blog' }, | ||||
|   { text: 'Topics', href: '/topics' }, | ||||
|   { text: 'About', href: '/about' }, | ||||
|   { text: 'RSS', href: '/rss' }, | ||||
| ]; | ||||
|  | ||||
| const socialLinks = [ | ||||
| @@ -59,15 +59,19 @@ const socialLinks = [ | ||||
|         <div class="col-span-1 md:col-span-3"> | ||||
|           <a href="/" class="group inline-block"> | ||||
|             <div class="flex items-center"> | ||||
|               <div class="mx-auto aspect-square overflow-hidden rounded-lg"> | ||||
|                 <img | ||||
|                   src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.logo}` | ||||
|                   alt="logo" | ||||
|                   class="max-h-[40px] max-w-[40px] object-cover" | ||||
|                   loading="eager" | ||||
|                 /> | ||||
|               <div | ||||
|                 class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400" | ||||
|               > | ||||
|                 <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> | ||||
|                 <div | ||||
|                   class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100" | ||||
|                 > | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <span | ||||
|                 class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" | ||||
|               > | ||||
| @@ -111,7 +115,7 @@ const socialLinks = [ | ||||
|         <!-- Quick links --> | ||||
|         <div class="col-span-1 md:col-span-3"> | ||||
|           <h3 | ||||
|             class="theme-transition-color after:bg-turquoise dark:after:bg-turquoise relative inline-block pb-2 text-sm font-semibold tracking-wider text-zinc-900 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-zinc-100" | ||||
|             class="theme-transition-color relative inline-block pb-2 text-sm font-semibold tracking-wider text-zinc-900 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700" | ||||
|           > | ||||
|             Navigation | ||||
|           </h3> | ||||
| @@ -125,6 +129,7 @@ const socialLinks = [ | ||||
|                   > | ||||
|                     <span class="relative inline-block overflow-hidden"> | ||||
|                       <span class="relative z-10">{link.text}</span> | ||||
|                       <span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" /> | ||||
|                     </span> | ||||
|                   </a> | ||||
|                 </li> | ||||
|   | ||||
| @@ -10,22 +10,7 @@ const parsedDate = typeof date === 'string' ? new Date(date) : date; | ||||
|  | ||||
| { | ||||
|   parsedDate && ( | ||||
|     <time datetime={parsedDate.toISOString()} class="z-10 flex items-center gap-1.5"> | ||||
|       <svg | ||||
|         xmlns="http://www.w3.org/2000/svg" | ||||
|         fill="none" | ||||
|         viewBox="0 0 24 24" | ||||
|         stroke-width="1.5" | ||||
|         stroke="currentColor" | ||||
|         class="h-3.5 w-3.5 sm:h-4 sm:w-4" | ||||
|       > | ||||
|         <path | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|           d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0 | ||||
|                       A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" | ||||
|         /> | ||||
|       </svg> | ||||
|     <time datetime={parsedDate.toISOString()}> | ||||
|       {parsedDate.toLocaleDateString('en-us', { | ||||
|         year: 'numeric', | ||||
|         month: 'long', | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| --- | ||||
| import ThemeToggle from './ThemeToggle.astro'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
| const links = await directus.request(readSingleton('links')); | ||||
|  | ||||
| const navItems = [ | ||||
|   { text: 'Home', href: '/' }, | ||||
|   { text: 'Blog', href: '/blog' }, | ||||
|   { text: 'Topics', href: '/topics' }, | ||||
|   { text: 'About', href: '/about' }, | ||||
|   { text: 'Gitea', href: links.gitea }, | ||||
|   { text: 'RSS', href: 'rss.xml' }, | ||||
| ]; | ||||
|  | ||||
| @@ -25,19 +24,7 @@ const currentPath = pathname.slice(1); | ||||
| > | ||||
|   <div class="mx-auto flex max-w-3xl items-center justify-between px-4"> | ||||
|     <!-- Logo --> | ||||
|     <a | ||||
|       href="/" | ||||
|       class="from-midnight to-turquoise relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br text-xl shadow-lg transition-transform" | ||||
|     > | ||||
|       <div class="mx-auto aspect-square overflow-hidden rounded-lg"> | ||||
|         <img | ||||
|           src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.logo}` | ||||
|           alt="logo" | ||||
|           class="max-h-[40px] max-w-[40px] object-cover" | ||||
|           loading="eager" | ||||
|         /> | ||||
|       </div> | ||||
|     </a> | ||||
|     <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a> | ||||
|  | ||||
|     <!-- Desktop navigation --> | ||||
|     <nav class="hidden items-center space-x-6 sm:flex"> | ||||
| @@ -49,8 +36,8 @@ const currentPath = pathname.slice(1); | ||||
|               href={item.href} | ||||
|               class={`text-sm font-medium ${ | ||||
|                 isActive | ||||
|                   ? 'text-zinc-900 dark:text-zinc-100' | ||||
|                   : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100' | ||||
|                   ? 'text-zinc-900 dark:text-white' | ||||
|                   : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' | ||||
|               }`} | ||||
|             > | ||||
|               {item.text} | ||||
| @@ -114,8 +101,8 @@ const currentPath = pathname.slice(1); | ||||
|             href={item.href} | ||||
|             class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${ | ||||
|               isActive | ||||
|                 ? 'text-zinc-900 dark:text-zinc-100' | ||||
|                 : 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100' | ||||
|                 ? 'text-zinc-900 dark:text-white' | ||||
|                 : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' | ||||
|             }`} | ||||
|             style={`transition-delay: ${index * 0.05}s;`} | ||||
|           > | ||||
|   | ||||
| @@ -8,21 +8,16 @@ const { tags = [], class: className = '' } = Astro.props; | ||||
| --- | ||||
|  | ||||
| { | ||||
|   tags && ( | ||||
|     <div class={`mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start ${className}`}> | ||||
|       {tags.slice(0, 2).map((postTag) => ( | ||||
|   tags.length > 0 && ( | ||||
|     <div class={`mt-3 flex flex-wrap gap-2 ${className}`}> | ||||
|       {tags.map((tag) => ( | ||||
|         <a | ||||
|           href={`/tags/${postTag}`} | ||||
|           class={`inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700`} | ||||
|           href={`/tag/${tag}`} | ||||
|           class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" | ||||
|         > | ||||
|           #{postTag} | ||||
|           {tag} | ||||
|         </a> | ||||
|       ))} | ||||
|       {tags.length > 2 && ( | ||||
|         <span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-900 dark:text-zinc-400"> | ||||
|           +{tags.length - 2} | ||||
|         </span> | ||||
|       )} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| <button | ||||
|   id="theme-toggle" | ||||
|   data-theme-toggle | ||||
|   class="group hover:bg-desert/50 dark:hover:bg-midnight/50 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 focus:ring-2 focus:ring-zinc-300 focus:outline-hidden sm:p-2 dark:focus:ring-zinc-700" | ||||
|   class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:ring-2 focus:ring-zinc-300 focus:outline-hidden sm:p-2 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700" | ||||
|   aria-label="Toggle dark mode" | ||||
| > | ||||
|   <div class="relative z-10 flex h-5 w-5 items-center justify-center"> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| --- | ||||
| import Layout from './Layout.astro'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import ShareButtons from '../components/ShareButtons.astro'; | ||||
| import TagList from '../components/TagList.astro'; | ||||
| import './styles/markdown.css'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readItems } from '@directus/sdk'; | ||||
|  | ||||
| export async function getStaticPaths() { | ||||
| @@ -18,6 +18,7 @@ export async function getStaticPaths() { | ||||
| } | ||||
|  | ||||
| const post = Astro.props; | ||||
| const published_date: string = post.published_date.toLocaleString(); | ||||
|  | ||||
| let canonicalURL; | ||||
| try { | ||||
| @@ -30,30 +31,18 @@ try { | ||||
|  | ||||
| <Layout title={post.title} description={post.description}> | ||||
|   <article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl"> | ||||
|     <div class="hero-text mb-12"> | ||||
|     <div class="mb-12"> | ||||
|       <h1 | ||||
|         class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100" | ||||
|       > | ||||
|         {post.title} | ||||
|       </h1> | ||||
|  | ||||
|       <p | ||||
|         class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400" | ||||
|       > | ||||
|         <!-- {post.description} --> | ||||
|       </p> | ||||
|  | ||||
|       <div | ||||
|         class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400" | ||||
|       > | ||||
|         <FormattedDate date={post.published_date} /> | ||||
|       <div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400"> | ||||
|         <FormattedDate date={published_date} /> | ||||
|       </div> | ||||
|  | ||||
|       <div | ||||
|         class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400" | ||||
|       > | ||||
|         <TagList tags={post.tags} /> | ||||
|       </div> | ||||
|       <TagList tags={post.tags} class="mt-2" /> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Hero image --> | ||||
| @@ -96,60 +85,7 @@ try { | ||||
|   <slot name="after-article" /> | ||||
| </Layout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       // Animate hero section | ||||
|       const heroElements = document.querySelectorAll( | ||||
|         '.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a' | ||||
|       ); | ||||
|       heroElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Animate posts with staggered delay | ||||
|       const articles = document.querySelectorAll('article.group'); | ||||
|       articles.forEach((article, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             article.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           500 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     animateContent(); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Content reveal animations */ | ||||
|   .hero-text h1, | ||||
|   .hero-text div, | ||||
|   .hero-text ~ div, | ||||
|   .hero-text span, | ||||
|   .hero-text p, | ||||
|   .hero-text + a, | ||||
|   article.group { | ||||
|     opacity: 0; | ||||
|     transform: translateY(20px); | ||||
|     transition: | ||||
|       opacity 0.8s ease, | ||||
|       transform 0.8s ease; | ||||
|   } | ||||
|  | ||||
|   .animate-reveal { | ||||
|     opacity: 1 !important; | ||||
|     transform: translateY(0) !important; | ||||
|   } | ||||
|  | ||||
|   /* Hero image styling */ | ||||
|   article img:first-of-type { | ||||
|     border-radius: 1rem; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ const { title, description } = Astro.props; | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width" /> | ||||
|     <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||
|     <link rel="icon" type="image/svg+xml" href="/favicon.png" /> | ||||
|     <meta name="generator" content={Astro.generator} /> | ||||
|     <meta name="description" content={description} /> | ||||
|     <title>{title}</title> | ||||
|   | ||||
| @@ -7,6 +7,22 @@ import Layout from '../layouts/Layout.astro'; | ||||
|     class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|     <!-- Animated background elements --> | ||||
|     <div class="absolute inset-0 overflow-hidden"> | ||||
|       <div | ||||
|         class="animate-blob absolute -top-20 -left-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 absolute top-1/2 right-1/4 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40" | ||||
|       > | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Main content with animation --> | ||||
|     <div class="relative z-10 mx-auto max-w-xl"> | ||||
|       <div class="glitch-wrapper"> | ||||
| @@ -29,8 +45,12 @@ import Layout from '../layouts/Layout.astro'; | ||||
|       <div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"> | ||||
|         <a | ||||
|           href="/" | ||||
|           class="relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-bg-turquoise hover:text-zinc-100 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-turquoise" | ||||
|           class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200" | ||||
|         > | ||||
|           <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> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             fill="none" | ||||
| @@ -51,7 +71,7 @@ import Layout from '../layouts/Layout.astro'; | ||||
|  | ||||
|         <button | ||||
|           id="back-button" | ||||
|           class="group inline-flex translate-y-0 items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-xs transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800" | ||||
|           class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-xs transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800" | ||||
|         > | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
| @@ -115,6 +135,34 @@ import Layout from '../layouts/Layout.astro'; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Animation for floating blobs */ | ||||
|   @keyframes blob { | ||||
|     0% { | ||||
|       transform: translate(0px, 0px) scale(1); | ||||
|     } | ||||
|     33% { | ||||
|       transform: translate(30px, -50px) scale(1.1); | ||||
|     } | ||||
|     66% { | ||||
|       transform: translate(-20px, 20px) scale(0.9); | ||||
|     } | ||||
|     100% { | ||||
|       transform: translate(0px, 0px) scale(1); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .animate-blob { | ||||
|     animation: blob 7s infinite; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-2000 { | ||||
|     animation-delay: 2s; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-4000 { | ||||
|     animation-delay: 4s; | ||||
|   } | ||||
|  | ||||
|   /* Glitch effect for 404 text */ | ||||
|   .glitch-wrapper { | ||||
|     position: relative; | ||||
|   | ||||
| @@ -2,11 +2,12 @@ | ||||
| import BaseLayout from '../layouts/BaseLayout.astro'; | ||||
| import DynamicIcon from '../utils/DynamicIcon.tsx'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readSingleton, readItems } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
| const about = await directus.request(readSingleton('about')); | ||||
|  | ||||
| const skills = await directus.request( | ||||
|   readItems('skills', { | ||||
|     fields: ['*'], | ||||
| @@ -19,21 +20,40 @@ const skills = await directus.request( | ||||
|     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" | ||||
|   > | ||||
|     <!-- Introduction Section --> | ||||
|     <!-- Hero Section --> | ||||
|     <div class="relative mb-12 sm:mb-16 md:mb-20"> | ||||
|       <!-- Decorative elements --> | ||||
|       <div | ||||
|         class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|  | ||||
|       <div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12"> | ||||
|         <div class="hero-text order-2 text-center md:order-1 md:text-left"> | ||||
|         <div class="order-2 text-center md:order-1 md:text-left"> | ||||
|           <h1 | ||||
|             class="theme-transition-color hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100" | ||||
|             class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100" | ||||
|           > | ||||
|             Hello, I'm <span class="theme-transition-all bg-clip-text">{global.name}</span> | ||||
|             Hello, I'm <span | ||||
|               class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100" | ||||
|               >{global.name}</span | ||||
|             > | ||||
|           </h1> | ||||
|  | ||||
|           <p | ||||
|             class="theme-transition-color hero-text mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400" | ||||
|             class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400" | ||||
|           > | ||||
|             {about.background} | ||||
|           </p> | ||||
|  | ||||
|           <div | ||||
|             class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start" | ||||
|           > | ||||
|             <!-- Social links remain the same --> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="relative order-1 md:order-2"> | ||||
| @@ -47,39 +67,42 @@ const skills = await directus.request( | ||||
|               loading="eager" | ||||
|             /> | ||||
|           </div> | ||||
|  | ||||
|           <!-- Decorative elements --> | ||||
|           <div | ||||
|             class="theme-transition-all absolute -right-4 -bottom-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg sm:-right-6 sm:-bottom-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24 dark:border-zinc-900 dark:bg-zinc-800" | ||||
|           > | ||||
|             <span class="text-2xl sm:text-3xl">👋</span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- About Me section --> | ||||
|     <!-- About Section --> | ||||
|     <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24"> | ||||
|       <div class="mx-auto max-w-3xl"> | ||||
|         <h2 | ||||
|           class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 sm:mb-8 sm:text-3xl md:justify-start dark:text-zinc-100" | ||||
|         > | ||||
|           <span class="theme-transition-bg bg-turquoise mr-4 hidden h-1 w-8 sm:inline-block sm:w-12" | ||||
|           <span | ||||
|             class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700" | ||||
|           ></span> | ||||
|           About Me | ||||
|           <span class="theme-transition-bg bg-turquoise ml-4 hidden h-1 w-8 sm:inline-block sm:w-12" | ||||
|           <span | ||||
|             class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700" | ||||
|           ></span> | ||||
|         </h2> | ||||
|  | ||||
|         <div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none"> | ||||
|           <p | ||||
|             class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg" | ||||
|           > | ||||
|         <div class="theme-transition-all prose prose-zinc dark:prose-invert max-w-none"> | ||||
|           <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> | ||||
|             {about.experience} | ||||
|           </p> | ||||
|  | ||||
|           <p | ||||
|             class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg" | ||||
|           > | ||||
|           <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> | ||||
|             {about.education} | ||||
|           </p> | ||||
|  | ||||
|           <p | ||||
|             class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg" | ||||
|           > | ||||
|           <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> | ||||
|             {about.certifications} | ||||
|           </p> | ||||
|         </div> | ||||
| @@ -99,7 +122,10 @@ const skills = await directus.request( | ||||
|         <div class="slider-track animate-slide flex"> | ||||
|           { | ||||
|             [...skills, ...skills, ...skills].map((skill, index) => ( | ||||
|               <div class="skill-card theme-transition-element Ztransition-all mx-2 min-w-[220px] transform rounded-xl border border-zinc-300 bg-white duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-200 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-900 dark:hover:border-zinc-800 dark:hover:bg-zinc-900"> | ||||
|               <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" | ||||
|               > | ||||
|                 <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"> | ||||
| @@ -117,7 +143,7 @@ const skills = await directus.request( | ||||
|  | ||||
|                   <div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 sm:h-2 dark:bg-zinc-700"> | ||||
|                     <div | ||||
|                       class="progress-bar-animate theme-transition-bg from-turquoise via-bermuda to-turquoise absolute top-0 left-0 h-full rounded-full bg-gradient-to-r transition-all duration-1000" | ||||
|                       class="progress-bar-animate theme-transition-bg absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200" | ||||
|                       style={`width: ${skill.level}%`} | ||||
|                     /> | ||||
|                   </div> | ||||
| @@ -143,7 +169,6 @@ const skills = await directus.request( | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Contact Section --> | ||||
|     <div class="theme-transition-all mx-auto max-w-3xl text-center"> | ||||
|       <h2 | ||||
| @@ -157,112 +182,57 @@ const skills = await directus.request( | ||||
|         I'm always open to new opportunities and collaborations. If you'd like to work together or | ||||
|         just say hello, feel free to reach out. | ||||
|       </p> | ||||
|       <div class="group"> | ||||
|         <a | ||||
|           href=`mailto:${global.email}` | ||||
|           class="theme-transition-all group-hover:bg-turquoise inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors duration-300 group-hover:text-zinc-100 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:group-hover:text-zinc-100" | ||||
|  | ||||
|       <a | ||||
|         href=`mailto:${global.email}` | ||||
|         class="hover theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300" | ||||
|       > | ||||
|         <svg | ||||
|           xmlns="http://www.w3.org/2000/svg" | ||||
|           class="mr-2 h-4 w-4 sm:h-5 sm:w-5" | ||||
|           fill="none" | ||||
|           viewBox="0 0 24 24" | ||||
|           stroke="currentColor" | ||||
|         > | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             class="mr-2 h-4 w-4 sm:h-5 sm:w-5" | ||||
|             fill="none" | ||||
|             viewBox="0 0 24 24" | ||||
|             stroke="currentColor" | ||||
|           > | ||||
|             <path | ||||
|               stroke-linecap="round" | ||||
|               stroke-linejoin="round" | ||||
|               stroke-width="2" | ||||
|               d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" | ||||
|             ></path> | ||||
|           </svg> | ||||
|           <span class="relative inline-block overflow-hidden"> | ||||
|             <span class="relative z-10">Send Email</span> | ||||
|           </span> | ||||
|         </a> | ||||
|       </div> | ||||
|           <path | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" | ||||
|             stroke-width="2" | ||||
|             d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" | ||||
|           ></path> | ||||
|         </svg> | ||||
|         Say Hello | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
| </BaseLayout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       const heroElements = document.querySelectorAll( | ||||
|         '.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p' | ||||
|       ); | ||||
|       heroElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     animateContent(); | ||||
|  | ||||
|     // Create seamless infinite scrolling effect | ||||
|     function setupInfiniteScroll() { | ||||
|       const cards = document.querySelectorAll('.skill-card'); | ||||
|       if (!cards.length) return; | ||||
|     } | ||||
|  | ||||
|     setupInfiniteScroll(); | ||||
|  | ||||
|     // Add hover effects to cards - only on non-touch devices | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|     const cards = document.querySelectorAll('.skill-card'); | ||||
|  | ||||
|     if (!isTouchDevice) { | ||||
|       cards.forEach((card) => { | ||||
|         card.addEventListener('mousemove', (e) => { | ||||
|           const rect = card.getBoundingClientRect(); | ||||
|           const x = e.clientX - rect.left; | ||||
|           const y = e.clientY - rect.top; | ||||
|  | ||||
|           const centerX = rect.width / 2; | ||||
|           const centerY = rect.height / 2; | ||||
|  | ||||
|           const angleX = (y - centerY) / 15; | ||||
|           const angleY = (centerX - x) / 15; | ||||
|  | ||||
|           card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`; | ||||
|  | ||||
|           // Dynamic shadow based on tilt | ||||
|           const shadowX = (x - centerX) / 25; | ||||
|           const shadowY = (y - centerY) / 25; | ||||
|           card.style.boxShadow = ` | ||||
|             ${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1), | ||||
|             0 10px 20px rgba(0, 0, 0, 0.05) | ||||
|           `; | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('mouseleave', () => { | ||||
|           card.style.transform = ''; | ||||
|           card.style.boxShadow = ''; | ||||
|         }); | ||||
|       }); | ||||
|     } else { | ||||
|       // Simpler effects for touch devices | ||||
|       cards.forEach((card) => { | ||||
|         card.addEventListener('touchstart', () => { | ||||
|           card.classList.add('is-touched'); | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('touchend', () => { | ||||
|           setTimeout(() => { | ||||
|             card.classList.remove('is-touched'); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Blob animation */ | ||||
|   .animate-blob { | ||||
|     animation: blob-bounce 8s infinite ease; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-2000 { | ||||
|     animation-delay: 2s; | ||||
|   } | ||||
|  | ||||
|   @keyframes blob-bounce { | ||||
|     0%, | ||||
|     100% { | ||||
|       transform: translate(0, 0) scale(1); | ||||
|     } | ||||
|     25% { | ||||
|       transform: translate(5%, 5%) scale(1.05); | ||||
|     } | ||||
|     50% { | ||||
|       transform: translate(0, 10%) scale(1); | ||||
|     } | ||||
|     75% { | ||||
|       transform: translate(-5%, 5%) scale(0.95); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Tech Stack Slider */ | ||||
|   .slider-track { | ||||
|     width: fit-content; | ||||
| @@ -381,4 +351,124 @@ const skills = await directus.request( | ||||
|       min-height: 44px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Theme transition effect */ | ||||
|   :global(.theme-switching) .theme-transition-element { | ||||
|     animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1); | ||||
|   } | ||||
|  | ||||
|   /* Smooth card transition during theme switch */ | ||||
|   .skill-card.theme-transition-element { | ||||
|     transition: | ||||
|       background-color var(--theme-transition), | ||||
|       border-color var(--theme-transition), | ||||
|       color var(--theme-transition), | ||||
|       box-shadow var(--theme-transition), | ||||
|       transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     const sliderTrack = document.querySelector('.slider-track'); | ||||
|  | ||||
|     // Create seamless infinite scrolling effect | ||||
|     function setupInfiniteScroll() { | ||||
|       const cards = document.querySelectorAll('.skill-card'); | ||||
|       if (!cards.length) return; | ||||
|  | ||||
|       // Set proper animation based on screen size | ||||
|       function updateScrollAnimation() { | ||||
|         if (window.innerWidth >= 640) { | ||||
|           sliderTrack.style.animation = 'scroll 60s linear infinite'; | ||||
|         } else { | ||||
|           sliderTrack.style.animation = 'scroll 40s linear infinite'; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       updateScrollAnimation(); | ||||
|       window.addEventListener('resize', updateScrollAnimation); | ||||
|     } | ||||
|  | ||||
|     setupInfiniteScroll(); | ||||
|  | ||||
|     // Pause animation on hover/touch | ||||
|     sliderTrack?.addEventListener('mouseenter', () => { | ||||
|       sliderTrack.style.animationPlayState = 'paused'; | ||||
|     }); | ||||
|  | ||||
|     sliderTrack?.addEventListener('touchstart', () => { | ||||
|       sliderTrack.style.animationPlayState = 'paused'; | ||||
|     }); | ||||
|  | ||||
|     sliderTrack?.addEventListener('mouseleave', () => { | ||||
|       sliderTrack.style.animationPlayState = 'running'; | ||||
|     }); | ||||
|  | ||||
|     sliderTrack?.addEventListener('touchend', () => { | ||||
|       setTimeout(() => { | ||||
|         sliderTrack.style.animationPlayState = 'running'; | ||||
|       }, 1000); // Delay resuming animation after touch | ||||
|     }); | ||||
|  | ||||
|     // Add hover effects to cards - only on non-touch devices | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|     const cards = document.querySelectorAll('.skill-card'); | ||||
|  | ||||
|     if (!isTouchDevice) { | ||||
|       cards.forEach((card) => { | ||||
|         card.addEventListener('mousemove', (e) => { | ||||
|           const rect = card.getBoundingClientRect(); | ||||
|           const x = e.clientX - rect.left; | ||||
|           const y = e.clientY - rect.top; | ||||
|  | ||||
|           const centerX = rect.width / 2; | ||||
|           const centerY = rect.height / 2; | ||||
|  | ||||
|           const angleX = (y - centerY) / 15; | ||||
|           const angleY = (centerX - x) / 15; | ||||
|  | ||||
|           card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`; | ||||
|  | ||||
|           // Dynamic shadow based on tilt | ||||
|           const shadowX = (x - centerX) / 25; | ||||
|           const shadowY = (y - centerY) / 25; | ||||
|           card.style.boxShadow = ` | ||||
|             ${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1), | ||||
|             0 10px 20px rgba(0, 0, 0, 0.05) | ||||
|           `; | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('mouseleave', () => { | ||||
|           card.style.transform = ''; | ||||
|           card.style.boxShadow = ''; | ||||
|         }); | ||||
|       }); | ||||
|     } else { | ||||
|       // Simpler effects for touch devices | ||||
|       cards.forEach((card) => { | ||||
|         card.addEventListener('touchstart', () => { | ||||
|           card.classList.add('is-touched'); | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('touchend', () => { | ||||
|           setTimeout(() => { | ||||
|             card.classList.remove('is-touched'); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Handle theme transition | ||||
|     document.addEventListener('themeChange', () => { | ||||
|       cards.forEach((card, index) => { | ||||
|         setTimeout(() => { | ||||
|           card.classList.add('theme-changing'); | ||||
|           setTimeout(() => { | ||||
|             card.classList.remove('theme-changing'); | ||||
|           }, 600); | ||||
|         }, index * 50); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| --- | ||||
| import BlogPost from '../../layouts/BlogPost.astro'; | ||||
|  | ||||
| import directus from '../../lib/directus'; | ||||
| import directus from '../../../lib/directus'; | ||||
| import { readItems } from '@directus/sdk'; | ||||
|  | ||||
| export const prerender = true; | ||||
|  | ||||
| export async function getStaticPaths() { | ||||
|   const posts = await directus.request( | ||||
|     readItems('posts', { | ||||
| @@ -43,14 +41,14 @@ const { post, nextPost, prevPost } = Astro.props; | ||||
|   updated_date={post.updated_date} | ||||
|   tags={post.tags} | ||||
| > | ||||
|   <!-- Main Content --> | ||||
|   <!-- Main Content - Enhanced with better typography and spacing --> | ||||
|   <div | ||||
|     class="hero-text prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none" | ||||
|     class="prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none" | ||||
|   > | ||||
|     <div set:html={post.content} /> | ||||
|   </div> | ||||
|  | ||||
|   <!-- Next/Previous Navigation --> | ||||
|   <!-- Next/Previous Navigation - Improved responsive design --> | ||||
|   <div | ||||
|     class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800" | ||||
|   > | ||||
| @@ -118,37 +116,6 @@ const { post, nextPost, prevPost } = Astro.props; | ||||
| </BlogPost> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       // Animate hero section | ||||
|       const heroElements = document.querySelectorAll( | ||||
|         '.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p' | ||||
|       ); | ||||
|       heroElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Animate posts with staggered delay | ||||
|       const articles = document.querySelectorAll('article.group'); | ||||
|       articles.forEach((article, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             article.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           500 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     animateContent(); | ||||
|   }); | ||||
|  | ||||
|   // Add copy buttons to code blocks | ||||
|   function initializeCodeCopyButtons() { | ||||
|     const codeBlocks = document.querySelectorAll('pre'); | ||||
| @@ -254,7 +221,7 @@ const { post, nextPost, prevPost } = Astro.props; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Typography for blog content  */ | ||||
|   /* Enhanced typography for blog content - Responsive adjustments */ | ||||
|   .prose { | ||||
|     @reference text-zinc-800 dark:text-zinc-200; | ||||
|   } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| --- | ||||
| import BaseLayout from '../../layouts/BaseLayout.astro'; | ||||
| import FormattedDate from '../../components/FormattedDate.astro'; | ||||
| import TagList from '../../components/TagList.astro'; | ||||
|  | ||||
| import directus from '../../lib/directus'; | ||||
| import directus from '../../../lib/directus'; | ||||
| import { readItems } from '@directus/sdk'; | ||||
|  | ||||
| const posts = await directus.request( | ||||
| @@ -13,57 +11,81 @@ const posts = await directus.request( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| // Group posts by year for timeline effect | ||||
| const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf()); | ||||
|  | ||||
| // Group posts by year for timeline effect | ||||
| const postsByYear = sortedPosts.reduce((acc, post) => { | ||||
|   const year = new Date(post.published_date).getFullYear(); | ||||
|   if (!acc[year]) acc[year] = []; | ||||
|   acc[year].push(post); | ||||
|   return acc; | ||||
| }, {}); | ||||
|  | ||||
| const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
| --- | ||||
|  | ||||
| <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="relative mb-12 sm:mb-20"> | ||||
|       <div class="hero-text relative text-center"> | ||||
|       <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" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|  | ||||
|       <div class="relative text-center"> | ||||
|         <h1 | ||||
|           class="hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100" | ||||
|           class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100" | ||||
|         > | ||||
|           Blog | ||||
|         </h1> | ||||
|  | ||||
|         <p | ||||
|           class="hero-text mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400" | ||||
|           class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400" | ||||
|         > | ||||
|           A couple thoughts, a few ideas, and some guides on technology, development, and selfhosting. | ||||
|           Thoughts, ideas, and explorations on technology and selfhosting. | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Featured post --> | ||||
|     <!-- Grid layout for mobile experience --> | ||||
|     <div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12"> | ||||
|       <!-- Featured post (if exists) --> | ||||
|       { | ||||
|         sortedPosts.length > 0 && ( | ||||
|           <div class="mb-8 sm:mb-12 md:col-span-12"> | ||||
|             <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8"> | ||||
|               <div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" /> | ||||
|  | ||||
|               <div class="flex flex-col gap-5 sm:flex-row sm:gap-6"> | ||||
|             <article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 sm:pb-8 dark:border-zinc-800"> | ||||
|               <div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row"> | ||||
|                 {sortedPosts[0].image && ( | ||||
|                   <div class="z-10 mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2"> | ||||
|                   <div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2"> | ||||
|                     <img | ||||
|                       src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}?width=500`} | ||||
|                       alt={sortedPosts[0].image_alt} | ||||
|                       class="h-full w-full object-cover" | ||||
|                       src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`} | ||||
|                       alt={sortedPosts[0].title} | ||||
|                       class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0" | ||||
|                       loading="eager" | ||||
|                     /> | ||||
|                   </div> | ||||
|                 )} | ||||
|  | ||||
|                 <div class="z-10 flex-1"> | ||||
|                   <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100"> | ||||
|                 <div class="flex flex-1 flex-col justify-center"> | ||||
|                   <div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 sm:text-sm md:justify-start dark:text-zinc-400"> | ||||
|                     <span class="font-medium tracking-wider uppercase">Featured</span> | ||||
|                     <span class="h-px w-6 bg-zinc-300 sm:w-8 dark:bg-zinc-700" /> | ||||
|                     {sortedPosts[0].published_date && ( | ||||
|                       <time datetime={sortedPosts[0].published_date.toLocaleString()}> | ||||
|                         {sortedPosts[0].published_date.toLocaleString('en-US', { | ||||
|                           year: 'numeric', | ||||
|                           month: 'long', | ||||
|                           day: 'numeric', | ||||
|                         })} | ||||
|                       </time> | ||||
|                     )} | ||||
|                   </div> | ||||
|  | ||||
|                   <h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-4 sm:text-3xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                     <a | ||||
|                       href={`/blog/${sortedPosts[0].slug}/`} | ||||
|                       class="before:absolute before:inset-0" | ||||
| @@ -72,44 +94,23 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|                     </a> | ||||
|                   </h2> | ||||
|  | ||||
|                   <p class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"> | ||||
|                     {/* {sortedPosts[0].description} */} | ||||
|                   <p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 sm:mb-6 sm:text-base md:text-left dark:text-zinc-400"> | ||||
|                     {sortedPosts[0].description} | ||||
|                   </p> | ||||
|  | ||||
|                   <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> | ||||
|                     <FormattedDate date={sortedPosts[0].published_date} /> | ||||
|                   <div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start"> | ||||
|                     {sortedPosts[0].tags && ( | ||||
|                       <div class="flex flex-wrap justify-center gap-2 md:justify-start"> | ||||
|                         {sortedPosts[0].tags.slice(0, 2).map((tag) => ( | ||||
|                           <span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400"> | ||||
|                             {tag} | ||||
|                           </span> | ||||
|                         ))} | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800"> | ||||
|                 <TagList tags={sortedPosts[0].tags} /> | ||||
|  | ||||
|                 <div class="mx-auto sm:mr-0 sm:ml-auto"> | ||||
|                   <a | ||||
|                     href={`/blog/${sortedPosts[0].slug}`} | ||||
|                     class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100" | ||||
|                   > | ||||
|                     <span class="relative inline-block overflow-hidden"> | ||||
|                       <span class="relative z-10">Read article</span> | ||||
|                       <span class="bg-turquoise absolute bottom-0 left-0 h-0.5 w-0 transition-all duration-300 group-hover:w-full" /> | ||||
|                     </span> | ||||
|                     <svg | ||||
|                       viewBox="0 0 16 16" | ||||
|                       fill="none" | ||||
|                       aria-hidden="true" | ||||
|                       class="ml-1 h-4 w-4 stroke-current transition-transform duration-300" | ||||
|                     > | ||||
|                       <path | ||||
|                         d="M6.75 5.75 9.25 8l-2.5 2.25" | ||||
|                         stroke-width="1.5" | ||||
|                         stroke-linecap="round" | ||||
|                         stroke-linejoin="round" | ||||
|                       /> | ||||
|                     </svg> | ||||
|                   </a> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </article> | ||||
|           </div> | ||||
|         ) | ||||
| @@ -121,9 +122,10 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|           <h3 | ||||
|             class="mb-4 text-center text-lg font-medium tracking-wider text-zinc-900 uppercase md:text-left dark:text-zinc-100" | ||||
|           > | ||||
|             History | ||||
|             Archive | ||||
|           </h3> | ||||
|  | ||||
|           <!-- Horizontal scrollable archive on mobile, vertical on desktop --> | ||||
|           <div | ||||
|             class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0" | ||||
|           > | ||||
| @@ -131,12 +133,12 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|               years.map((year, index) => ( | ||||
|                 <a | ||||
|                   href={`#year-${year}`} | ||||
|                   class={`mr-3 flex items-center rounded-xl border border-zinc-300 bg-white/50 px-4 py-2 whitespace-nowrap transition-all duration-300 hover:bg-zinc-50 sm:rounded-2xl md:mr-0 md:w-full md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-800/70 ${index === 0 ? 'bg-white/50 dark:bg-zinc-900/50' : ''}`} | ||||
|                   class={`hover mr-3 flex items-center rounded-full border-b border-zinc-100 px-4 py-2 whitespace-nowrap transition-colors hover:bg-zinc-50 md:mr-0 md:w-full md:rounded-none md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-900 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`} | ||||
|                 > | ||||
|                   <span class="mr-3 ml-3 text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100"> | ||||
|                   <span class="text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100"> | ||||
|                     {year} | ||||
|                   </span> | ||||
|                   <span class="mr-3 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400"> | ||||
|                   <span class="ml-2 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400"> | ||||
|                     {postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''} | ||||
|                   </span> | ||||
|                 </a> | ||||
| @@ -146,7 +148,7 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <!-- Post grid --> | ||||
|       <!-- Post grid for mobile --> | ||||
|       <div class="md:col-span-9"> | ||||
|         { | ||||
|           years.map((year) => ( | ||||
| @@ -159,15 +161,13 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|                 class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`} | ||||
|               > | ||||
|                 {postsByYear[year].map((post) => ( | ||||
|                   <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8"> | ||||
|                     <div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" /> | ||||
|  | ||||
|                   <article class="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="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg"> | ||||
|                       <div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56"> | ||||
|                         <img | ||||
|                           src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`} | ||||
|                           alt={post.title} | ||||
|                           class="h-full w-full object-cover" | ||||
|                           class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0" | ||||
|                           loading="lazy" | ||||
|                         /> | ||||
|                       </div> | ||||
| @@ -188,48 +188,30 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|                         )} | ||||
|                       </div> | ||||
|  | ||||
|                       <h3 class="z-10 mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                       <h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                         <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> | ||||
|                           {post.title} | ||||
|                         </a> | ||||
|                       </h3> | ||||
|  | ||||
|                       <p class="z-10 mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400"> | ||||
|                         {/* {post.description} */} | ||||
|                       <p class="mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400"> | ||||
|                         {post.description} | ||||
|                       </p> | ||||
|  | ||||
|                       <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> | ||||
|                         <FormattedDate date={post.published_date} /> | ||||
|                       </div> | ||||
|  | ||||
|                       <div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800"> | ||||
|                         <TagList tags={post.tags} /> | ||||
|  | ||||
|                         <div class="mx-auto sm:mr-0 sm:ml-auto"> | ||||
|                           <a | ||||
|                             href={`/blog/${post.slug}`} | ||||
|                             class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100" | ||||
|                           > | ||||
|                             <span class="relative inline-block overflow-hidden"> | ||||
|                               <span class="relative z-10">Read article</span> | ||||
|                               <span class="bg-turquoise absolute bottom-0 left-0 h-0.5 w-0 transition-all duration-300 group-hover:w-full" /> | ||||
|                       {post.tags && ( | ||||
|                         <div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start"> | ||||
|                           {post.tags.slice(0, 2).map((tag) => ( | ||||
|                             <span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400"> | ||||
|                               {tag} | ||||
|                             </span> | ||||
|                             <svg | ||||
|                               viewBox="0 0 16 16" | ||||
|                               fill="none" | ||||
|                               aria-hidden="true" | ||||
|                               class="ml-1 h-4 w-4 stroke-current transition-transform duration-300" | ||||
|                             > | ||||
|                               <path | ||||
|                                 d="M6.75 5.75 9.25 8l-2.5 2.25" | ||||
|                                 stroke-width="1.5" | ||||
|                                 stroke-linecap="round" | ||||
|                                 stroke-linejoin="round" | ||||
|                               /> | ||||
|                             </svg> | ||||
|                           </a> | ||||
|                           ))} | ||||
|                           {post.tags.length > 2 && ( | ||||
|                             <span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400"> | ||||
|                               +{post.tags.length - 2} | ||||
|                             </span> | ||||
|                           )} | ||||
|                         </div> | ||||
|                       </div> | ||||
|                       )} | ||||
|                     </div> | ||||
|                   </article> | ||||
|                 ))} | ||||
| @@ -242,40 +224,62 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|   </div> | ||||
| </BaseLayout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       // Animate hero section | ||||
|       const heroElements = document.querySelectorAll( | ||||
|         '.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p' | ||||
|       ); | ||||
|       heroElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // Animate posts with staggered delay | ||||
|       const articles = document.querySelectorAll('article.group'); | ||||
|       articles.forEach((article, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             article.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           500 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     animateContent(); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Blob animation */ | ||||
|   .animate-blob { | ||||
|     animation: blob-bounce 8s infinite ease; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-2000 { | ||||
|     animation-delay: 2s; | ||||
|   } | ||||
|  | ||||
|   @keyframes blob-bounce { | ||||
|     0%, | ||||
|     100% { | ||||
|       transform: translate(0, 0) scale(1); | ||||
|     } | ||||
|     25% { | ||||
|       transform: translate(5%, 5%) scale(1.05); | ||||
|     } | ||||
|     50% { | ||||
|       transform: translate(0, 10%) scale(1); | ||||
|     } | ||||
|     75% { | ||||
|       transform: translate(-5%, 5%) scale(0.95); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Search container hover effect */ | ||||
|   .search-container:hover .search-pulse { | ||||
|     animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | ||||
|   } | ||||
|  | ||||
|   @keyframes pulse { | ||||
|     0%, | ||||
|     100% { | ||||
|       opacity: 0; | ||||
|     } | ||||
|     50% { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Input focus animation */ | ||||
|   input:focus + div .search-pulse { | ||||
|     animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; | ||||
|   } | ||||
|  | ||||
|   /* Hide scrollbar but keep functionality */ | ||||
|   .hide-scrollbar { | ||||
|     -ms-overflow-style: none; | ||||
|     scrollbar-width: none; | ||||
|   } | ||||
|  | ||||
|   .hide-scrollbar::-webkit-scrollbar { | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   /* Line clamp for descriptions */ | ||||
|   .line-clamp-2 { | ||||
|     display: -webkit-box; | ||||
| @@ -291,38 +295,6 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   /* Prevent layout shifts */ | ||||
|   .grow { | ||||
|     grow: 1; | ||||
|   } | ||||
|  | ||||
|   .min-w-0 { | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   /* Ensure container doesn't overflow */ | ||||
|   .overflow-hidden { | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   /* Ensure text doesn't overflow on small screens */ | ||||
|   .truncate { | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
|     max-width: 100%; | ||||
|   } | ||||
|  | ||||
|   /* Ensure proper word breaking for long tag names */ | ||||
|   .break-words { | ||||
|     word-break: break-word; | ||||
|     overflow-wrap: break-word; | ||||
|   } | ||||
|  | ||||
|   .hyphens-auto { | ||||
|     hyphens: auto; | ||||
|   } | ||||
|  | ||||
|   /* Touch targets for mobile */ | ||||
|   @media (max-width: 640px) { | ||||
|     a, | ||||
| @@ -332,12 +304,73 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a); | ||||
|       align-items: center; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .touch-active { | ||||
|     transform: scale(0.97) !important; | ||||
|     opacity: 0.9; | ||||
|     transition: | ||||
|       transform 0.15s ease-in-out, | ||||
|       opacity 0.15s ease-in-out !important; | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     const backToTopButton = document.getElementById('back-to-top'); | ||||
|  | ||||
|     if (backToTopButton) { | ||||
|       // Show button when scrolled down | ||||
|       const toggleBackToTopButton = () => { | ||||
|         if (window.scrollY > 300) { | ||||
|           backToTopButton.classList.remove('opacity-0', 'invisible'); | ||||
|           backToTopButton.classList.add('opacity-100', 'visible'); | ||||
|         } else { | ||||
|           backToTopButton.classList.remove('opacity-100', 'visible'); | ||||
|           backToTopButton.classList.add('opacity-0', 'invisible'); | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       // Scroll to top when clicked | ||||
|       backToTopButton.addEventListener('click', () => { | ||||
|         window.scrollTo({ | ||||
|           top: 0, | ||||
|           behavior: 'smooth', | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       // Check scroll position | ||||
|       window.addEventListener('scroll', toggleBackToTopButton); | ||||
|       toggleBackToTopButton(); | ||||
|     } | ||||
|  | ||||
|     // Add smooth scrolling to year links | ||||
|     document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => { | ||||
|       anchor.addEventListener('click', function (e) { | ||||
|         e.preventDefault(); | ||||
|         const targetId = this.getAttribute('href'); | ||||
|         const targetElement = document.querySelector(targetId); | ||||
|  | ||||
|         if (targetElement) { | ||||
|           window.scrollTo({ | ||||
|             top: targetElement.offsetTop - 100, | ||||
|             behavior: 'smooth', | ||||
|           }); | ||||
|  | ||||
|           // Update URL hash without jumping | ||||
|           history.pushState(null, null, targetId); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     // Add touch support for hover effects | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|  | ||||
|     if (isTouchDevice) { | ||||
|       const articles = document.querySelectorAll('article'); | ||||
|  | ||||
|       articles.forEach((article) => { | ||||
|         article.addEventListener('touchstart', () => { | ||||
|           article.classList.add('is-touched'); | ||||
|         }); | ||||
|  | ||||
|         article.addEventListener('touchend', () => { | ||||
|           setTimeout(() => { | ||||
|             article.classList.remove('is-touched'); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| --- | ||||
| import Layout from '../layouts/Layout.astro'; | ||||
| import FormattedDate from '../components/FormattedDate.astro'; | ||||
| import TagList from '../components/TagList.astro'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readItems, readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| const global = await directus.request(readSingleton('global')); | ||||
|  | ||||
| const posts = await directus.request( | ||||
|   readItems('posts', { | ||||
|     fields: ['*'], | ||||
| @@ -17,16 +17,27 @@ const posts = await directus.request( | ||||
| const recentPosts = posts | ||||
|   .sort((a, b) => b.published_date.getTime() - a.published_date.getTime()) | ||||
|   .slice(0, 3); | ||||
|  | ||||
| const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5); | ||||
| --- | ||||
|  | ||||
| <Layout title=`Home | ${global.name}`> | ||||
|   <!-- Header section --> | ||||
|   <!-- 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" | ||||
|   > | ||||
|     <div class="relative mx-auto max-w-2xl"> | ||||
|       <!-- Adjusted blob positions and sizes for mobile appearance --> | ||||
|       <div | ||||
|         class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-64 sm:w-64 dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|  | ||||
|       <div class="relative text-center sm:text-left"> | ||||
|         <h1 | ||||
|           class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100" | ||||
| @@ -37,7 +48,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|             <span class="relative inline-block"> | ||||
|               selfhosting. | ||||
|               <span | ||||
|                 class="theme-transition-bg bg-turquoise absolute -bottom-1 left-0 h-1 w-full origin-left transform" | ||||
|                 class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200" | ||||
|               ></span> | ||||
|             </span> | ||||
|           </span> | ||||
| @@ -52,7 +63,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|         > | ||||
|           <a | ||||
|             href="/about" | ||||
|             class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-600 transition-all duration-300 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" | ||||
|             class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300" | ||||
|           > | ||||
|             <span>More about me</span> | ||||
|             <svg | ||||
| @@ -68,6 +79,9 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|                 stroke-linejoin="round" | ||||
|                 d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> | ||||
|             </svg> | ||||
|             <span | ||||
|               class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" | ||||
|             ></span> | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -76,7 +90,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|  | ||||
|   <!-- Featured post section --> | ||||
|   <section | ||||
|     class="theme-transition-all border-t border-zinc-200 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800" | ||||
|     class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800" | ||||
|   > | ||||
|     <div class="mx-auto max-w-3xl"> | ||||
|       <div | ||||
| @@ -89,7 +103,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|         </h2> | ||||
|         <a | ||||
|           href="/blog" | ||||
|           class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-600 transition-all duration-300 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" | ||||
|           class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-100 dark:hover:text-zinc-300" | ||||
|         > | ||||
|           <span class="flex items-center gap-1"> | ||||
|             View all posts | ||||
| @@ -107,22 +121,25 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|                 d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> | ||||
|             </svg> | ||||
|           </span> | ||||
|           <span | ||||
|             class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" | ||||
|           ></span> | ||||
|         </a> | ||||
|       </div> | ||||
|  | ||||
|       <!-- Post grid --> | ||||
|       <!-- Grid for mobile layout --> | ||||
|       <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3"> | ||||
|         { | ||||
|           recentPosts.map((post, index) => ( | ||||
|             <article class="theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0"> | ||||
|               <div class="theme-transition-all absolute -inset-x-4 -inset-y-6 z-0 border border-zinc-300 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 sm:-inset-x-6 sm:rounded-2xl dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70" /> | ||||
|             <article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0"> | ||||
|               <div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl dark:bg-zinc-800/50" /> | ||||
|  | ||||
|               {post.image && ( | ||||
|                 <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg"> | ||||
|                   <img | ||||
|                     src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`} | ||||
|                     alt={post.title} | ||||
|                     class="h-full w-full object-cover" | ||||
|                     class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" | ||||
|                     loading={index === 0 ? 'eager' : 'lazy'} | ||||
|                     width="400" | ||||
|                     height="225" | ||||
| @@ -130,6 +147,12 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               <div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 sm:justify-start sm:gap-x-4 dark:text-zinc-400"> | ||||
|                 <time datetime={post.published_date.toLocaleString()} class="font-medium"> | ||||
|                   <FormattedDate date={post.published_date} /> | ||||
|                 </time> | ||||
|               </div> | ||||
|  | ||||
|               <h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                 <a | ||||
|                   href={`/blog/${post.slug}`} | ||||
| @@ -140,29 +163,45 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|                 </a> | ||||
|               </h3> | ||||
|  | ||||
|               <p class="z-10 mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"> | ||||
|                 {/* {post.description} */} | ||||
|               <p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 sm:mt-3 sm:text-left dark:text-zinc-400"> | ||||
|                 {post.description} | ||||
|               </p> | ||||
|  | ||||
|               <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> | ||||
|                 <FormattedDate date={post.published_date} /> | ||||
|               </div> | ||||
|  | ||||
|               <TagList tags={post.tags} class="z-10" /> | ||||
|               {post.tags && post.tags.length > 0 && ( | ||||
|                 <div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start"> | ||||
|                   {post.tags.slice(0, 3).map((tag) => ( | ||||
|                     <a | ||||
|                       href={`/topics/${tag}`} | ||||
|                       class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 sm:px-3 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" | ||||
|                     > | ||||
|                       #{tag} | ||||
|                     </a> | ||||
|                   ))} | ||||
|                   {post.tags.length > 3 && ( | ||||
|                     <span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400"> | ||||
|                       +{post.tags.length - 3} more | ||||
|                     </span> | ||||
|                   )} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               <a | ||||
|                 href={`/blog/${post.slug}`} | ||||
|                 class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100" | ||||
|               > | ||||
|                 <span class="relative inline-block overflow-hidden"> | ||||
|                   <span class="relative z-10">Read article</span> | ||||
|                   <span class="bg-turquoise absolute bottom-0 left-0 h-0.5 w-0 transition-all duration-300 group-hover:w-full" /> | ||||
|                   <span class="block transition-transform duration-300 group-hover:-translate-y-full"> | ||||
|                     Read article | ||||
|                   </span> | ||||
|                   <span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0"> | ||||
|                     Explore now | ||||
|                   </span> | ||||
|                 </span> | ||||
|                 <svg | ||||
|                   viewBox="0 0 16 16" | ||||
|                   fill="none" | ||||
|                   aria-hidden="true" | ||||
|                   class="ml-1 h-4 w-4 stroke-current transition-transform duration-300" | ||||
|                   class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1" | ||||
|                 > | ||||
|                   <path | ||||
|                     d="M6.75 5.75 9.25 8l-2.5 2.25" | ||||
| @@ -179,13 +218,13 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|     </div> | ||||
|   </section> | ||||
|  | ||||
|   <!-- Tags section --> | ||||
|   <!-- Topics section --> | ||||
|   { | ||||
|     allTags.length > 0 && ( | ||||
|       <section class="theme-transition-all border-t border-zinc-200 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"> | ||||
|       <section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"> | ||||
|         <div class="mx-auto max-w-3xl"> | ||||
|           <h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100"> | ||||
|             Popular Tags | ||||
|             Explore Topics | ||||
|           </h2> | ||||
|  | ||||
|           <div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3"> | ||||
| @@ -193,14 +232,14 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|               const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length; | ||||
|               return ( | ||||
|                 <a | ||||
|                   href={`/tags/${tag}`} | ||||
|                   class="theme-transition-all flex min-h-[80px] flex-col rounded-xl border border-zinc-300 bg-white/50 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:bg-zinc-800/70" | ||||
|                   href={`/topics/${tag}`} | ||||
|                   class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/70" | ||||
|                 > | ||||
|                   <div class="mb-2 flex items-start justify-between"> | ||||
|                     <span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100"> | ||||
|                       #{tag} | ||||
|                     </span> | ||||
|                     <span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700"> | ||||
|                     <span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"> | ||||
|                       {tagCount} {tagCount === 1 ? 'post' : 'posts'} | ||||
|                     </span> | ||||
|                   </div> | ||||
| @@ -211,6 +250,29 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|               ); | ||||
|             })} | ||||
|           </div> | ||||
|  | ||||
|           <div class="mt-6 text-center sm:mt-8"> | ||||
|             <a | ||||
|               href="/tags" | ||||
|               class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300" | ||||
|             > | ||||
|               <span>View all topics</span> | ||||
|               <svg | ||||
|                 xmlns="http://www.w3.org/2000/svg" | ||||
|                 fill="none" | ||||
|                 viewBox="0 0 24 24" | ||||
|                 stroke-width="1.5" | ||||
|                 stroke="currentColor" | ||||
|                 class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" | ||||
|               > | ||||
|                 <path | ||||
|                   stroke-linecap="round" | ||||
|                   stroke-linejoin="round" | ||||
|                   d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" | ||||
|                 /> | ||||
|               </svg> | ||||
|             </a> | ||||
|           </div> | ||||
|         </div> | ||||
|       </section> | ||||
|     ) | ||||
| @@ -218,7 +280,135 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
| </Layout> | ||||
|  | ||||
| <script> | ||||
|   // Add hover effect for cards on touch devices | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||||
|  | ||||
|     if (isTouchDevice) { | ||||
|       const cards = document.querySelectorAll('.hover-3d'); | ||||
|  | ||||
|       cards.forEach((card) => { | ||||
|         card.addEventListener('touchstart', () => { | ||||
|           card.classList.add('is-touched'); | ||||
|         }); | ||||
|  | ||||
|         card.addEventListener('touchend', () => { | ||||
|           setTimeout(() => { | ||||
|             card.classList.remove('is-touched'); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       // Disable hover animations on touch devices | ||||
|       document.documentElement.classList.add('touch-device'); | ||||
|     } | ||||
|  | ||||
|     // Viewport height fix for mobile browsers | ||||
|     const setVh = () => { | ||||
|       const vh = window.innerHeight * 0.01; | ||||
|       document.documentElement.style.setProperty('--vh', `${vh}px`); | ||||
|     }; | ||||
|  | ||||
|     // Set initial value | ||||
|     setVh(); | ||||
|  | ||||
|     // Update on resize and scroll to prevent content shifting | ||||
|     window.addEventListener('resize', setVh); | ||||
|  | ||||
|     // Use a debounced scroll handler to prevent performance issues | ||||
|     let scrollTimeout; | ||||
|     window.addEventListener('scroll', () => { | ||||
|       if (scrollTimeout) { | ||||
|         window.cancelAnimationFrame(scrollTimeout); | ||||
|       } | ||||
|  | ||||
|       scrollTimeout = window.requestAnimationFrame(() => { | ||||
|         // Lock width during scroll | ||||
|         document.body.style.width = '100%'; | ||||
|         document.body.style.overflowX = 'hidden'; | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     // Fix for iOS Safari address bar height changes | ||||
|     if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { | ||||
|       // Force the layout to use the initial viewport size | ||||
|       document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`); | ||||
|  | ||||
|       // Apply fixed height to sections to prevent resizing | ||||
|       const sections = document.querySelectorAll('section'); | ||||
|       sections.forEach((section) => { | ||||
|         section.style.width = '100%'; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Theme change handler that preserves scroll position and provides smoother transitions | ||||
|     document.addEventListener('themeChanged', () => { | ||||
|       // Store current scroll position | ||||
|       const scrollPosition = window.scrollY; | ||||
|  | ||||
|       // Create a temporary overlay for smoother transition | ||||
|       const overlay = document.createElement('div'); | ||||
|       overlay.style.cssText = ` | ||||
|         position: fixed; | ||||
|         inset: 0; | ||||
|         background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}; | ||||
|         z-index: 9999; | ||||
|         pointer-events: none; | ||||
|         opacity: 0; | ||||
|         transition: opacity 0.3s ease; | ||||
|       `; | ||||
|       document.body.appendChild(overlay); | ||||
|  | ||||
|       // Fade in overlay | ||||
|       requestAnimationFrame(() => { | ||||
|         overlay.style.opacity = '0.5'; | ||||
|  | ||||
|         // Update theme-transition elements without forcing reflow of entire page | ||||
|         requestAnimationFrame(() => { | ||||
|           document | ||||
|             .querySelectorAll( | ||||
|               '.theme-transition-all, .theme-transition-bg, .theme-transition-color' | ||||
|             ) | ||||
|             .forEach((el) => { | ||||
|               // Apply a subtle animation instead of a hard reset | ||||
|               el.style.transition = 'all 0.5s ease'; | ||||
|             }); | ||||
|  | ||||
|           // Fade out overlay after transition completes | ||||
|           setTimeout(() => { | ||||
|             overlay.style.opacity = '0'; | ||||
|             setTimeout(() => { | ||||
|               overlay.remove(); | ||||
|             }, 300); | ||||
|           }, 300); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       // Restore scroll position (prevents jumping to top) | ||||
|       if (scrollPosition > 0) { | ||||
|         setTimeout(() => { | ||||
|           window.scrollTo({ | ||||
|             top: scrollPosition, | ||||
|             behavior: 'auto', // Use 'auto' to prevent animation | ||||
|           }); | ||||
|         }, 10); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Fix theme inconsistency issues by checking theme on visibility change | ||||
|     document.addEventListener('visibilitychange', () => { | ||||
|       if (document.visibilityState === 'visible') { | ||||
|         const storedTheme = localStorage.getItem('theme'); | ||||
|         const currentThemeIsDark = document.documentElement.classList.contains('dark'); | ||||
|  | ||||
|         if (storedTheme === 'dark' && !currentThemeIsDark) { | ||||
|           document.documentElement.classList.add('dark'); | ||||
|         } else if (storedTheme === 'light' && currentThemeIsDark) { | ||||
|           document.documentElement.classList.remove('dark'); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       // Animate hero section | ||||
| @@ -257,6 +447,93 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     animateContent(); | ||||
|     // Run animations after the loading screen is hidden | ||||
|     const loadingScreen = document.getElementById('loading-screen'); | ||||
|     if (loadingScreen) { | ||||
|       // Check if loading screen is already hidden (page refresh) | ||||
|       if (loadingScreen.style.display === 'none') { | ||||
|         animateContent(); | ||||
|       } else { | ||||
|         // Wait for loading screen to hide | ||||
|         const observer = new MutationObserver((mutations) => { | ||||
|           mutations.forEach((mutation) => { | ||||
|             if ( | ||||
|               mutation.target === loadingScreen && | ||||
|               mutation.type === 'attributes' && | ||||
|               mutation.attributeName === 'style' && | ||||
|               loadingScreen.style.display === 'none' | ||||
|             ) { | ||||
|               animateContent(); | ||||
|               observer.disconnect(); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         observer.observe(loadingScreen, { attributes: true }); | ||||
|  | ||||
|         // Fallback | ||||
|         setTimeout(animateContent, 3500); | ||||
|       } | ||||
|     } else { | ||||
|       // If loading screen doesn't exist for some reason | ||||
|       animateContent(); | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Fix for theme transition issues */ | ||||
|   :global(:root) { | ||||
|     --theme-transition-duration: 0.5s; | ||||
|     --theme-transition-timing: ease; | ||||
|   } | ||||
|  | ||||
|   :global(html), | ||||
|   :global(body) { | ||||
|     transition: background-color var(--theme-transition-duration) var(--theme-transition-timing); | ||||
|   } | ||||
|  | ||||
|   :global(.theme-transition-all) { | ||||
|     transition: all var(--theme-transition-duration) var(--theme-transition-timing); | ||||
|   } | ||||
|  | ||||
|   :global(.theme-transition-bg) { | ||||
|     transition: background-color var(--theme-transition-duration) var(--theme-transition-timing); | ||||
|   } | ||||
|  | ||||
|   :global(.theme-transition-color) { | ||||
|     transition: color var(--theme-transition-duration) var(--theme-transition-timing); | ||||
|   } | ||||
|  | ||||
|   /* Ensure transitions apply to all theme-related properties */ | ||||
|   :global(*) { | ||||
|     transition-property: background-color, border-color, color, fill, stroke, opacity; | ||||
|     transition-duration: var(--theme-transition-duration); | ||||
|     transition-timing-function: var(--theme-transition-timing); | ||||
|   } | ||||
|  | ||||
|   /* Remove the forced transition disabling which causes flickering */ | ||||
|   :global(.theme-switching), | ||||
|   :global(.theme-switching *) { | ||||
|     /* Use a subtle transition instead of none */ | ||||
|     transition-duration: 0.3s !important; | ||||
|   } | ||||
|  | ||||
|   /* Content reveal animations */ | ||||
|   .hero-text span, | ||||
|   .hero-text + p, | ||||
|   .hero-text ~ div, | ||||
|   article.group, | ||||
|   a.group.flex.flex-col { | ||||
|     opacity: 0; | ||||
|     transform: translateY(20px); | ||||
|     transition: | ||||
|       opacity 0.8s ease, | ||||
|       transform 0.8s ease; | ||||
|   } | ||||
|  | ||||
|   .animate-reveal { | ||||
|     opacity: 1 !important; | ||||
|     transform: translateY(0) !important; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import rss from '@astrojs/rss'; | ||||
|  | ||||
| import directus from '../lib/directus'; | ||||
| import directus from '../../lib/directus'; | ||||
| import { readItems, readSingleton } from '@directus/sdk'; | ||||
|  | ||||
| export async function GET(context: any) { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| import BaseLayout from '../../layouts/BaseLayout.astro'; | ||||
| import FormattedDate from '../../components/FormattedDate.astro'; | ||||
| 
 | ||||
| import directus from '../../lib/directus'; | ||||
| import directus from '../../../lib/directus'; | ||||
| import { readItems } from '@directus/sdk'; | ||||
| 
 | ||||
| export const prerender = true; | ||||
| @@ -32,10 +32,13 @@ export async function getStaticPaths() { | ||||
| const { tag } = Astro.params as { tag: string }; | ||||
| const { posts = [] } = Astro.props; | ||||
| 
 | ||||
| console.log(`Tag: ${tag}, Number of posts: ${posts.length}`); | ||||
| 
 | ||||
| const sortedPosts = | ||||
|   posts && posts.length > 0 | ||||
|     ? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf()) | ||||
|     : []; | ||||
| console.log(`Sorted posts length: ${sortedPosts.length}`); | ||||
| 
 | ||||
| const relatedTags = [ | ||||
|   ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)), | ||||
| @@ -43,11 +46,20 @@ const relatedTags = [ | ||||
| --- | ||||
| 
 | ||||
| <BaseLayout title={`Posts tagged with "${tag}"`}> | ||||
|   <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16" transition:animate="slide"> | ||||
|   <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> | ||||
|     <div class="relative mb-10 sm:mb-16"> | ||||
|       <div | ||||
|         class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl sm:h-48 sm:w-48 dark:bg-zinc-900/20" | ||||
|       > | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="relative text-center sm:text-left"> | ||||
|         <a | ||||
|           href="/blog#topics" | ||||
|           href="/tags" | ||||
|           class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" | ||||
|         > | ||||
|           <svg | ||||
| @@ -61,11 +73,9 @@ const relatedTags = [ | ||||
|             <path | ||||
|               stroke-linecap="round" | ||||
|               stroke-linejoin="round" | ||||
|               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" | ||||
|             > | ||||
|             </path> | ||||
|               d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> | ||||
|           </svg> | ||||
|           <span>Back to blog</span> | ||||
|           <span>Back to all topics</span> | ||||
|           <span | ||||
|             class="block h-0.5 max-w-0 bg-zinc-300 transition-all duration-300 group-hover:max-w-full dark:bg-zinc-700" | ||||
|           ></span> | ||||
| @@ -89,9 +99,8 @@ const relatedTags = [ | ||||
|                 stroke-linecap="round" | ||||
|                 stroke-linejoin="round" | ||||
|                 d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" | ||||
|               > | ||||
|               </path> | ||||
|               <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"> </path> | ||||
|               ></path> | ||||
|               <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"></path> | ||||
|             </svg> | ||||
|           </div> | ||||
| 
 | ||||
| @@ -103,7 +112,7 @@ const relatedTags = [ | ||||
|               <span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700" | ||||
|               ></span> | ||||
|               <span | ||||
|                 class="animate-expand bg-turquoise absolute -bottom-1 left-0 h-1 w-full opacity-70" | ||||
|                 class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100" | ||||
|               ></span> | ||||
|             </span> | ||||
|           </h1> | ||||
| @@ -124,14 +133,14 @@ const relatedTags = [ | ||||
|     <!-- Related tags section --> | ||||
|     { | ||||
|       relatedTags.length > 0 && ( | ||||
|         <div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12"> | ||||
|         <div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12"> | ||||
|           <h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100"> | ||||
|             Related topics | ||||
|           </h2> | ||||
|           <div class="flex flex-nowrap justify-center gap-2 sm:justify-start"> | ||||
|             {relatedTags.map((relatedTag) => ( | ||||
|               <a | ||||
|                 href={`/tags/${relatedTag}`} | ||||
|                 href={`/topics/${relatedTag}`} | ||||
|                 class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" | ||||
|               > | ||||
|                 #{relatedTag} | ||||
| @@ -144,31 +153,55 @@ const relatedTags = [ | ||||
| 
 | ||||
|     <!-- Posts list --> | ||||
|     <div class="relative"> | ||||
|       <div | ||||
|         class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10" | ||||
|       > | ||||
|       <div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="relative space-y-6 sm:space-y-8"> | ||||
|         { | ||||
|           sortedPosts.map((post) => ( | ||||
|             <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8"> | ||||
|               <div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" /> | ||||
|             <article class="hover-card group relative mx-auto flex max-w-2xl flex-col rounded-2xl border border-zinc-200 p-5 transition-all duration-300 hover:bg-zinc-50/80 hover:shadow-md sm:mx-0 sm:p-8 dark:border-zinc-800 dark:hover:bg-zinc-900/50"> | ||||
|               <div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 opacity-0 transition-opacity duration-500 group-hover:opacity-100 dark:from-zinc-900/0 dark:to-zinc-800/0" /> | ||||
| 
 | ||||
|               <div class="flex flex-col gap-5 sm:flex-row sm:gap-6"> | ||||
|                 {post.image && ( | ||||
|                   <div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl sm:mx-0 sm:w-56"> | ||||
|                   <div class="mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl shadow-xs transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56"> | ||||
|                     <img | ||||
|                       src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`} | ||||
|                       alt={post.image_alt} | ||||
|                       class="h-full w-full object-cover" | ||||
|                       class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" | ||||
|                       loading="lazy" | ||||
|                     /> | ||||
|                   </div> | ||||
|                 )} | ||||
| 
 | ||||
|                 <div class="z-10 flex-1"> | ||||
|                   <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100"> | ||||
|                 <div class="flex-1"> | ||||
|                   <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> | ||||
|                     {post.published_date && ( | ||||
|                       <time | ||||
|                         datetime={post.published_date.toLocaleString()} | ||||
|                         class="flex items-center gap-1.5" | ||||
|                       > | ||||
|                         <svg | ||||
|                           xmlns="http://www.w3.org/2000/svg" | ||||
|                           fill="none" | ||||
|                           viewBox="0 0 24 24" | ||||
|                           stroke-width="1.5" | ||||
|                           stroke="currentColor" | ||||
|                           class="h-3.5 w-3.5 sm:h-4 sm:w-4" | ||||
|                         > | ||||
|                           <path | ||||
|                             stroke-linecap="round" | ||||
|                             stroke-linejoin="round" | ||||
|                             d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0 | ||||
|                         A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" | ||||
|                           /> | ||||
|                         </svg> | ||||
|                         <FormattedDate date={post.published_date} /> | ||||
|                       </time> | ||||
|                     )} | ||||
|                   </div> | ||||
| 
 | ||||
|                   <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                     <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> | ||||
|                       {post.title} | ||||
|                     </a> | ||||
| @@ -177,19 +210,15 @@ const relatedTags = [ | ||||
|                   <p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"> | ||||
|                     {post.description} | ||||
|                   </p> | ||||
| 
 | ||||
|                   <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"> | ||||
|                     <FormattedDate date={post.published_date} /> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
| 
 | ||||
|               <div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800"> | ||||
|               <div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800"> | ||||
|                 {post.tags && post.tags.length > 0 && ( | ||||
|                   <div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start"> | ||||
|                     {post.tags.slice(0, 3).map((postTag) => ( | ||||
|                       <a | ||||
|                         href={`/blog/${postTag}`} | ||||
|                         href={`/topics/${postTag}`} | ||||
|                         class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${ | ||||
|                           postTag === tag | ||||
|                             ? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100' | ||||
| @@ -209,24 +238,31 @@ const relatedTags = [ | ||||
| 
 | ||||
|                 <div class="mx-auto sm:mr-0 sm:ml-auto"> | ||||
|                   <a | ||||
|                     href={`/blog/${post.slug}`} | ||||
|                     class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100" | ||||
|                     href={`/blog/${post.slug}/`} | ||||
|                     class="inline-flex items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100" | ||||
|                     aria-hidden="true" | ||||
|                     tabindex="-1" | ||||
|                   > | ||||
|                     <span class="relative inline-block overflow-hidden"> | ||||
|                       <span class="relative z-10">Read article</span> | ||||
|                       <span class="bg-turquoise absolute bottom-0 left-0 h-0.5 w-0 transition-all duration-300 group-hover:w-full" /> | ||||
|                       <span class="block transition-transform duration-300 group-hover:-translate-y-full"> | ||||
|                         Read article | ||||
|                       </span> | ||||
|                       <span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0"> | ||||
|                         Explore now | ||||
|                       </span> | ||||
|                     </span> | ||||
|                     <svg | ||||
|                       viewBox="0 0 16 16" | ||||
|                       xmlns="http://www.w3.org/2000/svg" | ||||
|                       fill="none" | ||||
|                       aria-hidden="true" | ||||
|                       class="ml-1 h-4 w-4 stroke-current transition-transform duration-300" | ||||
|                       viewBox="0 0 24 24" | ||||
|                       stroke-width="1.5" | ||||
|                       stroke="currentColor" | ||||
|                       class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" | ||||
|                     > | ||||
|                       <path | ||||
|                         d="M6.75 5.75 9.25 8l-2.5 2.25" | ||||
|                         stroke-width="1.5" | ||||
|                         stroke-linecap="round" | ||||
|                         stroke-linejoin="round" | ||||
|                         d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" | ||||
|                       /> | ||||
|                     </svg> | ||||
|                   </a> | ||||
| @@ -288,47 +324,34 @@ const relatedTags = [ | ||||
|   </div> | ||||
| </BaseLayout> | ||||
| 
 | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     // Add smooth reveal animations for content after loading | ||||
|     const animateContent = () => { | ||||
|       // Animate hero section | ||||
|       const heroElements = document.querySelectorAll( | ||||
|         '.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p' | ||||
|       ); | ||||
|       heroElements.forEach((el, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             el.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           100 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       // Animate posts | ||||
|       const articles = document.querySelectorAll('article.group'); | ||||
|       articles.forEach((article, index) => { | ||||
|         setTimeout( | ||||
|           () => { | ||||
|             article.classList.add('animate-reveal'); | ||||
|           }, | ||||
|           500 + index * 150 | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     animateContent(); | ||||
|   }); | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
|   /* Grid pattern background */ | ||||
|   .bg-grid-pattern { | ||||
|     background-size: 30px 30px; | ||||
|     background-image: radial-gradient(circle, rgba(0, 0, 0, 0.05) 1px, transparent 1px); | ||||
|   } | ||||
| 
 | ||||
|   :global(.dark) .bg-grid-pattern { | ||||
|     background-image: radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 1px); | ||||
|   } | ||||
| 
 | ||||
|   /* Hide scrollbar but keep functionality */ | ||||
|   .hide-scrollbar { | ||||
|     -ms-overflow-style: none; | ||||
|     scrollbar-width: none; | ||||
|   } | ||||
| 
 | ||||
|   .hide-scrollbar::-webkit-scrollbar { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   /* Animated underline */ | ||||
|   @keyframes expand { | ||||
|     from { | ||||
|       width: 0; | ||||
|     } | ||||
|     to { | ||||
|       width: 100%; | ||||
|       width: 50%; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @@ -336,9 +359,43 @@ const relatedTags = [ | ||||
|     animation: expand 1s ease-out forwards; | ||||
|   } | ||||
| 
 | ||||
|   .animate-reveal { | ||||
|     opacity: 1 !important; | ||||
|     transform: translateY(0) !important; | ||||
|   /* Blob animation */ | ||||
|   .animate-blob { | ||||
|     animation: blob 7s infinite; | ||||
|   } | ||||
| 
 | ||||
|   .animation-delay-2000 { | ||||
|     animation-delay: 2s; | ||||
|   } | ||||
| 
 | ||||
|   @keyframes blob { | ||||
|     0% { | ||||
|       transform: translate(0px, 0px) scale(1); | ||||
|     } | ||||
|     33% { | ||||
|       transform: translate(20px, -20px) scale(1.1); | ||||
|     } | ||||
|     66% { | ||||
|       transform: translate(-20px, 20px) scale(0.9); | ||||
|     } | ||||
|     100% { | ||||
|       transform: translate(0px, 0px) scale(1); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* Hover card effect */ | ||||
|   .hover-card { | ||||
|     transform: translateY(0); | ||||
|     transition: | ||||
|       transform 0.3s ease, | ||||
|       box-shadow 0.3s ease, | ||||
|       background-color 0.3s ease; | ||||
|   } | ||||
| 
 | ||||
|   @media (hover: hover) { | ||||
|     .hover-card:hover { | ||||
|       transform: translateY(-2px); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* Line clamp for descriptions */ | ||||
| @@ -355,4 +412,11 @@ const relatedTags = [ | ||||
|     -webkit-box-orient: vertical; | ||||
|     overflow: hidden; | ||||
|   } | ||||
| 
 | ||||
|   /* Responsive adjustments */ | ||||
|   @media (max-width: 640px) { | ||||
|     .animate-blob { | ||||
|       animation-duration: 10s; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										629
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										629
									
								
								src/pages/topics/index.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,629 @@ | ||||
| --- | ||||
| import BaseLayout from '../../layouts/BaseLayout.astro'; | ||||
|  | ||||
| import directus from '../../../lib/directus'; | ||||
| import { readItems } from '@directus/sdk'; | ||||
|  | ||||
| const posts = await directus.request( | ||||
|   readItems('posts', { | ||||
|     fields: ['*'], | ||||
|     sort: ['-published_date'], | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort(); | ||||
|  | ||||
| // Count posts for each tag and create tag objects with additional data | ||||
| const tagObjects = tags.map((tag) => { | ||||
|   const count = posts.filter((post) => post.tags?.includes(tag)).length; | ||||
|   // Generate a consistent but random-looking hue for each tag | ||||
|   const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360); | ||||
|   return { | ||||
|     name: tag, | ||||
|     count, | ||||
|     size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count | ||||
|     hue, | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); | ||||
| --- | ||||
|  | ||||
| <BaseLayout title="Explore Tags"> | ||||
|   <div | ||||
|     class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16" | ||||
|     transition:animate="slide" | ||||
|   > | ||||
|     <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16"> | ||||
|       <div | ||||
|         class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-16 -bottom-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30" | ||||
|       > | ||||
|       </div> | ||||
|       <div | ||||
|         class="animate-blob animation-delay-4000 theme-transition-bg absolute top-8 right-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl sm:h-32 sm:w-32 md:h-40 md:w-40 dark:bg-zinc-700/20" | ||||
|       > | ||||
|       </div> | ||||
|  | ||||
|       <h1 | ||||
|         class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl dark:text-zinc-100" | ||||
|       > | ||||
|         <span class="relative inline-block"> | ||||
|           <span class="relative inline-block"> | ||||
|             <span | ||||
|               class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-xs dark:from-zinc-800/50 dark:to-zinc-700/50" | ||||
|             ></span> | ||||
|             <span class="relative">Explore</span> | ||||
|           </span> | ||||
|           {' '} | ||||
|           <span class="relative inline-block"> | ||||
|             Topics | ||||
|             <span | ||||
|               class="animate-underline theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-full origin-left transform bg-gradient-to-r from-zinc-400 to-zinc-600 sm:-bottom-2 sm:h-1 dark:from-zinc-600 dark:to-zinc-400" | ||||
|             ></span> | ||||
|           </span> | ||||
|         </span> | ||||
|       </h1> | ||||
|       <p | ||||
|         class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 sm:text-base md:text-lg lg:text-xl dark:text-zinc-400" | ||||
|       > | ||||
|         Discover content organized by your interests | ||||
|       </p> | ||||
|     </div> | ||||
|  | ||||
|     { | ||||
|       tags.length === 0 ? ( | ||||
|         <div class="theme-transition-element py-8 text-center sm:py-12 md:py-16"> | ||||
|           <div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24 dark:bg-zinc-800"> | ||||
|             <svg | ||||
|               xmlns="http://www.w3.org/2000/svg" | ||||
|               fill="none" | ||||
|               viewBox="0 0 24 24" | ||||
|               stroke-width="1.5" | ||||
|               stroke="currentColor" | ||||
|               class="theme-transition-color h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 md:h-12 md:w-12 dark:text-zinc-400" | ||||
|             > | ||||
|               <path | ||||
|                 stroke-linecap="round" | ||||
|                 stroke-linejoin="round" | ||||
|                 d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" | ||||
|               /> | ||||
|               <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" /> | ||||
|             </svg> | ||||
|           </div> | ||||
|           <p class="theme-transition-color text-lg font-medium text-zinc-800 sm:text-xl md:text-2xl dark:text-zinc-200"> | ||||
|             No tags found yet. | ||||
|           </p> | ||||
|           <p class="theme-transition-color mt-2 text-xs text-zinc-500 sm:text-sm md:text-base dark:text-zinc-500"> | ||||
|             Check back later for categorized content. | ||||
|           </p> | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div class="flex w-full justify-center"> | ||||
|           <div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-xs sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8 dark:border-zinc-800 dark:bg-zinc-900/50"> | ||||
|             <div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" /> | ||||
|             <div class="theme-transition-bg absolute -top-8 -right-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" /> | ||||
|             <div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" /> | ||||
|  | ||||
|             <h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl dark:text-zinc-100"> | ||||
|               Popular Topics | ||||
|             </h2> | ||||
|  | ||||
|             <div class="xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 xxxs:gap-2 xxs:gap-2 xs:gap-2 grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 sm:gap-3 md:grid-cols-4 md:gap-4 lg:grid-cols-5"> | ||||
|               {sortedTags.map((tag) => ( | ||||
|                 <a | ||||
|                   href={`/topics/${tag.name}`} | ||||
|                   class="theme-transition-element theme-ripple group relative min-w-0 grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl dark:border-zinc-800 dark:hover:border-zinc-700" | ||||
|                   style={`--tag-hue: ${tag.hue};`} | ||||
|                 > | ||||
|                   <div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" /> | ||||
|  | ||||
|                   <div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4"> | ||||
|                     <div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-xs transition-all duration-300 sm:h-8 sm:w-8 md:h-10 md:w-10 dark:bg-zinc-800 dark:text-zinc-300"> | ||||
|                       <span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg"> | ||||
|                         # | ||||
|                       </span> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="min-w-0 flex-1 overflow-hidden"> | ||||
|                       <h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate text-[10px] font-bold break-words hyphens-auto text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-sm md:text-base dark:text-zinc-100 dark:group-hover:text-zinc-300"> | ||||
|                         {tag.name} | ||||
|                       </h3> | ||||
|                       <p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 sm:text-xs md:text-xs dark:text-zinc-400"> | ||||
|                         {tag.count} article{tag.count !== 1 ? 's' : ''} | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </a> | ||||
|               ))} | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       ) | ||||
|     } | ||||
|   </div> | ||||
| </BaseLayout> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('astro:page-load', () => { | ||||
|     const fixViewportWidth = () => { | ||||
|       // Force the viewport to be exactly the width of the device | ||||
|       const viewport = document.querySelector('meta[name="viewport"]'); | ||||
|       if (!viewport) { | ||||
|         const meta = document.createElement('meta'); | ||||
|         meta.name = 'viewport'; | ||||
|         meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; | ||||
|         document.getElementsByTagName('head')[0].appendChild(meta); | ||||
|       } else { | ||||
|         viewport.setAttribute( | ||||
|           'content', | ||||
|           'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // Fix for horizontal overflow | ||||
|       document.body.style.overflowX = 'hidden'; | ||||
|       document.documentElement.style.overflowX = 'hidden'; | ||||
|       document.documentElement.style.width = '100%'; | ||||
|       document.body.style.width = '100%'; | ||||
|     }; | ||||
|  | ||||
|     fixViewportWidth(); | ||||
|  | ||||
|     // Adjust tag items based on screen size with extreme precision | ||||
|     const adjustTagItems = () => { | ||||
|       const tagItems = document.querySelectorAll('.theme-ripple'); | ||||
|       const width = | ||||
|         window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; | ||||
|       const isVerySmall = width < 360; | ||||
|       const isExtremelySmall = width < 280; | ||||
|       const isMicroScreen = width < 240; | ||||
|  | ||||
|       // Fix container width to match viewport exactly | ||||
|       const container = document.querySelector('.tag-cloud'); | ||||
|       if (container) { | ||||
|         container.style.maxWidth = '100vw'; | ||||
|         container.style.width = '100%'; | ||||
|         container.style.boxSizing = 'border-box'; | ||||
|  | ||||
|         // Remove any margins that might cause overflow | ||||
|         container.style.marginLeft = '0'; | ||||
|         container.style.marginRight = '0'; | ||||
|       } | ||||
|  | ||||
|       // Fix grid width | ||||
|       const grid = document.querySelector('.grid'); | ||||
|       if (grid) { | ||||
|         grid.style.width = '100%'; | ||||
|         grid.style.maxWidth = '100%'; | ||||
|       } | ||||
|  | ||||
|       tagItems.forEach((item) => { | ||||
|         // Set appropriate classes based on screen size | ||||
|         if (isMicroScreen) { | ||||
|           item.classList.add('micro-screen'); | ||||
|           item.classList.remove('extremely-small-screen', 'very-small-screen'); | ||||
|         } else if (isExtremelySmall) { | ||||
|           item.classList.add('extremely-small-screen'); | ||||
|           item.classList.remove('very-small-screen', 'micro-screen'); | ||||
|         } else if (isVerySmall) { | ||||
|           item.classList.add('very-small-screen'); | ||||
|           item.classList.remove('extremely-small-screen', 'micro-screen'); | ||||
|         } else { | ||||
|           item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen'); | ||||
|         } | ||||
|  | ||||
|         // Ensure text doesn't overflow on small screens | ||||
|         const tagName = item.querySelector('h3'); | ||||
|         const tagCount = item.querySelector('p'); | ||||
|  | ||||
|         if (tagName) { | ||||
|           // Set title for accessibility | ||||
|           tagName.title = tagName.textContent.trim(); | ||||
|  | ||||
|           // Adjust text length based on screen size | ||||
|           if (isMicroScreen && tagName.textContent.length > 6) { | ||||
|             tagName.dataset.fullText = tagName.textContent; | ||||
|             tagName.textContent = tagName.textContent.substring(0, 6) + '...'; | ||||
|           } else if (isExtremelySmall && tagName.textContent.length > 8) { | ||||
|             tagName.dataset.fullText = tagName.textContent; | ||||
|             tagName.textContent = tagName.textContent.substring(0, 8) + '...'; | ||||
|           } else if (isVerySmall && tagName.textContent.length > 12) { | ||||
|             tagName.dataset.fullText = tagName.textContent; | ||||
|             tagName.textContent = tagName.textContent.substring(0, 12) + '...'; | ||||
|           } else if (tagName.dataset.fullText) { | ||||
|             tagName.textContent = tagName.dataset.fullText; | ||||
|             delete tagName.dataset.fullText; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Set the tag hue for hover effects | ||||
|         const hue = item.style.getPropertyValue('--tag-hue'); | ||||
|         item.style.setProperty('--hover-hue', hue); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     // Run on load | ||||
|     adjustTagItems(); | ||||
|  | ||||
|     // Run on resize with optimized debounce | ||||
|     let resizeTimer; | ||||
|     const handleResize = () => { | ||||
|       if (resizeTimer) { | ||||
|         window.cancelAnimationFrame(resizeTimer); | ||||
|       } | ||||
|  | ||||
|       resizeTimer = window.requestAnimationFrame(() => { | ||||
|         fixViewportWidth(); | ||||
|         adjustTagItems(); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     window.addEventListener('resize', handleResize); | ||||
|     window.addEventListener('orientationchange', handleResize); | ||||
|  | ||||
|     // Ensure layout is recalculated after page is fully loaded | ||||
|     window.addEventListener('load', () => { | ||||
|       fixViewportWidth(); | ||||
|       adjustTagItems(); | ||||
|       // Force recalculation after images and fonts are loaded | ||||
|       setTimeout(() => { | ||||
|         fixViewportWidth(); | ||||
|         adjustTagItems(); | ||||
|       }, 500); | ||||
|     }); | ||||
|  | ||||
|     // Fix for iOS Safari and other mobile browsers | ||||
|     if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) { | ||||
|       document.documentElement.style.setProperty( | ||||
|         '--safe-area-inset-bottom', | ||||
|         'env(safe-area-inset-bottom)' | ||||
|       ); | ||||
|  | ||||
|       // Fix for mobile viewport height issues | ||||
|       const setVh = () => { | ||||
|         const vh = window.innerHeight * 0.01; | ||||
|         document.documentElement.style.setProperty('--vh', `${vh}px`); | ||||
|       }; | ||||
|  | ||||
|       setVh(); | ||||
|       window.addEventListener('resize', setVh); | ||||
|       window.addEventListener('orientationchange', () => { | ||||
|         // Wait for orientation change to complete | ||||
|         setTimeout(() => { | ||||
|           setVh(); | ||||
|           fixViewportWidth(); | ||||
|         }, 100); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Add touch support for mobile devices | ||||
|     const addTouchSupport = () => { | ||||
|       const tagItems = document.querySelectorAll('.theme-ripple'); | ||||
|  | ||||
|       tagItems.forEach((item) => { | ||||
|         item.addEventListener( | ||||
|           'touchstart', | ||||
|           () => { | ||||
|             item.classList.add('touch-active'); | ||||
|           }, | ||||
|           { passive: true } | ||||
|         ); | ||||
|  | ||||
|         item.addEventListener( | ||||
|           'touchend', | ||||
|           () => { | ||||
|             setTimeout(() => { | ||||
|               item.classList.remove('touch-active'); | ||||
|             }, 150); | ||||
|           }, | ||||
|           { passive: true } | ||||
|         ); | ||||
|  | ||||
|         // Cancel active state if touch moves away | ||||
|         item.addEventListener( | ||||
|           'touchmove', | ||||
|           (e) => { | ||||
|             const touch = e.touches[0]; | ||||
|             const rect = item.getBoundingClientRect(); | ||||
|  | ||||
|             if ( | ||||
|               touch.clientX < rect.left || | ||||
|               touch.clientX > rect.right || | ||||
|               touch.clientY < rect.top || | ||||
|               touch.clientY > rect.bottom | ||||
|             ) { | ||||
|               item.classList.remove('touch-active'); | ||||
|             } | ||||
|           }, | ||||
|           { passive: true } | ||||
|         ); | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     addTouchSupport(); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Base styles */ | ||||
|   .tag-cloud { | ||||
|     box-shadow: | ||||
|       0 0 0 1px rgba(0, 0, 0, 0.03), | ||||
|       0 2px 4px rgba(0, 0, 0, 0.03), | ||||
|       0 4px 8px rgba(0, 0, 0, 0.05); | ||||
|     transform-style: preserve-3d; | ||||
|     perspective: 1000px; | ||||
|     transition: all var(--theme-transition); | ||||
|     width: 100% !important; | ||||
|     max-width: 100% !important; | ||||
|     box-sizing: border-box; | ||||
|     margin-left: 0 !important; | ||||
|     margin-right: 0 !important; | ||||
|   } | ||||
|  | ||||
|   /* Fix for horizontal overflow */ | ||||
|   :global(html), | ||||
|   :global(body) { | ||||
|     overflow-x: hidden; | ||||
|     width: 100%; | ||||
|     max-width: 100%; | ||||
|   } | ||||
|  | ||||
|   :global(.max-w-6xl) { | ||||
|     max-width: 100% !important; | ||||
|     width: 100% !important; | ||||
|   } | ||||
|  | ||||
|   /* Micro screens (below 240px) */ | ||||
|   @media (max-width: 239px) { | ||||
|     .tag-cloud { | ||||
|       padding: 0.5rem !important; | ||||
|       margin: 0 !important; | ||||
|       border-radius: 0.25rem !important; | ||||
|       width: 100% !important; | ||||
|     } | ||||
|  | ||||
|     .tag-cloud h2 { | ||||
|       font-size: 0.875rem !important; | ||||
|       margin-bottom: 0.375rem !important; | ||||
|     } | ||||
|  | ||||
|     .grid { | ||||
|       grid-template-columns: repeat(1, minmax(0, 1fr)) !important; | ||||
|       gap: 0.375rem !important; | ||||
|       width: 100% !important; | ||||
|     } | ||||
|  | ||||
|     .micro-screen .flex { | ||||
|       padding: 0.25rem !important; | ||||
|     } | ||||
|  | ||||
|     .micro-screen h3 { | ||||
|       font-size: 0.625rem !important; | ||||
|     } | ||||
|  | ||||
|     .micro-screen p { | ||||
|       font-size: 0.5rem !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Extra extra extra small screens (240px-279px) */ | ||||
|   @media (min-width: 240px) and (max-width: 279px) { | ||||
|     .xxxs\:grid-cols-2 { | ||||
|       grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||
|     } | ||||
|  | ||||
|     .xxxs\:px-2 { | ||||
|       padding-left: 0.5rem; | ||||
|       padding-right: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:py-2 { | ||||
|       padding-top: 0.5rem; | ||||
|       padding-bottom: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:w-6 { | ||||
|       width: 1.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:h-6 { | ||||
|       height: 1.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:text-xs { | ||||
|       font-size: 0.75rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:gap-2 { | ||||
|       gap: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxxs\:text-\[9px\] { | ||||
|       font-size: 9px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Extra extra small screens (280px-359px) */ | ||||
|   @media (min-width: 280px) { | ||||
|     .xxs\:grid-cols-2 { | ||||
|       grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||
|     } | ||||
|  | ||||
|     .xxs\:px-2 { | ||||
|       padding-left: 0.5rem; | ||||
|       padding-right: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:py-2 { | ||||
|       padding-top: 0.5rem; | ||||
|       padding-bottom: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:w-6 { | ||||
|       width: 1.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:h-6 { | ||||
|       height: 1.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:text-xs { | ||||
|       font-size: 0.75rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:gap-2 { | ||||
|       gap: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xxs\:text-\[9px\] { | ||||
|       font-size: 9px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Extra small screens (360px-639px) */ | ||||
|   @media (min-width: 360px) { | ||||
|     .xs\:grid-cols-3 { | ||||
|       grid-template-columns: repeat(3, minmax(0, 1fr)); | ||||
|     } | ||||
|  | ||||
|     .xs\:px-2 { | ||||
|       padding-left: 0.5rem; | ||||
|       padding-right: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:py-2 { | ||||
|       padding-top: 0.5rem; | ||||
|       padding-bottom: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:w-7 { | ||||
|       width: 1.75rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:h-7 { | ||||
|       height: 1.75rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:text-xs { | ||||
|       font-size: 0.75rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:text-sm { | ||||
|       font-size: 0.875rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:gap-2 { | ||||
|       gap: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .xs\:text-\[10px\] { | ||||
|       font-size: 10px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* Ensure text doesn't overflow on small screens */ | ||||
|   .truncate { | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
|     max-width: 100%; | ||||
|   } | ||||
|  | ||||
|   /* Ensure proper word breaking for long tag names */ | ||||
|   .break-words { | ||||
|     word-break: break-word; | ||||
|     overflow-wrap: break-word; | ||||
|   } | ||||
|  | ||||
|   .hyphens-auto { | ||||
|     hyphens: auto; | ||||
|   } | ||||
|  | ||||
|   /* Shadow for dark mode */ | ||||
|   :global(.dark) .tag-cloud { | ||||
|     box-shadow: | ||||
|       0 0 0 1px rgba(255, 255, 255, 0.05), | ||||
|       0 2px 4px rgba(0, 0, 0, 0.1), | ||||
|       0 4px 8px rgba(0, 0, 0, 0.15); | ||||
|   } | ||||
|  | ||||
|   /* Prevent layout shifts */ | ||||
|   .grow { | ||||
|     grow: 1; | ||||
|   } | ||||
|  | ||||
|   .min-w-0 { | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   /* Ensure container doesn't overflow */ | ||||
|   .overflow-hidden { | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   /* Touch support for mobile */ | ||||
|   .touch-active { | ||||
|     transform: scale(0.97) !important; | ||||
|     opacity: 0.9; | ||||
|     transition: | ||||
|       transform 0.15s ease-in-out, | ||||
|       opacity 0.15s ease-in-out !important; | ||||
|   } | ||||
|  | ||||
|   /* Animation for blob */ | ||||
|   @keyframes blob { | ||||
|     0%, | ||||
|     100% { | ||||
|       transform: translate(0, 0) scale(1); | ||||
|     } | ||||
|     25% { | ||||
|       transform: translate(10px, -10px) scale(1.05); | ||||
|     } | ||||
|     50% { | ||||
|       transform: translate(0, 20px) scale(0.95); | ||||
|     } | ||||
|     75% { | ||||
|       transform: translate(-10px, -10px) scale(1.05); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .animate-blob { | ||||
|     animation: blob 20s infinite ease-in-out; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-2000 { | ||||
|     animation-delay: 2s; | ||||
|   } | ||||
|  | ||||
|   .animation-delay-4000 { | ||||
|     animation-delay: 4s; | ||||
|   } | ||||
|  | ||||
|   /* Animation for underline */ | ||||
|   @keyframes underline { | ||||
|     0% { | ||||
|       transform: scaleX(0); | ||||
|     } | ||||
|     100% { | ||||
|       transform: scaleX(1); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .animate-underline { | ||||
|     animation: underline 1.5s ease-out forwards; | ||||
|   } | ||||
|  | ||||
|   /* Fix for iOS Safari notch */ | ||||
|   @supports (padding: max(0px)) { | ||||
|     .tag-cloud { | ||||
|       padding-left: max(0.75rem, env(safe-area-inset-left)); | ||||
|       padding-right: max(0.75rem, env(safe-area-inset-right)); | ||||
|       padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
| @@ -4,15 +4,6 @@ | ||||
| /* https://tailwindcss.com/docs/dark-mode */ | ||||
| @custom-variant dark (&:where(.dark, .dark *)); | ||||
|  | ||||
| /* Add custom color palette */ | ||||
| @theme { | ||||
|   --color-midnight: #0c354d; | ||||
|   --color-turquoise: #0da797; | ||||
|   --color-bermuda: #7fbab4; | ||||
|   --color-desert: #f9deb2; | ||||
|   --color-bronze: #9e7f5e; | ||||
| } | ||||
|  | ||||
| @layer base { | ||||
|   :root { | ||||
|     font-family: 'Inter', sans-serif; | ||||
| @@ -141,22 +132,7 @@ button { | ||||
|   transition: all 0.5s ease; | ||||
| } | ||||
|  | ||||
| /* Content reveal animations */ | ||||
| .hero-text h1, | ||||
| .hero-text span, | ||||
| .hero-text p, | ||||
| .hero-text + p, | ||||
| .hero-text ~ div, | ||||
| article.group, | ||||
| a.group.flex.flex-col { | ||||
|   opacity: 0; | ||||
|   transform: translateY(20px); | ||||
|   transition: | ||||
|     opacity 0.8s ease, | ||||
|     transform 0.8s ease; | ||||
| } | ||||
|  | ||||
| .animate-reveal { | ||||
|   opacity: 1 !important; | ||||
|   transform: translateY(0) !important; | ||||
| a.hover:hover, | ||||
| button:hover { | ||||
|   transform: translateY(-2px); | ||||
| } | ||||
|   | ||||
| @@ -23,7 +23,19 @@ const iconSets = { | ||||
|   si: SiIcons, | ||||
| }; | ||||
|  | ||||
| const DynamicIcon = ({ name, set = 'fa' }: { name: string; set?: string }) => { | ||||
| 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')) { | ||||
| @@ -34,7 +46,7 @@ const DynamicIcon = ({ name, set = 'fa' }: { name: string; set?: string }) => { | ||||
|     IconComponent = iconSets[set][name]; | ||||
|   } | ||||
|  | ||||
|   return <IconComponent />; | ||||
|   return <IconComponent size={size} color={color} className={className} />; | ||||
| }; | ||||
|  | ||||
| export default DynamicIcon; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user