apply prettier formatting
Some checks failed
renovate / renovate (push) Has been cancelled

This commit is contained in:
2025-06-08 16:45:36 -05:00
parent 67f12ecf72
commit 51041f6ae9
32 changed files with 3303 additions and 2509 deletions

View File

@@ -2,7 +2,7 @@ name: renovate
on: on:
schedule: schedule:
- cron: "@daily" - cron: '@daily'
push: push:
branches: branches:

View File

@@ -15,4 +15,3 @@
} }
] ]
} }

View File

@@ -11,8 +11,5 @@ const getSiteURL = () => {
export default defineConfig({ export default defineConfig({
site: getSiteURL(), site: getSiteURL(),
integrations: [ integrations: [tailwind(), react()],
tailwind(),
react(),
],
}); });

View File

@@ -1,4 +1,4 @@
import { createDirectus, rest, } from '@directus/sdk'; import { createDirectus, rest } from '@directus/sdk';
type Global = { type Global = {
title: string; title: string;
@@ -10,26 +10,26 @@ type Global = {
portrait: string; portrait: string;
portrait_alt: string; portrait_alt: string;
about: string; about: string;
} };
type About = { type About = {
background: string; background: string;
experience: string; experience: string;
education: string; education: string;
certifications: string; certifications: string;
} };
type Links = { type Links = {
github: string; github: string;
linkedin: string; linkedin: string;
} };
type Skill = { type Skill = {
title: string; title: string;
description: string; description: string;
icon: string; icon: string;
level: string; level: string;
} };
export type Post = { export type Post = {
slug: string; slug: string;
@@ -41,7 +41,7 @@ export type Post = {
published_date: Date; published_date: Date;
updated_date: Date; updated_date: Date;
tags: string[]; tags: string[];
} };
type Schema = { type Schema = {
global: Global; global: Global;
@@ -49,8 +49,10 @@ type Schema = {
links: Links; links: Links;
skills: Skill[]; skills: Skill[];
posts: Post[]; posts: Post[];
} };
const directus = createDirectus<Schema>(process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev").with(rest()); const directus = createDirectus<Schema>(
process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'
).with(rest());
export default directus; export default directus;

View File

@@ -7,6 +7,7 @@
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"format": "prettier . --write",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
@@ -30,7 +31,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-astro": "^0.12.3", "prettier-plugin-astro": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.14" "prettier-plugin-tailwindcss": "^0.6.0"
} }
} }

36
pnpm-lock.yaml generated
View File

@@ -64,11 +64,11 @@ importers:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
prettier-plugin-astro: prettier-plugin-astro:
specifier: ^0.12.3 specifier: ^0.14.0
version: 0.12.3 version: 0.14.1
prettier-plugin-tailwindcss: prettier-plugin-tailwindcss:
specifier: ^0.5.14 specifier: ^0.6.0
version: 0.5.14(prettier-plugin-astro@0.12.3)(prettier@3.5.3) version: 0.6.12(prettier-plugin-astro@0.14.1)(prettier@3.5.3)
packages: packages:
@@ -80,9 +80,6 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@astrojs/compiler@1.8.2':
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
'@astrojs/compiler@2.12.1': '@astrojs/compiler@2.12.1':
resolution: {integrity: sha512-WDSyVIiz7sNcJcCJxJFITu6XjfGhJ50Z0auyaWsrM+xb07IlhBLFtQuDkNy0caVHWNcKTM2LISAaHhgkRqGAVg==} resolution: {integrity: sha512-WDSyVIiz7sNcJcCJxJFITu6XjfGhJ50Z0auyaWsrM+xb07IlhBLFtQuDkNy0caVHWNcKTM2LISAaHhgkRqGAVg==}
@@ -1792,25 +1789,26 @@ packages:
resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prettier-plugin-astro@0.12.3: prettier-plugin-astro@0.14.1:
resolution: {integrity: sha512-GthUSu3zCvmtVyqlArosez0xE08vSJ0R1sWurxIWpABaCkNGYFANoUdFkqmIo54EV2uPLGcVJzOucWvCjPBWvg==} resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==}
engines: {node: ^14.15.0 || >=16.0.0} engines: {node: ^14.15.0 || >=16.0.0}
prettier-plugin-tailwindcss@0.5.14: prettier-plugin-tailwindcss@0.6.12:
resolution: {integrity: sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==} resolution: {integrity: sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
peerDependencies: peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*' '@ianvs/prettier-plugin-sort-imports': '*'
'@prettier/plugin-pug': '*' '@prettier/plugin-pug': '*'
'@shopify/prettier-plugin-liquid': '*' '@shopify/prettier-plugin-liquid': '*'
'@trivago/prettier-plugin-sort-imports': '*' '@trivago/prettier-plugin-sort-imports': '*'
'@zackad/prettier-plugin-twig-melody': '*' '@zackad/prettier-plugin-twig': '*'
prettier: ^3.0 prettier: ^3.0
prettier-plugin-astro: '*' prettier-plugin-astro: '*'
prettier-plugin-css-order: '*' prettier-plugin-css-order: '*'
prettier-plugin-import-sort: '*' prettier-plugin-import-sort: '*'
prettier-plugin-jsdoc: '*' prettier-plugin-jsdoc: '*'
prettier-plugin-marko: '*' prettier-plugin-marko: '*'
prettier-plugin-multiline-arrays: '*'
prettier-plugin-organize-attributes: '*' prettier-plugin-organize-attributes: '*'
prettier-plugin-organize-imports: '*' prettier-plugin-organize-imports: '*'
prettier-plugin-sort-imports: '*' prettier-plugin-sort-imports: '*'
@@ -1825,7 +1823,7 @@ packages:
optional: true optional: true
'@trivago/prettier-plugin-sort-imports': '@trivago/prettier-plugin-sort-imports':
optional: true optional: true
'@zackad/prettier-plugin-twig-melody': '@zackad/prettier-plugin-twig':
optional: true optional: true
prettier-plugin-astro: prettier-plugin-astro:
optional: true optional: true
@@ -1837,6 +1835,8 @@ packages:
optional: true optional: true
prettier-plugin-marko: prettier-plugin-marko:
optional: true optional: true
prettier-plugin-multiline-arrays:
optional: true
prettier-plugin-organize-attributes: prettier-plugin-organize-attributes:
optional: true optional: true
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
@@ -2466,8 +2466,6 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@astrojs/compiler@1.8.2': {}
'@astrojs/compiler@2.12.1': {} '@astrojs/compiler@2.12.1': {}
'@astrojs/internal-helpers@0.6.1': {} '@astrojs/internal-helpers@0.6.1': {}
@@ -4585,17 +4583,17 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prettier-plugin-astro@0.12.3: prettier-plugin-astro@0.14.1:
dependencies: dependencies:
'@astrojs/compiler': 1.8.2 '@astrojs/compiler': 2.12.1
prettier: 3.5.3 prettier: 3.5.3
sass-formatter: 0.7.9 sass-formatter: 0.7.9
prettier-plugin-tailwindcss@0.5.14(prettier-plugin-astro@0.12.3)(prettier@3.5.3): prettier-plugin-tailwindcss@0.6.12(prettier-plugin-astro@0.14.1)(prettier@3.5.3):
dependencies: dependencies:
prettier: 3.5.3 prettier: 3.5.3
optionalDependencies: optionalDependencies:
prettier-plugin-astro: 0.12.3 prettier-plugin-astro: 0.14.1
prettier@3.5.3: {} prettier@3.5.3: {}

View File

@@ -1,12 +1,8 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended", "mergeConfidence:all-badges", ":rebaseStalePrs"],
"config:recommended",
"mergeConfidence:all-badges",
":rebaseStalePrs"
],
"timezone": "US/Central", "timezone": "US/Central",
"schedule": [ "* */1 * * *" ], "schedule": ["* */1 * * *"],
"labels": [], "labels": [],
"prHourlyLimit": 0, "prHourlyLimit": 0,
"prConcurrentLimit": 0, "prConcurrentLimit": 0,

View File

@@ -2,16 +2,29 @@
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions // Background.astro - Dot pattern and ambient glow background with smooth theme transitions
--- ---
<div class="fixed inset-0 -z-10 overflow-hidden theme-transition-all"> <div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
<!-- Dot pattern background --> <!-- Dot pattern background -->
<div class="absolute inset-0 bg-grid-pattern bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)] theme-transition-bg"></div> <div
class="bg-grid-pattern theme-transition-bg absolute inset-0 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]"
>
</div>
<!-- Ambient glow effects --> <!-- Ambient glow effects -->
<div class="absolute left-1/4 top-1/4 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-zinc-400/20 dark:bg-zinc-500/20 rounded-full blur-3xl opacity-50 animate-glow theme-transition-bg"></div> <div
<div class="absolute right-1/4 bottom-1/3 translate-x-1/2 translate-y-1/2 w-64 h-64 bg-zinc-300/20 dark:bg-zinc-600/20 rounded-full blur-3xl opacity-40 animate-glow animation-delay-1000 theme-transition-bg"></div> class="animate-glow theme-transition-bg absolute left-1/4 top-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
>
</div>
<div
class="animate-glow animation-delay-1000 theme-transition-bg absolute bottom-1/3 right-1/4 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
>
</div>
<!-- Theme transition overlay --> <!-- Theme transition overlay -->
<div id="theme-transition-overlay" class="absolute inset-0 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none"></div> <div
id="theme-transition-overlay"
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
>
</div>
</div> </div>
<script> <script>
@@ -59,7 +72,9 @@
/* Ambient glow animations */ /* Ambient glow animations */
.animate-glow { .animate-glow {
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transition: background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1); transition:
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
} }
.animation-delay-1000 { .animation-delay-1000 {
@@ -67,7 +82,8 @@
} }
@keyframes glow { @keyframes glow {
0%, 100% { 0%,
100% {
opacity: 0.4; opacity: 0.4;
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }

View File

@@ -1,9 +1,9 @@
--- ---
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton("links")); const links = await directus.request(readSingleton('links'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@@ -18,105 +18,157 @@ const socialLinks = [
{ {
name: 'GitHub', name: 'GitHub',
href: links.github, href: links.github,
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>` icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
}, },
{ {
name: 'LinkedIn', name: 'LinkedIn',
href: links.linkedin, href: links.linkedin,
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>` icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
} },
]; ];
--- ---
<footer class="relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> <footer
<div class="absolute inset-0 pointer-events-none overflow-hidden"> class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
<div class="absolute -top-40 -right-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow"></div> >
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow animation-delay-2000"></div> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute top-20 left-1/4 w-40 h-40 bg-zinc-200/50 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-30 theme-transition-all animate-float-slow animation-delay-1000"></div> <div
class="theme-transition-all animate-float-slow absolute -right-40 -top-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute left-1/4 top-20 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
>
</div>
</div> </div>
<div class="relative pt-16 pb-12 px-4 sm:px-6"> <div class="relative px-4 pb-12 pt-16 sm:px-6">
<div class="max-w-4xl mx-auto"> <div class="mx-auto max-w-4xl">
<!-- Main footer content --> <!-- Main footer content -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-10"> <div class="grid grid-cols-1 gap-10 md:grid-cols-12">
<!-- Brand section --> <!-- Brand section -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<a href="/" class="inline-block group"> <a href="/" class="group inline-block">
<div class="flex items-center"> <div class="flex items-center">
<div class="relative w-10 h-10 rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 dark:from-zinc-200 dark:to-zinc-400 flex items-center justify-center overflow-hidden shadow-lg transform transition-transform group-hover:scale-105"> <div
<span class="text-white dark:text-zinc-900 text-xl font-bold theme-transition-all group-hover:scale-110 transition-transform duration-300">{global.initals}</span> 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"
<div class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> >
<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="ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100 theme-transition-color">Blog</span> </div>
<span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
>Blog</span
>
</div> </div>
</a> </a>
<p class="mt-4 text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color leading-relaxed"> <p
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{global.description} {global.description}
</p> </p>
<!-- Social links --> <!-- Social links -->
<div class="mt-6 flex items-center space-x-4"> <div class="mt-6 flex items-center space-x-4">
{socialLinks.map(social => ( {
socialLinks.map((social) => (
<a <a
href={social.href} href={social.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="group relative flex items-center justify-center w-10 h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-all duration-300 hover:ring-2 hover:ring-zinc-300 dark:hover:ring-zinc-700 transform hover:-translate-y-1" class="group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
aria-label={social.name} aria-label={social.name}
> >
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 dark:from-zinc-700 dark:to-zinc-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span> <span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" />
<svg class="w-5 h-5 relative z-10 transition-transform duration-300 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg
class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<Fragment set:html={social.icon} /> <Fragment set:html={social.icon} />
</svg> </svg>
</a> </a>
))} ))
}
</div> </div>
</div> </div>
<!-- Quick links --> <!-- Quick links -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 uppercase tracking-wider theme-transition-color relative inline-block after:content-[''] after:absolute after:w-8 after:h-0.5 after:bg-zinc-300 dark:after:bg-zinc-700 after:bottom-0 after:left-0 pb-2">Navigation</h3> <h3
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 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>
<ul class="mt-4 space-y-3"> <ul class="mt-4 space-y-3">
{navLinks.map(link => ( {
navLinks.map((link) => (
<li> <li>
<a <a
href={link.href} href={link.href}
class="group flex items-center text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors" class="group flex items-center text-base text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
> >
<span class="relative overflow-hidden inline-block"> <span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.text}</span> <span class="relative z-10">{link.text}</span>
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full"></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> </span>
</a> </a>
</li> </li>
))} ))
}
</ul> </ul>
</div> </div>
<!-- Bottom section --> <!-- Bottom section -->
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 theme-transition-all"> <div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col md:flex-row items-center justify-between gap-4"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<p class="text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color"> <p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
&copy; {currentYear} All rights reserved. &copy; {currentYear} All rights reserved.
</p> </p>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color">Built with</span> <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
>Built with</span
>
<a <a
href="https://astro.build" href="https://astro.build"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="group inline-flex items-center text-xs text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors" class="group inline-flex items-center text-xs text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
> >
<svg class="h-4 w-4 mr-1 text-[#FF5D01] group-hover:animate-pulse" viewBox="0 0 36 36" fill="none"> <svg
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z" fill="currentColor"/> class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z" fill="currentColor"/> viewBox="0 0 36 36"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg> </svg>
<span class="relative"> <span class="relative">
Astro Astro
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"></span> <span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
></span>
</span> </span>
</a> </a>
</div> </div>
@@ -124,9 +176,9 @@ const socialLinks = [
</div> </div>
</div> </div>
</div> </div>
</footer> </div>
<style> <style>
.theme-transition-all { .theme-transition-all {
transition-property: background-color, border-color, color, fill, stroke; transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -146,7 +198,8 @@ const socialLinks = [
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
@@ -157,7 +210,8 @@ const socialLinks = [
} }
@keyframes float-slow { @keyframes float-slow {
0%, 100% { 0%,
100% {
transform: translateY(0) translateX(0); transform: translateY(0) translateX(0);
} }
25% { 25% {
@@ -186,5 +240,5 @@ const socialLinks = [
.animation-delay-2000 { .animation-delay-2000 {
animation-delay: 2s; animation-delay: 2s;
} }
</style>
</style> </footer>

View File

@@ -8,7 +8,8 @@ const { date } = Astro.props;
const parsedDate = typeof date === 'string' ? new Date(date) : date; const parsedDate = typeof date === 'string' ? new Date(date) : date;
--- ---
{parsedDate && ( {
parsedDate && (
<time datetime={parsedDate.toISOString()}> <time datetime={parsedDate.toISOString()}>
{parsedDate.toLocaleDateString('en-us', { {parsedDate.toLocaleDateString('en-us', {
year: 'numeric', year: 'numeric',
@@ -16,4 +17,5 @@ const parsedDate = typeof date === 'string' ? new Date(date) : date;
day: 'numeric', day: 'numeric',
})} })}
</time> </time>
)} )
}

View File

@@ -1,10 +1,10 @@
--- ---
import ThemeToggle from './ThemeToggle.astro'; import ThemeToggle from './ThemeToggle.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const navItems = [ const navItems = [
{ text: 'Home', href: '/' }, { text: 'Home', href: '/' },
@@ -15,68 +15,102 @@ const navItems = [
]; ];
const pathname = new URL(Astro.request.url).pathname; const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1); // remove the first "/" const currentPath = pathname.slice(1);
--- ---
<header class="py-4 fixed top-0 left-0 right-0 z-40 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-800"> <header
<div class="max-w-3xl mx-auto px-4 flex items-center justify-between"> class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
>
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
<!-- Logo --> <!-- Logo -->
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">{global.initals}</a> <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav class="hidden sm:flex items-center space-x-6"> <nav class="hidden items-center space-x-6 sm:flex">
{navItems.map(item => { {
navItems.map((item) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1)); const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return ( return (
<a <a
href={item.href} href={item.href}
class={`text-sm font-medium ${isActive class={`text-sm font-medium ${
isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`} : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
}`}
> >
{item.text} {item.text}
</a> </a>
) );
})} })
}
<ThemeToggle /> <ThemeToggle />
</nav> </nav>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<button id="mobile-menu-button" class="sm:hidden flex items-center" aria-label="Menu"> <button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-zinc-900 dark:text-white"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-900 dark:text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
</svg> </svg>
</button> </button>
</div> </div>
</header> </header>
<!-- Mobile menu overlay --> <!-- Mobile menu overlay -->
<div id="mobile-menu" class="fixed inset-0 z-50 bg-white dark:bg-zinc-900 flex flex-col opacity-0 pointer-events-none transition-all duration-300 ease-in-out"> <div
<div class="flex justify-between items-center p-4 border-b border-zinc-100 dark:border-zinc-800"> id="mobile-menu"
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">JD</a> class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
<button id="close-menu-button" class="text-zinc-900 dark:text-white p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" aria-label="Close menu"> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a>
<button
id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>
</button> </button>
</div> </div>
<nav class="flex-1 flex flex-col items-center justify-center space-y-6 text-center"> <nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
{navItems.map((item, index) => { {
navItems.map((item, index) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1)); const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return ( return (
<a <a
href={item.href} href={item.href}
class={`text-lg font-medium mobile-nav-item opacity-0 translate-y-4 ${isActive class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`} : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
}`}
style={`transition-delay: ${index * 0.05}s;`} style={`transition-delay: ${index * 0.05}s;`}
> >
{item.text} {item.text}
</a> </a>
) );
})} })
<div class="pt-4 mobile-nav-item opacity-0 translate-y-4" style="transition-delay: 0.25s;"> }
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
<ThemeToggle /> <ThemeToggle />
</div> </div>
</nav> </nav>
@@ -109,7 +143,7 @@ const currentPath = pathname.slice(1); // remove the first "/"
mobileMenu.style.opacity = '1'; mobileMenu.style.opacity = '1';
// Animate each nav item with staggered delay // Animate each nav item with staggered delay
navItems.forEach(item => { navItems.forEach((item) => {
setTimeout(() => { setTimeout(() => {
item.classList.remove('opacity-0', 'translate-y-4'); item.classList.remove('opacity-0', 'translate-y-4');
}, 150); }, 150);
@@ -122,7 +156,7 @@ const currentPath = pathname.slice(1); // remove the first "/"
if (!mobileMenu) return; if (!mobileMenu) return;
// Fade out nav items first // Fade out nav items first
navItems.forEach(item => { navItems.forEach((item) => {
item.classList.add('opacity-0', 'translate-y-4'); item.classList.add('opacity-0', 'translate-y-4');
}); });
@@ -144,7 +178,7 @@ const currentPath = pathname.slice(1); // remove the first "/"
// Close menu when clicking a link // Close menu when clicking a link
const mobileLinks = mobileMenu?.querySelectorAll('a'); const mobileLinks = mobileMenu?.querySelectorAll('a');
mobileLinks?.forEach(link => { mobileLinks?.forEach((link) => {
link.addEventListener('click', closeMenu); link.addEventListener('click', closeMenu);
}); });
@@ -180,12 +214,18 @@ const currentPath = pathname.slice(1); // remove the first "/"
<style> <style>
/* Smooth animations for mobile navigation */ /* Smooth animations for mobile navigation */
.mobile-nav-item { .mobile-nav-item {
transition: opacity 0.5s ease, transform 0.5s ease, color 0.3s ease; transition:
opacity 0.5s ease,
transform 0.5s ease,
color 0.3s ease;
} }
/* Header transition */ /* Header transition */
header { header {
transition: box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease; transition:
box-shadow 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
} }
/* Mobile menu button hover effect */ /* Mobile menu button hover effect */

View File

@@ -17,37 +17,85 @@ const encodedUrl = encodeURIComponent(url);
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`} href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Twitter" aria-label="Share on Twitter"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
></path></svg
>
</a> </a>
<a <a
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Facebook" aria-label="Share on Facebook"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
>
</a> </a>
<a <a
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on LinkedIn" aria-label="Share on LinkedIn"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
></circle></svg
>
</a> </a>
<button <button
id="copy-link-button" id="copy-link-button"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 relative" class="relative rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Copy link" aria-label="Copy link"
title="Copy link to clipboard" title="Copy link to clipboard"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> <svg
<span id="copy-tooltip" class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-zinc-800 dark:bg-zinc-700 text-white text-xs py-1 px-2 rounded opacity-0 transition-opacity duration-300 whitespace-nowrap"> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
>
<span
id="copy-tooltip"
class="absolute -top-8 left-1/2 -translate-x-1/2 transform whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
>
Copied! Copied!
</span> </span>
</button> </button>
@@ -59,13 +107,15 @@ const encodedUrl = encodeURIComponent(url);
function setupCopyLinkButton() { function setupCopyLinkButton() {
const copyButtons = document.querySelectorAll('#copy-link-button'); const copyButtons = document.querySelectorAll('#copy-link-button');
copyButtons.forEach(button => { copyButtons.forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
// Get the current URL // Get the current URL
const url = window.location.href; const url = window.location.href;
// Copy to clipboard // Copy to clipboard
navigator.clipboard.writeText(url).then(() => { navigator.clipboard
.writeText(url)
.then(() => {
// Show tooltip // Show tooltip
const tooltip = button.querySelector('#copy-tooltip'); const tooltip = button.querySelector('#copy-tooltip');
if (tooltip) { if (tooltip) {
@@ -76,7 +126,8 @@ const encodedUrl = encodeURIComponent(url);
tooltip.classList.remove('opacity-100'); tooltip.classList.remove('opacity-100');
}, 2000); }, 2000);
} }
}).catch(err => { })
.catch((err) => {
console.error('Failed to copy: ', err); console.error('Failed to copy: ', err);
}); });
}); });
@@ -98,7 +149,7 @@ const encodedUrl = encodeURIComponent(url);
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]'); const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
// Make sure external share links don't trigger page transitions // Make sure external share links don't trigger page transitions
shareLinks.forEach(link => { shareLinks.forEach((link) => {
link.setAttribute('data-spa-external', 'true'); link.setAttribute('data-spa-external', 'true');
}); });
} }

View File

@@ -7,9 +7,10 @@ export interface Props {
const { tags = [], class: className = '' } = Astro.props; const { tags = [], class: className = '' } = Astro.props;
--- ---
{tags.length > 0 && ( {
<div class={`flex flex-wrap gap-2 mt-3 ${className}`}> tags.length > 0 && (
{tags.map(tag => ( <div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
{tags.map((tag) => (
<a <a
href={`/tag/${tag}`} 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" 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"
@@ -18,4 +19,5 @@ const { tags = [], class: className = '' } = Astro.props;
</a> </a>
))} ))}
</div> </div>
)} )
}

View File

@@ -1,17 +1,18 @@
--- ---
--- ---
<button <button
id="theme-toggle" id="theme-toggle"
data-theme-toggle data-theme-toggle
class="relative overflow-hidden rounded-full p-1.5 sm:p-2 transition-all duration-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:focus:ring-zinc-700 group touch-manipulation" class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2"
aria-label="Toggle dark mode" aria-label="Toggle dark mode"
> >
<div class="relative z-10 flex h-5 w-5 items-center justify-center"> <div class="relative z-10 flex h-5 w-5 items-center justify-center">
<!-- Sun icon --> <!-- Sun icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-light absolute h-5 w-5 rotate-0 scale-100 transition-all duration-500 dark:-rotate-90 dark:scale-0 text-zinc-800 dark:text-zinc-200" class="icon-light absolute h-5 w-5 rotate-0 scale-100 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -19,14 +20,16 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<circle cx="12" cy="12" r="5"/> <circle cx="12" cy="12" r="5"></circle>
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/> <path
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
></path>
</svg> </svg>
<!-- Moon icon --> <!-- Moon icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 transition-all duration-500 dark:rotate-0 dark:scale-100 text-zinc-800 dark:text-zinc-200" class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -39,7 +42,9 @@
</div> </div>
<!-- Ripple effect --> <!-- Ripple effect -->
<span class="absolute inset-0 h-full w-full bg-zinc-200 dark:bg-zinc-700 opacity-0 transition-opacity duration-300 group-active:opacity-20"></span> <span
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
></span>
</button> </button>
<script> <script>
@@ -70,10 +75,12 @@
} }
// Toggle theme when any theme toggle button is clicked // Toggle theme when any theme toggle button is clicked
themeToggles.forEach(toggle => { themeToggles.forEach((toggle) => {
// Add event listeners for both click and touch events // Add event listeners for both click and touch events
['click', 'touchend'].forEach(eventType => { ['click', 'touchend'].forEach((eventType) => {
toggle.addEventListener(eventType, (e) => { toggle.addEventListener(
eventType,
(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -102,7 +109,8 @@
// Show overlay during transition // Show overlay during transition
if (overlay) { if (overlay) {
overlay.style.backgroundColor = newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)'; overlay.style.backgroundColor =
newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
overlay.style.opacity = '1'; overlay.style.opacity = '1';
} }
@@ -129,9 +137,11 @@
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
// Dispatch a custom event for other components to react to // Dispatch a custom event for other components to react to
document.dispatchEvent(new CustomEvent('themeChanged', { document.dispatchEvent(
detail: { isDark: newTheme === 'dark' } new CustomEvent('themeChanged', {
})); detail: { isDark: newTheme === 'dark' },
})
);
// Force another reflow to ensure all elements update // Force another reflow to ensure all elements update
document.body.offsetHeight; document.body.offsetHeight;
@@ -147,19 +157,29 @@
ripple.remove(); ripple.remove();
}, 300); }, 300);
}, 50); }, 50);
}, { passive: false }); },
{ passive: false }
);
}); });
// Add touch feedback // Add touch feedback
toggle.addEventListener('touchstart', () => { toggle.addEventListener(
'touchstart',
() => {
toggle.classList.add('active-touch'); toggle.classList.add('active-touch');
}, { passive: true }); },
{ passive: true }
);
toggle.addEventListener('touchend', () => { toggle.addEventListener(
'touchend',
() => {
setTimeout(() => { setTimeout(() => {
toggle.classList.remove('active-touch'); toggle.classList.remove('active-touch');
}, 150); }, 150);
}, { passive: true }); },
{ passive: true }
);
}); });
} }
@@ -193,7 +213,9 @@
<style> <style>
/* Smooth transition for the entire page when theme changes */ /* Smooth transition for the entire page when theme changes */
:global(body) { :global(body) {
transition: background-color 0.5s ease, color 0.5s ease; transition:
background-color 0.5s ease,
color 0.5s ease;
} }
/* Theme transition overlay */ /* Theme transition overlay */
@@ -270,11 +292,13 @@
/* Optimize animations for mobile */ /* Optimize animations for mobile */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.icon-light, .icon-dark { .icon-light,
.icon-dark {
transition: all 0.2s ease-out !important; transition: all 0.2s ease-out !important;
} }
#theme-toggle, #theme-toggle:hover { #theme-toggle,
#theme-toggle:hover {
transform: none; transform: none;
transition: none; transition: none;
} }

1
src/env.d.ts vendored
View File

@@ -1,4 +1,3 @@
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
/// <reference types="astro/content" /> /// <reference types="astro/content" />

View File

@@ -1,16 +1,15 @@
--- ---
import Layout from './Layout.astro'; import Layout from './Layout.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
export interface Props { export interface Props {
title: string; title: string;
description?: string; description?: string;
} }
--- ---
<Layout title={global.title} description={global.description}> <Layout title={global.title} description={global.description}>

View File

@@ -1,16 +1,15 @@
--- ---
import Layout from './Layout.astro'; import Layout from './Layout.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
export interface Props { export interface Props {
title: string; title: string;
description?: string; description?: string;
} }
--- ---
<Layout title={global.title} description={global.description}> <Layout title={global.title} description={global.description}>
@@ -26,7 +25,7 @@ export interface Props {
document.documentElement.classList.add('theme-switching'); document.documentElement.classList.add('theme-switching');
const rippleElements = document.querySelectorAll('.theme-ripple'); const rippleElements = document.querySelectorAll('.theme-ripple');
rippleElements.forEach(el => { rippleElements.forEach((el) => {
el.classList.add('ripple-active'); el.classList.add('ripple-active');
setTimeout(() => { setTimeout(() => {
el.classList.remove('ripple-active'); el.classList.remove('ripple-active');
@@ -35,8 +34,8 @@ export interface Props {
const event = new CustomEvent('themeChange', { const event = new CustomEvent('themeChange', {
detail: { detail: {
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light' theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
} },
}); });
document.dispatchEvent(event); document.dispatchEvent(event);
@@ -47,8 +46,7 @@ export interface Props {
} }
const socialLinks = document.querySelectorAll('.social-link'); const socialLinks = document.querySelectorAll('.social-link');
socialLinks.forEach(link => { socialLinks.forEach((link) => {
link.addEventListener('mouseenter', () => { link.addEventListener('mouseenter', () => {
link.classList.add('hover-active'); link.classList.add('hover-active');
}); });

View File

@@ -5,13 +5,15 @@ import ShareButtons from '../components/ShareButtons.astro';
import TagList from '../components/TagList.astro'; import TagList from '../components/TagList.astro';
import './styles/markdown.css'; import './styles/markdown.css';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", { const posts = await directus.request(
readItems('posts', {
fields: ['*'], fields: ['*'],
})); })
);
return posts.map((post) => ({ params: { slug: post.slug }, props: post })); return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
} }
@@ -23,19 +25,20 @@ try {
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL); canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
} catch (error) { } catch (error) {
console.error('Error creating canonical URL:', error); console.error('Error creating canonical URL:', error);
canonicalURL = new URL("https://www.example.com"); canonicalURL = new URL('https://www.example.com');
} }
--- ---
<Layout title={post.title} description={post.description}> <Layout title={post.title} description={post.description}>
<article class="prose dark:prose-invert prose-zinc lg:prose-lg mx-auto max-w-4xl"> <article class="prose prose-zinc mx-auto max-w-4xl dark:prose-invert lg:prose-lg">
<div class="mb-12"> <div class="mb-12">
<h1 class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"> <h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"
>
{post.title} {post.title}
</h1> </h1>
<div class="flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400 mb-6"> <div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
<FormattedDate date={published_date} /> <FormattedDate date={published_date} />
</div> </div>
@@ -43,36 +46,41 @@ try {
</div> </div>
<!-- Hero image --> <!-- Hero image -->
{post.image && ( {
<div class="relative mb-8 sm:mb-12 overflow-hidden rounded-xl shadow-lg"> post.image && (
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
<div class="aspect-[16/9] w-full"> <div class="aspect-[16/9] w-full">
<img <img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
alt={post.image_alt} alt={post.image_alt}
class="w-full h-full object-cover" class="h-full w-full object-cover"
loading="eager" loading="eager"
/> />
</div> </div>
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
</div> </div>
)} )
}
<div class="markdown-content"> <div class="markdown-content">
<slot /> <slot />
</div> </div>
<!-- Add the like button after the content --> <!-- Add the like button after the content -->
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800"> <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col sm:flex-row items-center justify-between gap-6"> <div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<ShareButtons url={canonicalURL.toString()} title={post.title} /> <!-- Convert URL to string --> <ShareButtons url={canonicalURL.toString()} title={post.title} />
<!-- Convert URL to string -->
</div> </div>
</div> </div>
{post.updated_date && ( {
<div class="mt-8 text-sm text-zinc-500 dark:text-zinc-400 italic"> post.updated_date && (
<div class="mt-8 text-sm italic text-zinc-500 dark:text-zinc-400">
Last updated on <FormattedDate date={post.updated_date} /> Last updated on <FormattedDate date={post.updated_date} />
</div> </div>
)} )
}
</article> </article>
<slot name="after-article" /> <slot name="after-article" />
@@ -94,14 +102,17 @@ try {
// Ensure consistent code block styling // Ensure consistent code block styling
function updateCodeBlockStyles() { function updateCodeBlockStyles() {
document.querySelectorAll('pre').forEach(pre => { document.querySelectorAll('pre').forEach((pre) => {
// Force the background color with !important for both light and dark mode // Force the background color with !important for both light and dark mode
pre.setAttribute('style', 'background-color: #1e293b !important'); pre.setAttribute('style', 'background-color: #1e293b !important');
// Also apply to any nested code elements // Also apply to any nested code elements
const codeElements = pre.querySelectorAll('code'); const codeElements = pre.querySelectorAll('code');
codeElements.forEach(code => { codeElements.forEach((code) => {
code.setAttribute('style', 'background-color: transparent !important; color: #e5e7eb !important;'); code.setAttribute(
'style',
'background-color: transparent !important; color: #e5e7eb !important;'
);
}); });
}); });
} }
@@ -138,7 +149,7 @@ try {
// Handle prev/next navigation links // Handle prev/next navigation links
const navLinks = document.querySelectorAll('.blog-nav-link'); const navLinks = document.querySelectorAll('.blog-nav-link');
navLinks.forEach(link => { navLinks.forEach((link) => {
if (!link.hasAttribute('data-spa-handled')) { if (!link.hasAttribute('data-spa-handled')) {
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
@@ -156,19 +167,22 @@ try {
const animateHeadings = () => { const animateHeadings = () => {
const headings = document.querySelectorAll('article h2, article h3'); const headings = document.querySelectorAll('article h2, article h3');
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver(
entries.forEach(entry => { (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add('heading-visible'); entry.target.classList.add('heading-visible');
observer.unobserve(entry.target); observer.unobserve(entry.target);
} }
}); });
}, { },
{
threshold: 0.2, threshold: 0.2,
rootMargin: '0px 0px -100px 0px' rootMargin: '0px 0px -100px 0px',
}); }
);
headings.forEach(heading => { headings.forEach((heading) => {
heading.classList.add('heading-animated'); heading.classList.add('heading-animated');
observer.observe(heading); observer.observe(heading);
}); });
@@ -183,7 +197,7 @@ try {
function enhanceCodeBlocks() { function enhanceCodeBlocks() {
const codeBlocks = document.querySelectorAll('pre code'); const codeBlocks = document.querySelectorAll('pre code');
codeBlocks.forEach(codeBlock => { codeBlocks.forEach((codeBlock) => {
// Skip if already processed // Skip if already processed
if (codeBlock.parentElement.classList.contains('enhanced')) return; if (codeBlock.parentElement.classList.contains('enhanced')) return;
@@ -267,7 +281,8 @@ try {
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)'; languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
languageBadge.style.color = '#e5e7eb'; languageBadge.style.color = '#e5e7eb';
languageBadge.style.borderRadius = '0.25rem'; languageBadge.style.borderRadius = '0.25rem';
languageBadge.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; languageBadge.style.fontFamily =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
languageBadge.style.zIndex = '10'; languageBadge.style.zIndex = '10';
codeBlock.parentElement.appendChild(languageBadge); codeBlock.parentElement.appendChild(languageBadge);
} }
@@ -278,7 +293,7 @@ try {
function enhanceTables() { function enhanceTables() {
const tables = document.querySelectorAll('.markdown-content table'); const tables = document.querySelectorAll('.markdown-content table');
tables.forEach(table => { tables.forEach((table) => {
if (table.classList.contains('enhanced-table')) return; if (table.classList.contains('enhanced-table')) return;
table.classList.add('enhanced-table'); table.classList.add('enhanced-table');
@@ -305,7 +320,7 @@ try {
function enhanceBlockquotes() { function enhanceBlockquotes() {
const blockquotes = document.querySelectorAll('.markdown-content blockquote'); const blockquotes = document.querySelectorAll('.markdown-content blockquote');
blockquotes.forEach(blockquote => { blockquotes.forEach((blockquote) => {
if (blockquote.classList.contains('enhanced-quote')) return; if (blockquote.classList.contains('enhanced-quote')) return;
blockquote.classList.add('enhanced-quote'); blockquote.classList.add('enhanced-quote');
@@ -353,7 +368,9 @@ try {
/* Enhanced hero image styling */ /* Enhanced hero image styling */
article img:first-of-type { article img:first-of-type {
border-radius: 1rem; border-radius: 1rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }

View File

@@ -12,7 +12,7 @@ interface Props {
const { title, description } = Astro.props; const { title, description } = Astro.props;
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -21,20 +21,28 @@ const { title, description } = Astro.props;
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<title>{title}</title> <title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</head> </head>
<body class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 min-h-screen flex flex-col"> <body
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
>
<!-- Page transition overlay - for smooth transitions between pages --> <!-- Page transition overlay - for smooth transitions between pages -->
<div id="page-transition" class="fixed inset-0 z-40 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none transition-opacity duration-300 flex items-center justify-center"> <div
id="page-transition"
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
>
<div class="transition-spinner"></div> <div class="transition-spinner"></div>
</div> </div>
<!-- Background component with dot pattern and ambient glow --> <!-- Background component with dot pattern and ambient glow -->
<Background /> <Background />
<div class="max-w-3xl mx-auto px-4 sm:px-6 w-full flex-grow"> <div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6">
<Navigation /> <Navigation />
<main class="py-12"> <main class="py-12">
<slot /> <slot />
@@ -95,21 +103,24 @@ const { title, description } = Astro.props;
if (newDescription) { if (newDescription) {
const currentDescription = document.querySelector('meta[name="description"]'); const currentDescription = document.querySelector('meta[name="description"]');
if (currentDescription) { if (currentDescription) {
currentDescription.setAttribute('content', newDescription.getAttribute('content') || ''); currentDescription.setAttribute(
'content',
newDescription.getAttribute('content') || ''
);
} }
} }
// Wait a bit for transition effect // Wait a bit for transition effect
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
// Replace the content // Replace the content
if (mainContent && newContent) { if (mainContent && newContent) {
mainContent.innerHTML = newContent.innerHTML; mainContent.innerHTML = newContent.innerHTML;
// Run scripts in the new content // Run scripts in the new content
Array.from(newContent.querySelectorAll('script')).forEach(oldScript => { Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
const newScript = document.createElement('script'); const newScript = document.createElement('script');
Array.from(oldScript.attributes).forEach(attr => { Array.from(oldScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value); newScript.setAttribute(attr.name, attr.value);
}); });
newScript.textContent = oldScript.textContent; newScript.textContent = oldScript.textContent;
@@ -146,16 +157,17 @@ const { title, description } = Astro.props;
} }
// Dispatch custom event for content loaded // Dispatch custom event for content loaded
document.dispatchEvent(new CustomEvent('spa-content-loaded', { document.dispatchEvent(
detail: { url } new CustomEvent('spa-content-loaded', {
})); detail: { url },
})
);
// Scroll to top or to saved position // Scroll to top or to saved position
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Re-attach event listeners to new content // Re-attach event listeners to new content
attachLinkListeners(); attachLinkListeners();
} catch (error) { } catch (error) {
console.error('Error loading content:', error); console.error('Error loading content:', error);
@@ -166,7 +178,7 @@ const { title, description } = Astro.props;
// Function to attach event listeners to all links // Function to attach event listeners to all links
function attachLinkListeners() { function attachLinkListeners() {
document.querySelectorAll('a').forEach(link => { document.querySelectorAll('a').forEach((link) => {
// Skip links that are already handled, anchor links, external links, or have special attributes // Skip links that are already handled, anchor links, external links, or have special attributes
if ( if (
link.hasAttribute('data-spa-handled') || link.hasAttribute('data-spa-handled') ||
@@ -238,7 +250,10 @@ const { title, description } = Astro.props;
function setupThemeHandling() { function setupThemeHandling() {
// Apply theme from localStorage or system preference // Apply theme from localStorage or system preference
const theme = localStorage.getItem('theme'); const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
@@ -285,14 +300,18 @@ const { title, description } = Astro.props;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Content entrance animation */ /* Content entrance animation */
main { main {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
transition: opacity 0.5s ease, transform 0.5s ease; transition:
opacity 0.5s ease,
transform 0.5s ease;
} }
main.content-entering { main.content-entering {

File diff suppressed because it is too large Load Diff

View File

@@ -3,54 +3,100 @@ import Layout from '../layouts/Layout.astro';
--- ---
<Layout title="404 - Page Not Found"> <Layout title="404 - Page Not Found">
<div class="relative flex flex-col items-center justify-center min-h-[80vh] py-20 text-center px-4 overflow-hidden"> <div
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
>
<!-- Animated background elements --> <!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-20 -left-20 w-64 h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob"></div> <div
<div class="absolute top-1/2 right-1/4 w-96 h-96 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div> class="animate-blob absolute -left-20 -top-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
<div class="absolute bottom-20 left-1/3 w-72 h-72 bg-zinc-100 dark:bg-zinc-800/40 rounded-full blur-3xl opacity-40 animate-blob animation-delay-4000"></div> >
</div>
<div
class="animate-blob animation-delay-2000 absolute right-1/4 top-1/2 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> </div>
<!-- Main content with animation --> <!-- Main content with animation -->
<div class="relative z-10 max-w-xl mx-auto"> <div class="relative z-10 mx-auto max-w-xl">
<div class="glitch-wrapper"> <div class="glitch-wrapper">
<h1 class="glitch text-9xl sm:text-[12rem] font-bold text-zinc-900 dark:text-zinc-100 leading-none" data-text="404">404</h1> <h1
class="glitch text-9xl font-bold leading-none text-zinc-900 dark:text-zinc-100 sm:text-[12rem]"
data-text="404"
>
404
</h1>
</div> </div>
<h2 class="mt-6 text-2xl sm:text-3xl font-bold text-zinc-800 dark:text-zinc-200">Page Not Found</h2> <h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl">
Page Not Found
</h2>
<p class="mt-6 text-zinc-600 dark:text-zinc-400 max-w-md mx-auto text-lg"> <p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
The page you're looking for does not exist. The page you're looking for does not exist.
</p> </p>
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4"> <div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a <a
href="/" href="/"
class="group relative inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-xl" 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 bg-gradient-to-r from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-0"></span> <span
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 relative z-10"> 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"
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> ></span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="relative z-10 h-5 w-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
></path>
</svg> </svg>
<span class="font-medium relative z-10">Return Home</span> <span class="relative z-10 font-medium">Return Home</span>
</a> </a>
<button <button
id="back-button" id="back-button"
class="group inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 shadow-sm hover:shadow-md" class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-sm 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" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
</svg> </svg>
<span class="font-medium">Go Back</span> <span class="font-medium">Go Back</span>
</button> </button>
</div> </div>
<!-- Random fun fact --> <!-- Random fun fact -->
<div class="mt-16 p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl shadow-sm max-w-md mx-auto backdrop-blur-sm border border-zinc-100 dark:border-zinc-700/50"> <div
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Did you know?</h3> class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50"
<p class="mt-2 text-zinc-700 dark:text-zinc-300 text-sm" id="fun-fact"> >
The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found. <h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
Did you know?
</h3>
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found.
</p> </p>
</div> </div>
</div> </div>
@@ -68,11 +114,11 @@ import Layout from '../layouts/Layout.astro';
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.", "The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.", "In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.", "Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
"The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.", 'The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.',
"Many companies use creative 404 pages as a way to showcase their brand personality and humor.", 'Many companies use creative 404 pages as a way to showcase their brand personality and humor.',
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.", "The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
"Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.", 'Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.',
"The most common cause of 404 errors is mistyped URLs." 'The most common cause of 404 errors is mistyped URLs.',
]; ];
// Display a random fun fact // Display a random fun fact
@@ -85,11 +131,13 @@ import Layout from '../layouts/Layout.astro';
// Handle SPA transitions for 404 page // Handle SPA transitions for 404 page
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' || link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')) { link.hasAttribute('data-spa-handled')
) {
return; return;
} }
@@ -201,7 +249,9 @@ import Layout from '../layouts/Layout.astro';
.glitch::after { .glitch::after {
left: -2px; left: -2px;
text-shadow: -2px 0 #00fff9, 2px 2px #ff00c1; text-shadow:
-2px 0 #00fff9,
2px 2px #ff00c1;
animation: glitch-anim2 1s infinite linear alternate-reverse; animation: glitch-anim2 1s infinite linear alternate-reverse;
} }

View File

@@ -3,54 +3,73 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa'; import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
import { SiTypescript, SiAstro } from 'react-icons/si'; import { SiTypescript, SiAstro } from 'react-icons/si';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton, readItems } from "@directus/sdk"; import { readSingleton, readItems } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const about = await directus.request(readSingleton("about")); const about = await directus.request(readSingleton('about'));
const skills = await directus.request( const skills = await directus.request(
readItems("skills", { readItems('skills', {
fields: ['*'] fields: ['*'],
}) })
); );
--- ---
<BaseLayout title="About Me" description={global.description}> <BaseLayout title="About Me" description={global.description}>
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 md:py-16 theme-transition-all"> <div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
<!-- Hero Section --> <!-- Hero Section -->
<div class="relative mb-12 sm:mb-16 md:mb-20"> <div class="relative mb-12 sm:mb-16 md:mb-20">
<!-- Decorative elements --> <!-- Decorative elements -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob theme-transition-bg"></div> <div
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div> class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div class="relative grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center"> <div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
<div class="order-2 md:order-1 text-center md:text-left"> <div class="order-2 text-center md:order-1 md:text-left">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color"> <h1
Hello, I'm <span class="text-transparent bg-clip-text bg-gradient-to-r from-zinc-500 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 theme-transition-all">{global.name}</span> class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-4xl md:text-5xl"
>
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> </h1>
<p class="text-lg sm:text-xl text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 leading-relaxed theme-transition-color"> <p
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-xl"
>
{about.background} {about.background}
</p> </p>
<div class="flex flex-wrap gap-4 social-links-container justify-center md:justify-start theme-transition-children"> <div
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
>
<!-- Social links remain the same --> <!-- Social links remain the same -->
</div> </div>
</div> </div>
<div class="order-1 md:order-2 relative"> <div class="relative order-1 md:order-2">
<div class="aspect-square w-full max-w-[280px] sm:max-w-[320px] md:max-w-md mx-auto overflow-hidden rounded-3xl border-4 sm:border-8 border-white dark:border-zinc-800 shadow-xl sm:shadow-2xl theme-transition-all"> <div
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl dark:border-zinc-800 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md"
>
<img <img
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}` src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
alt={global.portrait_alt} alt={global.portrait_alt}
class="w-full h-full object-cover" class="h-full w-full object-cover"
loading="eager" loading="eager"
/> />
</div> </div>
<!-- Decorative elements --> <!-- Decorative elements -->
<div class="absolute -bottom-4 sm:-bottom-6 -right-4 sm:-right-6 w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 bg-zinc-100 dark:bg-zinc-800 rounded-full border-2 sm:border-4 border-white dark:border-zinc-900 shadow-lg flex items-center justify-center theme-transition-all"> <div
class="theme-transition-all absolute -bottom-4 -right-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg dark:border-zinc-900 dark:bg-zinc-800 sm:-bottom-6 sm:-right-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24"
>
<span class="text-2xl sm:text-3xl">👋</span> <span class="text-2xl sm:text-3xl">👋</span>
</div> </div>
</div> </div>
@@ -58,87 +77,131 @@ const skills = await directus.request(
</div> </div>
<!-- About Section --> <!-- About Section -->
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all"> <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<div class="max-w-3xl mx-auto"> <div class="mx-auto max-w-3xl">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 flex items-center justify-center md:justify-start theme-transition-color"> <h2
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 mr-4 theme-transition-bg"></span> class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-3xl md:justify-start"
>
<span
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
></span>
About Me About Me
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 ml-4 theme-transition-bg"></span> <span
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
></span>
</h2> </h2>
<div class="prose prose-zinc dark:prose-invert max-w-none theme-transition-all"> <div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert">
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color"> <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.experience} {about.experience}
</p> </p>
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color"> <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.education} {about.education}
</p> </p>
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color"> <p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.certifications} {about.certifications}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Skills Section --> <!-- Skills Section -->
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all"> <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-8 sm:mb-12 text-center theme-transition-color">Tech Stack</h2> <h2
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-12 sm:text-3xl"
>
Tech Stack
</h2>
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8"> <div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container --> <!-- Main slider container -->
<div class="slider-track flex animate-slide"> <div class="slider-track animate-slide flex">
{ skills.map((skill, index) => ( {
<div key={`${skill.title}-${index}`} class="skill-card min-w-[220px] sm:min-w-[280px] mx-2 sm:mx-4 bg-white dark:bg-zinc-800/50 rounded-xl border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition-all duration-300 hover:shadow-xl transform hover:-translate-y-2 hover:scale-105 theme-transition-element"> skills.map((skill, index) => (
<div class="p-4 sm:p-6">
<div class="flex items-center justify-between mb-4 sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4">
<div class="w-8 h-8 sm:w-12 sm:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 rounded-lg text-zinc-800 dark:text-zinc-200 transform transition-transform group-hover:rotate-12 theme-transition-bg theme-transition-color">
<skill.icon size={20} className="sm:text-2xl transform transition-all hover:scale-125" />
</div>
<h3 class="text-base sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 theme-transition-color">{skill.title}</h3>
</div>
<span class="text-xs sm:text-sm font-mono bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full theme-transition-all">{skill.level}%</span>
</div>
<div class="relative h-1.5 sm:h-2 w-full bg-zinc-100 dark:bg-zinc-700 overflow-hidden rounded-full theme-transition-bg">
<div <div
class="absolute top-0 left-0 h-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200 rounded-full transition-all duration-1000 progress-bar-animate theme-transition-bg" key={`${skill.title}-${index}`}
style={`width: ${skill.level}%`} 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 dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]"
></div> >
<div class="p-4 sm:p-6">
<div class="mb-4 flex items-center justify-between sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4">
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 dark:bg-zinc-800 dark:text-zinc-200 sm:h-12 sm:w-12">
<skill.icon
size={20}
className="sm:text-2xl transform transition-all hover:scale-125"
/>
</div>
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 dark:text-zinc-100 sm:text-xl">
{skill.title}
</h3>
</div>
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 sm:px-2.5 sm:py-1 sm:text-sm">
{skill.level}%
</span>
</div> </div>
<div class="flex justify-between mt-1 sm:mt-2 text-[10px] sm:text-xs text-zinc-400 dark:text-zinc-500 font-mono theme-transition-color"> <div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-700 sm:h-2">
<div
class="progress-bar-animate theme-transition-bg absolute left-0 top-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>
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 dark:text-zinc-500 sm:mt-2 sm:text-xs">
<span>Beginner</span> <span>Beginner</span>
<span>Advanced</span> <span>Advanced</span>
</div> </div>
</div> </div>
</div> </div>
))} ))
}
</div> </div>
<!-- Gradient overlays for smooth fade effect --> <!-- Gradient overlays for smooth fade effect -->
<div class="absolute top-0 bottom-0 left-0 w-12 sm:w-24 bg-gradient-to-r from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div> <div
<div class="absolute top-0 bottom-0 right-0 w-12 sm:w-24 bg-gradient-to-l from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div> class="theme-transition-bg absolute bottom-0 left-0 top-0 z-10 w-12 bg-gradient-to-r from-white to-transparent dark:from-zinc-900 sm:w-24"
>
</div>
<div
class="theme-transition-bg absolute bottom-0 right-0 top-0 z-10 w-12 bg-gradient-to-l from-white to-transparent dark:from-zinc-900 sm:w-24"
>
</div>
</div> </div>
</div> </div>
<!-- Contact Section --> <!-- Contact Section -->
<div class="max-w-3xl mx-auto text-center theme-transition-all"> <div class="theme-transition-all mx-auto max-w-3xl text-center">
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color">Get in Touch</h2> <h2
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 theme-transition-color"> class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl"
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. Get in Touch
</h2>
<p
class="theme-transition-color mb-6 text-base text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-lg"
>
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> </p>
<a <a
href=`mailto:${global.email}` href=`mailto:${global.email}`
class="inline-flex items-center justify-center px-6 sm:px-8 py-3 sm:py-4 rounded-lg bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 transition-colors text-base sm:text-lg font-medium theme-transition-all" class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 sm:px-8 sm:py-4 sm:text-lg"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 sm:h-5 sm:w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<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" /> 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> </svg>
Say Hello Say Hello
</a> </a>
@@ -157,7 +220,8 @@ const skills = await directus.request(
} }
@keyframes blob-bounce { @keyframes blob-bounce {
0%, 100% { 0%,
100% {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
25% { 25% {
@@ -218,7 +282,9 @@ const skills = await directus.request(
/* Reduce animation complexity on mobile for better performance */ /* Reduce animation complexity on mobile for better performance */
@media (max-width: 640px) { @media (max-width: 640px) {
.skill-card { .skill-card {
transition: transform 0.3s ease, box-shadow 0.3s ease; transition:
transform 0.3s ease,
box-shadow 0.3s ease;
} }
.skill-card:hover { .skill-card:hover {
@@ -234,7 +300,11 @@ const skills = await directus.request(
left: -10%; left: -10%;
width: 120%; width: 120%;
height: 120%; height: 120%;
background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%); background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
pointer-events: none; pointer-events: none;
@@ -256,7 +326,7 @@ const skills = await directus.request(
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progress-shine 2s infinite; animation: progress-shine 2s infinite;
} }
@@ -271,7 +341,8 @@ const skills = await directus.request(
/* Improved touch targets for mobile */ /* Improved touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, button { a,
button {
min-height: 44px; min-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -290,7 +361,8 @@ const skills = await directus.request(
/* Smooth card transition during theme switch */ /* Smooth card transition during theme switch */
.skill-card.theme-transition-element { .skill-card.theme-transition-element {
transition: background-color var(--theme-transition), transition:
background-color var(--theme-transition),
border-color var(--theme-transition), border-color var(--theme-transition),
color var(--theme-transition), color var(--theme-transition),
box-shadow var(--theme-transition), box-shadow var(--theme-transition),
@@ -350,7 +422,7 @@ const skills = await directus.request(
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
if (!isTouchDevice) { if (!isTouchDevice) {
cards.forEach(card => { cards.forEach((card) => {
card.addEventListener('mousemove', (e) => { card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect(); const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
@@ -380,7 +452,7 @@ const skills = await directus.request(
}); });
} else { } else {
// Simpler effects for touch devices // Simpler effects for touch devices
cards.forEach(card => { cards.forEach((card) => {
card.addEventListener('touchstart', () => { card.addEventListener('touchstart', () => {
card.classList.add('is-touched'); card.classList.add('is-touched');
}); });
@@ -413,11 +485,13 @@ const skills = await directus.request(
// Handle SPA transitions for about page // Handle SPA transitions for about page
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' || link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')) { link.hasAttribute('data-spa-handled')
) {
return; return;
} }
@@ -455,9 +529,12 @@ const skills = await directus.request(
// Animate hero section elements // Animate hero section elements
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container'); const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
heroElements.forEach((el, index) => { heroElements.forEach((el, index) => {
setTimeout(() => { setTimeout(
() => {
el.classList.add('animate-reveal'); el.classList.add('animate-reveal');
}, 100 + (index * 150)); },
100 + index * 150
);
}); });
// Animate profile image // Animate profile image
@@ -471,17 +548,23 @@ const skills = await directus.request(
// Animate skill bars with staggered delay // Animate skill bars with staggered delay
const skillBars = document.querySelectorAll('.skill-bar'); const skillBars = document.querySelectorAll('.skill-bar');
skillBars.forEach((bar, index) => { skillBars.forEach((bar, index) => {
setTimeout(() => { setTimeout(
() => {
bar.classList.add('animate-skill'); bar.classList.add('animate-skill');
}, 500 + (index * 100)); },
500 + index * 100
);
}); });
// Animate sections with staggered delay // Animate sections with staggered delay
const sections = document.querySelectorAll('section'); const sections = document.querySelectorAll('section');
sections.forEach((section, index) => { sections.forEach((section, index) => {
setTimeout(() => { setTimeout(
() => {
section.classList.add('animate-reveal'); section.classList.add('animate-reveal');
}, 300 + (index * 200)); },
300 + index * 200
);
}); });
} }

View File

@@ -1,13 +1,15 @@
--- ---
import BlogPost from '../../layouts/BlogPost.astro'; import BlogPost from '../../layouts/BlogPost.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", { const posts = await directus.request(
readItems('posts', {
fields: ['*'], fields: ['*'],
})); })
);
const sortedEntries = [...posts].sort( const sortedEntries = [...posts].sort(
(a, b) => b.published_date.valueOf() - a.published_date.valueOf() (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
@@ -19,58 +21,97 @@ export async function getStaticPaths() {
props: { props: {
post, post,
nextPost: index > 0 ? sortedEntries[index - 1] : null, nextPost: index > 0 ? sortedEntries[index - 1] : null,
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
}, },
}; };
}); });
} }
const { post, nextPost, prevPost } = Astro.props; const { post, nextPost, prevPost } = Astro.props;
--- ---
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}> <BlogPost
slug={post.slug}
title={post.title}
description={post.description}
content={post.content}
image={post.image}
image_alt={post.image_alt}
published_date={post.published_date}
updated_date={post.updated_date}
tags={post.tags}
>
<!-- Main Content - Enhanced with better typography and spacing --> <!-- Main Content - Enhanced with better typography and spacing -->
<div class="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:text-zinc-800 dark:prose-a:text-zinc-300 prose-a:font-medium prose-a:underline-offset-4 hover:prose-a:text-zinc-600 dark:hover:prose-a:text-zinc-100 prose-img:rounded-xl sm:prose-base prose-sm"> <div
class="prose prose-sm prose-zinc max-w-none 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"
>
<div set:html={post.content} /> <div set:html={post.content} />
</div> </div>
<!-- Next/Previous Navigation - Improved responsive design --> <!-- Next/Previous Navigation - Improved responsive design -->
<div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12"> <div
{prevPost && ( class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2"
>
{
prevPost && (
<a <a
href={`/blog/${prevPost.slug}`} href={`/blog/${prevPost.slug}`}
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden" class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6"
> >
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> <div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2"> <span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1"> <svg
<path d="m15 18-6-6 6-6"></path> xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
>
<path d="m15 18-6-6 6-6" />
</svg> </svg>
Previous Article Previous Article
</span> </span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors"> <h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{prevPost.title} {prevPost.title}
</h3> </h3>
</a> </a>
)} )
{nextPost && ( }
{
nextPost && (
<a <a
href={`/blog/${nextPost.slug}`} href={`/blog/${nextPost.slug}`}
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 md:text-right overflow-hidden" class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right"
> >
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> <div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end"> <span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
Next Article Next Article
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path d="m9 18 6-6-6-6"></path> xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
>
<path d="m9 18 6-6-6-6" />
</svg> </svg>
</span> </span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors"> <h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{nextPost.title} {nextPost.title}
</h3> </h3>
</a> </a>
)} )
}
</div> </div>
</BlogPost> </BlogPost>
@@ -81,9 +122,12 @@ const { post, nextPost, prevPost } = Astro.props;
function initializeCodeCopyButtons() { function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre'); const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach(block => { codeBlocks.forEach((block) => {
// Skip if already processed by either method // Skip if already processed by either method
if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) { if (
block.classList.contains('code-block-processed') ||
block.classList.contains('enhanced')
) {
return; return;
} }
@@ -91,7 +135,10 @@ const { post, nextPost, prevPost } = Astro.props;
// Create wrapper if not already wrapped // Create wrapper if not already wrapped
let wrapper; let wrapper;
if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) { if (
block.parentNode.classList.contains('relative') &&
block.parentNode.classList.contains('group')
) {
wrapper = block.parentNode; wrapper = block.parentNode;
} else { } else {
wrapper = document.createElement('div'); wrapper = document.createElement('div');
@@ -103,7 +150,8 @@ const { post, nextPost, prevPost } = Astro.props;
// Add copy button if not already present // Add copy button if not already present
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) { if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
const copyButton = document.createElement('button'); const copyButton = document.createElement('button');
copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200'; copyButton.className =
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
@@ -138,7 +186,7 @@ const { post, nextPost, prevPost } = Astro.props;
// Handle SPA transitions for blog post navigation // Handle SPA transitions for blog post navigation
function setupSPATransitions() { function setupSPATransitions() {
// Handle prev/next navigation links // Handle prev/next navigation links
document.querySelectorAll('a[href^="/blog/"]').forEach(link => { document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed // Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) { if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return; return;
@@ -206,7 +254,9 @@ const { post, nextPost, prevPost } = Astro.props;
/* Language badge styling */ /* Language badge styling */
.language-badge { .language-badge {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
text-transform: lowercase; text-transform: lowercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -227,8 +277,11 @@ const { post, nextPost, prevPost } = Astro.props;
@apply text-zinc-800 dark:text-zinc-200; @apply text-zinc-800 dark:text-zinc-200;
} }
.prose h1, .prose h2, .prose h3, .prose h4 { .prose h1,
@apply text-zinc-900 dark:text-zinc-100 font-semibold; .prose h2,
.prose h3,
.prose h4 {
@apply font-semibold text-zinc-900 dark:text-zinc-100;
} }
.prose h1 { .prose h1 {
@@ -236,52 +289,52 @@ const { post, nextPost, prevPost } = Astro.props;
} }
.prose h2 { .prose h2 {
@apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800; @apply mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
} }
.prose h3 { .prose h3 {
@apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3; @apply mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
} }
.prose p { .prose p {
@apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base; @apply mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
} }
.prose a { .prose a {
@apply text-zinc-800 dark:text-zinc-300 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-100 hover:decoration-zinc-600 dark:hover:decoration-zinc-400 transition-colors; @apply font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
} }
.prose blockquote { .prose blockquote {
@apply border-l-4 border-zinc-300 dark:border-zinc-700 pl-4 italic text-zinc-700 dark:text-zinc-300 my-4 sm:my-6; @apply my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
} }
.prose code { .prose code {
@apply bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-800 dark:text-zinc-200 text-sm font-medium; @apply rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
} }
.prose pre { .prose pre {
@apply bg-[#1e293b] dark:bg-[#1e293b] text-zinc-200 p-3 sm:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm my-4 sm:my-6 shadow-md !important; @apply my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
} }
.prose pre code { .prose pre code {
@apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important; @apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
} }
.prose img { .prose img {
@apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto; @apply mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
} }
.prose ul, .prose ol { .prose ul,
@apply my-4 sm:my-6 pl-5 sm:pl-6; .prose ol {
@apply my-4 pl-5 sm:my-6 sm:pl-6;
} }
.prose li { .prose li {
@apply mb-1 sm:mb-2 text-sm sm:text-base; @apply mb-1 text-sm sm:mb-2 sm:text-base;
} }
.prose hr { .prose hr {
@apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800; @apply my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
} }
/* Line clamp for truncating text */ /* Line clamp for truncating text */

View File

@@ -1,19 +1,17 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
const sortedPosts = posts.sort( const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
);
// Group posts by year for timeline effect // Group posts by year for timeline effect
const postsByYear = sortedPosts.reduce((acc, post) => { const postsByYear = sortedPosts.reduce((acc, post) => {
@@ -29,78 +27,90 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
const totalPosts = sortedPosts.length; const totalPosts = sortedPosts.length;
// Get unique tags for search suggestions // Get unique tags for search suggestions
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))]; const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
--- ---
<BaseLayout title="Blog"> <BaseLayout title="Blog">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16"> <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
<!-- Header with search --> <!-- Header with search -->
<div class="relative mb-12 sm:mb-20"> <div class="relative mb-12 sm:mb-20">
<!-- Decorative elements --> <!-- Decorative elements -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div> <div
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div> class="animate-blob absolute -left-10 -top-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-72 sm:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-72 sm:w-72"
>
</div>
<div class="relative text-center"> <div class="relative text-center">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4"> <h1
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl"
>
Blog Blog
</h1> </h1>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto"> <p
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:mb-10 sm:text-base"
>
Thoughts, ideas, and explorations on technology and selfhosting. Thoughts, ideas, and explorations on technology and selfhosting.
</p> </p>
</div> </div>
</div> </div>
<!-- Grid layout for mobile experience --> <!-- Grid layout for mobile experience -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8"> <div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
<!-- Featured post (if exists) --> <!-- Featured post (if exists) -->
{sortedPosts.length > 0 && ( {
<div class="md:col-span-12 mb-8 sm:mb-12"> sortedPosts.length > 0 && (
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8"> <div class="mb-8 sm:mb-12 md:col-span-12">
<div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8"> <article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 dark:border-zinc-800 sm:pb-8">
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
{sortedPosts[0].image && ( {sortedPosts[0].image && (
<div class="w-full md:w-1/2 h-60 sm:h-80 md:h-96 overflow-hidden mx-auto md:mx-0 max-w-full sm:max-w-md"> <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 <img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
alt={sortedPosts[0].title} alt={sortedPosts[0].title}
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105" class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="eager" loading="eager"
/> />
</div> </div>
)} )}
<div class="flex-1 flex flex-col justify-center"> <div class="flex flex-1 flex-col justify-center">
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-2 mb-3 justify-center md:justify-start"> <div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 sm:text-sm md:justify-start">
<span class="font-medium uppercase tracking-wider">Featured</span> <span class="font-medium uppercase tracking-wider">Featured</span>
<span class="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span> <span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
{sortedPosts[0].published_date && ( {sortedPosts[0].published_date && (
<time datetime={sortedPosts[0].published_date.toLocaleString()}> <time datetime={sortedPosts[0].published_date.toLocaleString()}>
{sortedPosts[0].published_date.toLocaleString('en-US', { {sortedPosts[0].published_date.toLocaleString('en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric' day: 'numeric',
})} })}
</time> </time>
)} )}
</div> </div>
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left"> <h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-4 sm:text-3xl md:text-left">
<a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0"> <a
href={`/blog/${sortedPosts[0].slug}/`}
class="before:absolute before:inset-0"
>
{sortedPosts[0].title} {sortedPosts[0].title}
</a> </a>
</h2> </h2>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 sm:mb-6 line-clamp-3 text-center md:text-left"> <p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mb-6 sm:text-base md:text-left">
{sortedPosts[0].description} {sortedPosts[0].description}
</p> </p>
<!-- Improved mobile layout for featured post metadata --> <div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
<div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">
{sortedPosts[0].tags && ( {sortedPosts[0].tags && (
<div class="flex flex-wrap gap-2 justify-center md:justify-start"> <div class="flex flex-wrap justify-center gap-2 md:justify-start">
{sortedPosts[0].tags.slice(0, 2).map((tag) => ( {sortedPosts[0].tags.slice(0, 2).map((tag) => (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400"> <span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag} {tag}
</span> </span>
))} ))}
@@ -111,83 +121,100 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
</div> </div>
</article> </article>
</div> </div>
)} )
}
<!-- Improved sidebar for mobile --> <!-- Improved sidebar for mobile -->
<div class="md:col-span-3 relative"> <div class="relative md:col-span-3">
<div class="md:sticky md:top-24 space-y-4 mb-8 md:mb-0"> <div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-4 uppercase tracking-wider text-center md:text-left">Archive</h3> <h3
class="mb-4 text-center text-lg font-medium uppercase tracking-wider text-zinc-900 dark:text-zinc-100 md:text-left"
>
Archive
</h3>
<!-- Horizontal scrollable archive on mobile, vertical on desktop --> <!-- Horizontal scrollable archive on mobile, vertical on desktop -->
<div class="flex md:flex-col overflow-x-auto md:overflow-visible pb-4 md:pb-0 hide-scrollbar"> <div
{years.map((year, index) => ( class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
>
{
years.map((year, index) => (
<a <a
href={`#year-${year}`} href={`#year-${year}`}
class={`flex items-center py-2 md:py-3 px-4 md:px-0 mr-3 md:mr-0 border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors md:w-full whitespace-nowrap md:whitespace-normal rounded-full md:rounded-none ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`} class={`mr-3 flex items-center whitespace-nowrap rounded-full border-b border-zinc-100 px-4 py-2 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900 md:mr-0 md:w-full md:whitespace-normal md:rounded-none md:px-0 md:py-3 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
> >
<span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span> <span class="text-base font-medium text-zinc-900 dark:text-zinc-100 md:text-lg">
<span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400"> {year}
</span>
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 md:ml-auto md:text-sm">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''} {postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span> </span>
</a> </a>
))} ))
}
</div> </div>
</div> </div>
</div> </div>
<!-- Improved post grid for mobile --> <!-- Improved post grid for mobile -->
<div class="md:col-span-9"> <div class="md:col-span-9">
{years.map((year) => ( {
<div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16"> years.map((year) => (
<h2 class="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 border-b border-zinc-200 dark:border-zinc-800 pb-3 sm:pb-4 text-center md:text-left"> <div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 dark:border-zinc-800 dark:text-zinc-100 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left">
{year} {year}
</h2> </h2>
<div class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}> <div
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
>
{postsByYear[year].map((post, index) => ( {postsByYear[year].map((post, index) => (
<article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md"> <article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
{post.image && ( {post.image && (
<div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg"> <div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
<img <img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title} alt={post.title}
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105" class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="lazy" loading="lazy"
/> />
</div> </div>
)} )}
<div class="flex-1 flex flex-col"> <div class="flex flex-1 flex-col">
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center md:justify-start flex-wrap"> <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start">
{post.pubDate && ( {post.pubDate && (
<time datetime={post.published_date.toLocaleString()} class="flex items-center"> <time
datetime={post.published_date.toLocaleString()}
class="flex items-center"
>
{post.published_date.toLocaleString('en-US', { {post.published_date.toLocaleString('en-US', {
month: 'short', month: 'short',
day: 'numeric' day: 'numeric',
})} })}
</time> </time>
)} )}
</div> </div>
<h3 class="text-lg sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left"> <h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-xl md:text-left">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title} {post.title}
</a> </a>
</h3> </h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left"> <p class="mb-4 line-clamp-2 flex-grow text-center text-sm text-zinc-600 dark:text-zinc-400 md:text-left">
{post.description} {post.description}
</p> </p>
{post.tags && ( {post.tags && (
<div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start"> <div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
{post.tags.slice(0, 2).map((tag) => ( {post.tags.slice(0, 2).map((tag) => (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400"> <span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag} {tag}
</span> </span>
))} ))}
{post.tags.length > 2 && ( {post.tags.length > 2 && (
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400"> <span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
+{post.tags.length - 2} +{post.tags.length - 2}
</span> </span>
)} )}
@@ -198,7 +225,8 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
))} ))}
</div> </div>
</div> </div>
))} ))
}
</div> </div>
</div> </div>
</div> </div>
@@ -215,7 +243,8 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
} }
@keyframes blob-bounce { @keyframes blob-bounce {
0%, 100% { 0%,
100% {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
25% { 25% {
@@ -235,7 +264,8 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 0; opacity: 0;
} }
50% { 50% {
@@ -275,7 +305,8 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
/* Improved touch targets for mobile */ /* Improved touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, button { a,
button {
min-height: 44px; min-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -304,7 +335,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
backToTopButton.addEventListener('click', () => { backToTopButton.addEventListener('click', () => {
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: 'smooth',
}); });
}); });
@@ -314,7 +345,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
} }
// Add smooth scrolling to year links // Add smooth scrolling to year links
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => { document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) { anchor.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const targetId = this.getAttribute('href'); const targetId = this.getAttribute('href');
@@ -323,7 +354,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
if (targetElement) { if (targetElement) {
window.scrollTo({ window.scrollTo({
top: targetElement.offsetTop - 100, top: targetElement.offsetTop - 100,
behavior: 'smooth' behavior: 'smooth',
}); });
// Update URL hash without jumping // Update URL hash without jumping
@@ -338,7 +369,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
if (isTouchDevice) { if (isTouchDevice) {
const articles = document.querySelectorAll('article'); const articles = document.querySelectorAll('article');
articles.forEach(article => { articles.forEach((article) => {
article.addEventListener('touchstart', () => { article.addEventListener('touchstart', () => {
article.classList.add('is-touched'); article.classList.add('is-touched');
}); });
@@ -355,7 +386,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
// SPA transition handling // SPA transition handling
function setupSPATransitions() { function setupSPATransitions() {
// Handle all blog post links for SPA transitions // Handle all blog post links for SPA transitions
document.querySelectorAll('a[href^="/blog/"]').forEach(link => { document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed // Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) { if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return; return;
@@ -391,7 +422,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
}); });
// Handle year anchor links specially // Handle year anchor links specially
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => { document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.setAttribute('data-spa-internal', 'true'); anchor.setAttribute('data-spa-internal', 'true');
}); });
} }

View File

@@ -2,15 +2,15 @@
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import FormattedDate from '../components/FormattedDate.astro'; import FormattedDate from '../components/FormattedDate.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readItems,readSingleton } from "@directus/sdk"; import { readItems, readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
@@ -18,42 +18,67 @@ const recentPosts = posts
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime()) .sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
.slice(0, 3); .slice(0, 3);
const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5); const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
--- ---
<Layout title=`Home | ${global.name}`> <Layout title=`Home | ${global.name}`>
<!-- Hero Section with improved mobile responsiveness --> <!-- Hero Section with improved mobile responsiveness -->
<section class="py-10 sm:py-16 md:py-20 px-4 sm:px-6 theme-transition-all"> <section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
<div class="max-w-2xl mx-auto relative"> <div class="relative mx-auto max-w-2xl">
<!-- Adjusted blob positions and sizes for better mobile appearance --> <!-- Adjusted blob positions and sizes for better mobile appearance -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div> <div
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div> class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:-left-20 sm:-top-20 sm:h-64 sm:w-64"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-64 sm:w-64"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color hero-text"> <h1
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl lg:text-6xl"
>
<span class="block">Writing on technology,</span> <span class="block">Writing on technology,</span>
<span class="block mt-1">development, and</span> <span class="mt-1 block">development, and</span>
<span class="block mt-1 relative"> <span class="relative mt-1 block">
<span class="relative inline-block"> <span class="relative inline-block">
selfhosting. selfhosting.
<span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-800 dark:bg-zinc-200 transform origin-left theme-transition-bg"></span> <span
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>
</span> </span>
</h1> </h1>
<p class="mt-4 sm:mt-6 md:mt-8 text-base sm:text-lg text-zinc-600 dark:text-zinc-400 leading-relaxed theme-transition-color max-w-lg mx-auto sm:mx-0"> <p
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8"
>
{global.about} {global.about}
</p> </p>
<div class="mt-6 sm:mt-8 md:mt-10 flex flex-wrap gap-3 sm:gap-4 md:gap-6 justify-center sm:justify-start"> <div
class="mt-6 flex flex-wrap justify-center gap-3 sm:mt-8 sm:justify-start sm:gap-4 md:mt-10 md:gap-6"
>
<a <a
href="/about" href="/about"
class="group relative inline-flex items-center gap-2 text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 transition-all duration-300 theme-transition-color min-h-[44px]" 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> <span>More about me</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="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"></path>
</svg> </svg>
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></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> </a>
</div> </div>
</div> </div>
@@ -61,72 +86,96 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
</section> </section>
<!-- Featured Post Section - Improved for mobile --> <!-- Featured Post Section - Improved for mobile -->
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> <section
<div class="max-w-3xl mx-auto"> class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16"
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8 md:mb-12"> >
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color text-center sm:text-left">Recent Posts</h2> <div class="mx-auto max-w-3xl">
<div
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
>
<h2
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-left sm:text-2xl md:text-3xl"
>
Recent Posts
</h2>
<a <a
href="/blog" href="/blog"
class="group relative text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 self-center sm:self-auto theme-transition-color min-h-[44px] flex items-center justify-center" 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 dark:text-zinc-100 dark:hover:text-zinc-300 sm:self-auto"
> >
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
View all posts View all posts
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="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"></path>
</svg> </svg>
</span> </span>
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></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> </a>
</div> </div>
<!-- Improved grid for better mobile layout --> <!-- Improved grid for better mobile layout -->
<div class="grid gap-6 sm:gap-8 md:gap-12 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
{recentPosts.map((post, index) => ( {
<article class="group relative flex flex-col items-start hover-3d theme-transition-element max-w-sm mx-auto sm:mx-0 w-full"> recentPosts.map((post, index) => (
<div class="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 dark:bg-zinc-800/50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl theme-transition-bg"></div> <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 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
{post.image && ( {post.image && (
<div class="relative z-10 w-full aspect-video mb-4 overflow-hidden rounded-lg"> <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
<img <img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title} alt={post.title}
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
loading={index === 0 ? "eager" : "lazy"} loading={index === 0 ? 'eager' : 'lazy'}
width="400" width="400"
height="225" height="225"
/> />
</div> </div>
)} )}
<div class="relative z-10 flex items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color justify-center sm:justify-start w-full"> <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 dark:text-zinc-400 sm:justify-start sm:gap-x-4">
<time datetime={post.published_date.toLocaleString()} class="font-medium"> <time datetime={post.published_date.toLocaleString()} class="font-medium">
<FormattedDate date={post.published_date} /> <FormattedDate date={post.published_date} />
</time> </time>
</div> </div>
<h3 class="relative z-10 mt-3 text-lg sm:text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color text-center sm:text-left w-full"> <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 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-left sm:text-xl">
<a href={`/blog/${post.slug}`} class="min-h-[44px] flex items-center justify-center sm:justify-start"> <a
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4"></span> href={`/blog/${post.slug}`}
class="flex min-h-[44px] items-center justify-center sm:justify-start"
>
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
{post.title} {post.title}
</a> </a>
</h3> </h3>
<p class="relative z-10 mt-2 sm:mt-3 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-3 theme-transition-color text-center sm:text-left w-full"> <p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mt-3 sm:text-left">
{post.description} {post.description}
</p> </p>
{post.tags && post.tags.length > 0 && ( {post.tags && post.tags.length > 0 && (
<div class="relative z-10 mt-3 sm:mt-4 flex flex-wrap gap-2 justify-center sm:justify-start w-full"> <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 => ( {post.tags.slice(0, 3).map((tag) => (
<a <a
href={`/topics/${tag}`} href={`/topics/${tag}`}
class="inline-flex items-center rounded-full bg-zinc-100 px-2 sm:px-3 py-1 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors theme-transition-all min-h-[28px]" 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 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 sm:px-3"
> >
#{tag} #{tag}
</a> </a>
))} ))}
{post.tags.length > 3 && ( {post.tags.length > 3 && (
<span class="inline-flex 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 theme-transition-all min-h-[28px]"> <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 +{post.tags.length - 3} more
</span> </span>
)} )}
@@ -135,65 +184,96 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
<a <a
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
class="relative z-10 mt-3 sm:mt-4 flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors theme-transition-color mx-auto sm:mx-0 min-h-[44px]" 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 dark:text-zinc-300 dark:group-hover:text-zinc-100 sm:mx-0 sm:mt-4"
> >
<span class="relative overflow-hidden inline-block"> <span class="relative inline-block overflow-hidden">
<span class="group-hover:-translate-y-full block transition-transform duration-300">Read article</span> <span class="block transition-transform duration-300 group-hover:-translate-y-full">
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span> Read article
</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 group-hover:translate-x-1"> <span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
<path d="M6.75 5.75 9.25 8l-2.5 2.25" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> 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 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"
/>
</svg> </svg>
</a> </a>
</article> </article>
))} ))
}
</div> </div>
</div> </div>
</section> </section>
<!-- Topics/Tags Section - Improved for mobile --> <!-- Topics/Tags Section - Improved for mobile -->
{allTags.length > 0 && ( {
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> allTags.length > 0 && (
<div class="max-w-3xl mx-auto"> <section class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16">
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 theme-transition-color text-center sm:text-left">Explore Topics</h2> <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 dark:text-zinc-100 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl">
Explore Topics
</h2>
<!-- Improved grid layout for mobile --> <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">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4 max-w-xs sm:max-w-none mx-auto"> {allTags.map((tag) => {
{allTags.map(tag => { const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
const tagCount = posts.filter(post => post.tags && post.tags.includes(tag)).length;
return ( return (
<a <a
href={`/topics/${tag}`} href={`/topics/${tag}`}
class="group flex flex-col p-3 sm:p-4 md:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/70 transition-all duration-300 theme-transition-all min-h-[80px] sm:min-h-[90px]" 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 dark:border-zinc-800 dark:hover:bg-zinc-800/70 sm:min-h-[90px] sm:p-4 md:p-6"
> >
<div class="flex items-start justify-between mb-2"> <div class="mb-2 flex items-start justify-between">
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100 theme-transition-color mr-2">#{tag}</span> <span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
<span class="text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 px-2 py-0.5 rounded-full flex-shrink-0 theme-transition-all"> #{tag}
</span>
<span class="theme-transition-all flex-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'} {tagCount} {tagCount === 1 ? 'post' : 'posts'}
</span> </span>
</div> </div>
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1 theme-transition-color"> <p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
Explore articles about {tag} Explore articles about {tag}
</p> </p>
</a> </a>
) );
})} })}
</div> </div>
<div class="mt-6 sm:mt-8 text-center"> <div class="mt-6 text-center sm:mt-8">
<a <a
href="/tags" href="/tags"
class="inline-flex items-center text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 theme-transition-color min-h-[44px]" 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> <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="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> 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> </svg>
</a> </a>
</div> </div>
</div> </div>
</section> </section>
)} )
}
</Layout> </Layout>
<script> <script>
@@ -205,7 +285,7 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
if (isTouchDevice) { if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d'); const cards = document.querySelectorAll('.hover-3d');
cards.forEach(card => { cards.forEach((card) => {
card.addEventListener('touchstart', () => { card.addEventListener('touchstart', () => {
card.classList.add('is-touched'); card.classList.add('is-touched');
}); });
@@ -254,7 +334,7 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// Apply fixed height to sections to prevent resizing // Apply fixed height to sections to prevent resizing
const sections = document.querySelectorAll('section'); const sections = document.querySelectorAll('section');
sections.forEach(section => { sections.forEach((section) => {
section.style.width = '100%'; section.style.width = '100%';
}); });
} }
@@ -283,8 +363,11 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// Update theme-transition elements without forcing reflow of entire page // Update theme-transition elements without forcing reflow of entire page
requestAnimationFrame(() => { requestAnimationFrame(() => {
document.querySelectorAll('.theme-transition-all, .theme-transition-bg, .theme-transition-color') document
.forEach(el => { .querySelectorAll(
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
)
.forEach((el) => {
// Apply a subtle animation instead of a hard reset // Apply a subtle animation instead of a hard reset
el.style.transition = 'all 0.5s ease'; el.style.transition = 'all 0.5s ease';
}); });
@@ -304,7 +387,7 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
setTimeout(() => { setTimeout(() => {
window.scrollTo({ window.scrollTo({
top: scrollPosition, top: scrollPosition,
behavior: 'auto' // Use 'auto' to prevent animation behavior: 'auto', // Use 'auto' to prevent animation
}); });
}, 10); }, 10);
} }
@@ -327,27 +410,38 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// Add smooth reveal animations for content after loading // Add smooth reveal animations for content after loading
const animateContent = () => { const animateContent = () => {
// Animate hero section // Animate hero section
const heroElements = document.querySelectorAll('.hero-text span, .hero-text + p, .hero-text ~ div'); const heroElements = document.querySelectorAll(
'.hero-text span, .hero-text + p, .hero-text ~ div'
);
heroElements.forEach((el, index) => { heroElements.forEach((el, index) => {
setTimeout(() => { setTimeout(
() => {
el.classList.add('animate-reveal'); el.classList.add('animate-reveal');
}, 100 + (index * 150)); },
100 + index * 150
);
}); });
// Animate posts with staggered delay // Animate posts with staggered delay
const articles = document.querySelectorAll('article.group'); const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => { articles.forEach((article, index) => {
setTimeout(() => { setTimeout(
() => {
article.classList.add('animate-reveal'); article.classList.add('animate-reveal');
}, 500 + (index * 150)); },
500 + index * 150
);
}); });
// Animate topic cards with staggered delay // Animate topic cards with staggered delay
const topicCards = document.querySelectorAll('a.group.flex.flex-col'); const topicCards = document.querySelectorAll('a.group.flex.flex-col');
topicCards.forEach((card, index) => { topicCards.forEach((card, index) => {
setTimeout(() => { setTimeout(
() => {
card.classList.add('animate-reveal'); card.classList.add('animate-reveal');
}, 800 + (index * 100)); },
800 + index * 100
);
}); });
}; };
@@ -361,10 +455,12 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// Wait for loading screen to hide // Wait for loading screen to hide
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.target === loadingScreen && if (
mutation.target === loadingScreen &&
mutation.type === 'attributes' && mutation.type === 'attributes' &&
mutation.attributeName === 'style' && mutation.attributeName === 'style' &&
loadingScreen.style.display === 'none') { loadingScreen.style.display === 'none'
) {
animateContent(); animateContent();
observer.disconnect(); observer.disconnect();
} }
@@ -385,11 +481,13 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// SPA transition handling for homepage // SPA transition handling for homepage
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' || link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')) { link.hasAttribute('data-spa-handled')
) {
return; return;
} }
@@ -440,7 +538,8 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
--theme-transition-timing: ease; --theme-transition-timing: ease;
} }
:global(html), :global(body) { :global(html),
:global(body) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing); transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
} }
@@ -464,7 +563,8 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
} }
/* Remove the forced transition disabling which causes flickering */ /* Remove the forced transition disabling which causes flickering */
:global(.theme-switching), :global(.theme-switching *) { :global(.theme-switching),
:global(.theme-switching *) {
/* Use a subtle transition instead of none */ /* Use a subtle transition instead of none */
transition-duration: 0.3s !important; transition-duration: 0.3s !important;
} }
@@ -477,7 +577,9 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
a.group.flex.flex-col { a.group.flex.flex-col {
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(20px);
transition: opacity 0.8s ease, transform 0.8s ease; transition:
opacity 0.8s ease,
transform 0.8s ease;
} }
.animate-reveal { .animate-reveal {

View File

@@ -1,14 +1,14 @@
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readItems,readSingleton } from "@directus/sdk"; import { readItems, readSingleton } from '@directus/sdk';
export async function GET(context: any) { export async function GET(context: any) {
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );

View File

@@ -2,24 +2,26 @@
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro'; import FormattedDate from '../../components/FormattedDate.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
export const prerender = true; export const prerender = true;
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", { const posts = await directus.request(
readItems('posts', {
fields: ['*'], fields: ['*'],
})); })
);
// Get all unique tags // Get all unique tags
const uniqueTags = [...new Set(posts.flatMap(post => post.tags || []))]; const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
// Create a path for each tag // Create a path for each tag
return uniqueTags.map(tag => { return uniqueTags.map((tag) => {
// Make tag matching case-insensitive // Make tag matching case-insensitive
const filteredPosts = posts.filter(post => const filteredPosts = posts.filter(
post.tags?.some(t => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string (post) => post.tags?.some((t) => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
); );
return { return {
params: { tag }, params: { tag },
@@ -33,125 +35,191 @@ const { posts = [] } = Astro.props;
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`); console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
const sortedPosts = posts && posts.length > 0 const sortedPosts =
posts && posts.length > 0
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf()) ? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
: []; : [];
console.log(`Sorted posts length: ${sortedPosts.length}`); console.log(`Sorted posts length: ${sortedPosts.length}`);
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360); const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
const relatedTags = [...new Set( const relatedTags = [
sortedPosts.flatMap(post => post.tags || []) ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
.filter(t => t !== tag) ].slice(0, 5);
)].slice(0, 5);
--- ---
<BaseLayout title={`Posts tagged with "${tag}"`}> <BaseLayout title={`Posts tagged with "${tag}"`}>
<div class="max-w-5xl mx-auto px-4 py-10 sm:py-16"> <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
<!-- Header section --> <!-- Header section -->
<div class="relative mb-10 sm:mb-16"> <div class="relative mb-10 sm:mb-16">
<div class="absolute -top-20 -left-20 w-48 sm:w-64 h-48 sm:h-64 bg-zinc-100 dark:bg-zinc-900/30 rounded-full blur-3xl opacity-30 animate-blob"></div> <div
<div class="absolute -bottom-10 -right-10 w-36 sm:w-48 h-36 sm:h-48 bg-zinc-200 dark:bg-zinc-900/20 rounded-full blur-2xl opacity-20 animate-blob animation-delay-2000"></div> class="animate-blob absolute -left-20 -top-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-900/30 sm:h-64 sm:w-64"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl dark:bg-zinc-900/20 sm:h-48 sm:w-48"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<a href="/tags" class="inline-flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-4 group"> <a
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:-translate-x-1"> href="/tags"
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> 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
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
</svg> </svg>
<span>Back to all topics</span> <span>Back to all topics</span>
<span class="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-zinc-300 dark:bg-zinc-700"></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>
</a> </a>
<div class="flex flex-col sm:flex-row sm:items-center gap-4 mb-2 justify-center sm:justify-start"> <div
<div class="tag-icon flex items-center justify-center w-12 h-12 rounded-xl bg-zinc-100 dark:bg-zinc-800 shadow-sm mx-auto sm:mx-0"> class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-zinc-700 dark:text-zinc-300"> >
<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" /> <div
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" /> class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-sm dark:bg-zinc-800 sm:mx-0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-700 dark:text-zinc-300"
>
<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>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"></path>
</svg> </svg>
</div> </div>
<h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100"> <h1
class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl"
>
<span class="relative"> <span class="relative">
#{tag} #{tag}
<span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-200 dark:bg-zinc-700"></span> <span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
<span class="absolute -bottom-1 left-0 w-1/2 h-1 bg-zinc-900 dark:bg-zinc-100 opacity-70 animate-expand"></span> ></span>
<span
class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100"
></span>
</span> </span>
</h1> </h1>
</div> </div>
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mt-4 max-w-2xl mx-auto sm:mx-0"> <p
Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100">{sortedPosts.length}</span> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100">"{tag}"</span> class="mx-auto mt-4 max-w-2xl text-base text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:text-lg"
>
Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100"
>{sortedPosts.length}</span
> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100"
>"{tag}"</span
>
</p> </p>
</div> </div>
</div> </div>
<!-- Related tags section --> <!-- Related tags section -->
{relatedTags.length > 0 && ( {
<div class="mb-8 sm:mb-12 overflow-x-auto pb-4 hide-scrollbar"> relatedTags.length > 0 && (
<h2 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-3 text-center sm:text-left">Related topics</h2> <div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
<div class="flex gap-2 flex-nowrap justify-center sm:justify-start"> <h2 class="mb-3 text-center text-lg font-medium text-zinc-900 dark:text-zinc-100 sm:text-left">
{relatedTags.map(relatedTag => ( Related topics
</h2>
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
{relatedTags.map((relatedTag) => (
<a <a
href={`/topics/${relatedTag}`} href={`/topics/${relatedTag}`}
class="flex-shrink-0 inline-flex items-center rounded-full px-3 py-1.5 text-sm font-medium bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors" class="inline-flex 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} #{relatedTag}
</a> </a>
))} ))}
</div> </div>
</div> </div>
)} )
}
<!-- Posts list --> <!-- Posts list -->
<div class="relative"> <div class="relative">
<div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 pointer-events-none"></div> <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"> <div class="relative space-y-6 sm:space-y-8">
{sortedPosts.map((post) => ( {
<article class="group relative flex flex-col p-5 sm:p-8 rounded-2xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50/80 dark:hover:bg-zinc-900/50 transition-all duration-300 hover:shadow-md hover-card max-w-2xl mx-auto sm:mx-0"> sortedPosts.map((post) => (
<div class="absolute inset-0 bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 dark:from-zinc-900/0 dark:to-zinc-800/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl"></div> <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 dark:border-zinc-800 dark:hover:bg-zinc-900/50 sm:mx-0 sm:p-8">
<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 sm:flex-row gap-5 sm:gap-6"> <div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{post.image && ( {post.image && (
<div class="flex-shrink-0 w-full sm:w-56 h-40 rounded-xl overflow-hidden shadow-sm group-hover:shadow-md transition-all duration-300 mx-auto sm:mx-0"> <div class="mx-auto h-40 w-full flex-shrink-0 overflow-hidden rounded-xl shadow-sm transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56">
<img <img
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
alt={post.image_alt} alt={post.image_alt}
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy" loading="lazy"
/> />
</div> </div>
)} )}
<div class="flex-1"> <div class="flex-1">
<div class="flex flex-wrap items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center sm:justify-start"> <div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm">
{post.published_date && ( {post.published_date && (
<time datetime={post.published_date.toLocaleString()} class="flex items-center gap-1.5"> <time
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 sm:w-4 sm:h-4"> datetime={post.published_date.toLocaleString()}
<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 class="flex items-center gap-1.5"
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
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> </svg>
<FormattedDate date={post.published_date} /> <FormattedDate date={post.published_date} />
</time> </time>
)} )}
</div> </div>
<h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center sm:text-left"> <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-left sm:text-2xl">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0"> <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title} {post.title}
</a> </a>
</h2> </h2>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 sm:line-clamp-3 text-center sm:text-left"> <p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:line-clamp-3 sm:text-left sm:text-base">
{post.description} {post.description}
</p> </p>
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center sm:justify-between items-end mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800"> <div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 dark:border-zinc-800 sm:justify-between">
{post.tags && post.tags.length > 0 && ( {post.tags && post.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mb-3 sm:mb-0 justify-center sm:justify-start"> <div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
{post.tags.slice(0, 3).map(postTag => ( {post.tags.slice(0, 3).map((postTag) => (
<a <a
href={`/topics/${postTag}`} href={`/topics/${postTag}`}
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${ class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
@@ -174,43 +242,88 @@ const relatedTags = [...new Set(
<div class="mx-auto sm:ml-auto sm:mr-0"> <div class="mx-auto sm:ml-auto sm:mr-0">
<a <a
href={`/blog/${post.slug}/`} href={`/blog/${post.slug}/`}
class="inline-flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors" 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" aria-hidden="true"
tabindex="-1" tabindex="-1"
> >
<span class="relative overflow-hidden inline-block"> <span class="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full">Read article</span> <span class="block transition-transform duration-300 group-hover:-translate-y-full">
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span> Read article
</span> </span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1"> <span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> Explore now
</span>
</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> </svg>
</a> </a>
</div> </div>
</div> </div>
</article> </article>
))} ))
}
</div> </div>
</div> </div>
<!-- Empty state với màu zinc --> <!-- Empty state với màu zinc -->
{sortedPosts.length === 0 && ( {
<div class="text-center py-12 sm:py-20"> sortedPosts.length === 0 && (
<div class="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-4 sm:mb-6"> <div class="py-12 text-center sm:py-20">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 sm:w-10 sm:h-10 text-zinc-500 dark:text-zinc-400"> <div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 sm:mb-6 sm:h-20 sm:w-20">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" /> <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-8 w-8 text-zinc-500 dark:text-zinc-400 sm:h-10 sm:w-10"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg> </svg>
</div> </div>
<h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">No posts found</h2> <h2 class="mb-2 text-xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-2xl">
No posts found
</h2>
<p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p> <p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
<a href="/blog" class="inline-flex items-center gap-2 mt-6 px-4 py-2 rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-all duration-300 text-sm font-medium"> <a
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> href="/blog"
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" /> class="mt-6 inline-flex items-center gap-2 rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-800 transition-all duration-300 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</svg> </svg>
<span>Browse all articles</span> <span>Browse all articles</span>
</a> </a>
</div> </div>
)} )
}
</div> </div>
</BaseLayout> </BaseLayout>
@@ -237,8 +350,12 @@ const relatedTags = [...new Set(
/* Animated underline */ /* Animated underline */
@keyframes expand { @keyframes expand {
from { width: 0; } from {
to { width: 50%; } width: 0;
}
to {
width: 50%;
}
} }
.animate-expand { .animate-expand {
@@ -272,7 +389,10 @@ const relatedTags = [...new Set(
/* Hover card effect */ /* Hover card effect */
.hover-card { .hover-card {
transform: translateY(0); transform: translateY(0);
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease; transition:
transform 0.3s ease,
box-shadow 0.3s ease,
background-color 0.3s ease;
} }
@media (hover: hover) { @media (hover: hover) {
@@ -308,11 +428,13 @@ const relatedTags = [...new Set(
// Handle SPA transitions for tag pages // Handle SPA transitions for tag pages
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' || link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')) { link.hasAttribute('data-spa-handled')
) {
return; return;
} }
@@ -350,25 +472,34 @@ const relatedTags = [...new Set(
// Animate header elements // Animate header elements
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description'); const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
headerElements.forEach((el, index) => { headerElements.forEach((el, index) => {
setTimeout(() => { setTimeout(
() => {
el.classList.add('animate-reveal'); el.classList.add('animate-reveal');
}, 100 + (index * 150)); },
100 + index * 150
);
}); });
// Animate posts with staggered delay // Animate posts with staggered delay
const articles = document.querySelectorAll('article'); const articles = document.querySelectorAll('article');
articles.forEach((article, index) => { articles.forEach((article, index) => {
setTimeout(() => { setTimeout(
() => {
article.classList.add('animate-reveal'); article.classList.add('animate-reveal');
}, 400 + (index * 100)); },
400 + index * 100
);
}); });
// Animate related tags // Animate related tags
const relatedTags = document.querySelectorAll('.related-tags a'); const relatedTags = document.querySelectorAll('.related-tags a');
relatedTags.forEach((tag, index) => { relatedTags.forEach((tag, index) => {
setTimeout(() => { setTimeout(
() => {
tag.classList.add('animate-reveal'); tag.classList.add('animate-reveal');
}, 600 + (index * 50)); },
600 + index * 50
);
}); });
} }
@@ -387,4 +518,3 @@ const relatedTags = [...new Set(
</script> </script>
<!-- Add this at the end of your page --> <!-- Add this at the end of your page -->
</BaseLayout>

View File

@@ -1,102 +1,138 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
const tags = [...new Set(posts.flatMap(post => post.tags || []))].sort(); const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
// Count posts for each tag and create tag objects with additional data // Count posts for each tag and create tag objects with additional data
const tagObjects = tags.map(tag => { const tagObjects = tags.map((tag) => {
const count = posts.filter(post => post.tags?.includes(tag)).length; const count = posts.filter((post) => post.tags?.includes(tag)).length;
// Generate a consistent but random-looking hue for each tag // 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); const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
return { return {
name: tag, name: tag,
count, count,
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
hue hue,
}; };
}); });
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
--- ---
<BaseLayout title="Explore Tags"> <BaseLayout title="Explore Tags">
<div class="w-full mx-auto px-3 sm:px-6 py-6 sm:py-12 md:py-16 theme-transition-all"> <div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
<!-- Enhanced header section with animated elements - improved for mobile --> <!-- Enhanced header section with animated elements - improved for mobile -->
<div class="relative mb-8 sm:mb-12 md:mb-16 text-center theme-transition-element"> <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
<div class="absolute -top-16 -left-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div> <div
<div class="absolute -bottom-16 -right-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div> class="animate-blob theme-transition-bg absolute -left-16 -top-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:h-48 sm:w-48 md:h-72 md:w-72"
<div class="absolute top-8 right-8 w-24 sm:w-32 md:w-40 h-24 sm:h-32 md:h-40 bg-zinc-100/30 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-40 animate-blob animation-delay-4000 theme-transition-bg"></div> >
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-16 -right-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div
class="animate-blob animation-delay-4000 theme-transition-bg absolute right-8 top-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl dark:bg-zinc-700/20 sm:h-32 sm:w-32 md:h-40 md:w-40"
>
</div>
<h1 class="relative text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 theme-transition-color"> <h1
<span class="inline-block relative"> class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl"
>
<span class="relative inline-block"> <span class="relative inline-block">
<span class="absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 dark:from-zinc-800/50 dark:to-zinc-700/50 blur-sm theme-transition-bg"></span> <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-sm dark:from-zinc-800/50 dark:to-zinc-700/50"
></span>
<span class="relative">Explore</span> <span class="relative">Explore</span>
</span> </span>
{" "} {' '}
<span class="relative inline-block"> <span class="relative inline-block">
Topics Topics
<span class="absolute -bottom-1 sm:-bottom-2 left-0 w-full h-0.5 sm:h-1 bg-gradient-to-r from-zinc-400 to-zinc-600 dark:from-zinc-600 dark:to-zinc-400 transform origin-left animate-underline theme-transition-bg"></span> <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 dark:from-zinc-600 dark:to-zinc-400 sm:-bottom-2 sm:h-1"
></span>
</span> </span>
</span> </span>
</h1> </h1>
<p class="relative text-sm sm:text-base md:text-lg lg:text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto theme-transition-color"> <p
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:text-base md:text-lg lg:text-xl"
>
Discover content organized by your interests Discover content organized by your interests
</p> </p>
</div> </div>
{tags.length === 0 ? ( {
<div class="text-center py-8 sm:py-12 md:py-16 theme-transition-element"> tags.length === 0 ? (
<div class="inline-flex items-center justify-center w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-3 sm:mb-4 md:mb-6 shadow-inner theme-transition-bg"> <div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 sm:w-10 md:w-12 h-8 sm:h-10 md:h-12 text-zinc-500 dark:text-zinc-400 theme-transition-color"> <div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner dark:bg-zinc-800 sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24">
<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" /> <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 dark:text-zinc-400 sm:h-10 sm:w-10 md:h-12 md:w-12"
>
<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" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
</svg> </svg>
</div> </div>
<p class="text-lg sm:text-xl md:text-2xl font-medium text-zinc-800 dark:text-zinc-200 theme-transition-color">No tags found yet.</p> <p class="theme-transition-color text-lg font-medium text-zinc-800 dark:text-zinc-200 sm:text-xl md:text-2xl">
<p class="mt-2 text-xs sm:text-sm md:text-base text-zinc-500 dark:text-zinc-500 theme-transition-color">Check back later for categorized content.</p> No tags found yet.
</p>
<p class="theme-transition-color mt-2 text-xs text-zinc-500 dark:text-zinc-500 sm:text-sm md:text-base">
Check back later for categorized content.
</p>
</div> </div>
) : ( ) : (
<div class="flex justify-center w-full"> <div class="flex w-full justify-center">
<!-- Featured Tags Section - ultra-responsive design --> <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-sm dark:border-zinc-800 dark:bg-zinc-900/50 sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8">
<div class="tag-cloud relative p-3 sm:p-4 md:p-6 lg:p-8 rounded-lg sm:rounded-xl md:rounded-2xl lg:rounded-3xl border border-zinc-100 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm hover-3d glass theme-transition-all w-full"> <div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
<div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 theme-transition-bg"></div> <div class="theme-transition-bg absolute -right-8 -top-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl dark:from-zinc-700/20 dark:to-zinc-800/10 sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40" />
<div class="absolute -top-8 -right-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div> <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 dark:from-zinc-700/20 dark:to-zinc-800/10 sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40" />
<div class="absolute -bottom-8 -left-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div>
<h2 class="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 lg:mb-8 text-center theme-transition-color">Popular Topics</h2> <h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 dark:text-zinc-100 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl">
Popular Topics
</h2>
<!-- Ultra-responsive grid layout with fallbacks --> <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">
<div class="grid grid-cols-2 xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-1.5 xxxs:gap-2 xxs:gap-2 xs:gap-2 sm:gap-3 md:gap-4 w-full">
{sortedTags.map((tag) => ( {sortedTags.map((tag) => (
<a <a
href={`/topics/${tag.name}`} href={`/topics/${tag.name}`}
class="group relative overflow-hidden rounded-md sm:rounded-lg md:rounded-xl border border-zinc-200 dark:border-zinc-800 transition-all duration-300 hover:shadow-md sm:hover:shadow-lg hover:scale-[1.03] hover:border-zinc-300 dark:hover:border-zinc-700 active:scale-95 theme-transition-element theme-ripple flex-grow min-w-0" class="theme-transition-element theme-ripple group relative min-w-0 flex-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 dark:border-zinc-800 dark:hover:border-zinc-700 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl"
style={`--tag-hue: ${tag.hue};`} style={`--tag-hue: ${tag.hue};`}
> >
<div class="absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 dark:from-zinc-800/90 dark:to-zinc-900/90 opacity-100 group-hover:opacity-95 transition-opacity theme-transition-bg"></div> <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="relative px-1.5 xxxs:px-2 xxs:px-2 xs:px-2 sm:px-3 md:px-4 py-1.5 xxxs:py-2 xxs:py-2 xs:py-2 sm:py-3 md:py-4 flex items-center gap-1.5 xxs:gap-2 w-full"> <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="flex-shrink-0 flex items-center justify-center w-5 h-5 xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 sm:w-8 sm:h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light transition-all duration-300 shadow-sm theme-transition-all"> <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 flex-shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-sm transition-all duration-300 dark:bg-zinc-800 dark:text-zinc-300 sm:h-8 sm:w-8 md:h-10 md:w-10">
<span class="text-xs xxxs:text-xs xxs:text-xs xs:text-sm sm:text-base md:text-lg font-semibold">#</span> <span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
#
</span>
</div> </div>
<div class="flex-1 min-w-0 overflow-hidden"> <div class="min-w-0 flex-1 overflow-hidden">
<h3 class="text-[10px] xxxs:text-xs xxs:text-xs xs:text-xs sm:text-sm md:text-base font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color break-words hyphens-auto truncate"> <h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate hyphens-auto break-words text-[10px] font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-sm md:text-base">
{tag.name} {tag.name}
</h3> </h3>
<p class="text-[8px] xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] sm:text-xs md:text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color truncate">{tag.count} article{tag.count !== 1 ? 's' : ''}</p> <p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 dark:text-zinc-400 sm:text-xs md:text-xs">
{tag.count} article{tag.count !== 1 ? 's' : ''}
</p>
</div> </div>
</div> </div>
</a> </a>
@@ -104,7 +140,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
</div> </div>
</div> </div>
</div> </div>
)} )
}
</div> </div>
</BaseLayout> </BaseLayout>
@@ -121,7 +158,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta); document.getElementsByTagName('head')[0].appendChild(meta);
} else { } else {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
} }
// Fix for horizontal overflow // Fix for horizontal overflow
@@ -136,7 +176,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// Adjust tag items based on screen size with extreme precision // Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => { const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple'); const tagItems = document.querySelectorAll('.theme-ripple');
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360; const isVerySmall = width < 360;
const isExtremelySmall = width < 280; const isExtremelySmall = width < 280;
const isMicroScreen = width < 240; const isMicroScreen = width < 240;
@@ -160,7 +201,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
grid.style.maxWidth = '100%'; grid.style.maxWidth = '100%';
} }
tagItems.forEach(item => { tagItems.forEach((item) => {
// Set appropriate classes based on screen size // Set appropriate classes based on screen size
if (isMicroScreen) { if (isMicroScreen) {
item.classList.add('micro-screen'); item.classList.add('micro-screen');
@@ -237,7 +278,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// Fix for iOS Safari and other mobile browsers // Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) { if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty('--safe-area-inset-bottom', 'env(safe-area-inset-bottom)'); document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues // Fix for mobile viewport height issues
const setVh = () => { const setVh = () => {
@@ -260,19 +304,29 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
const addTouchSupport = () => { const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple'); const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach(item => { tagItems.forEach((item) => {
item.addEventListener('touchstart', () => { item.addEventListener(
'touchstart',
() => {
item.classList.add('touch-active'); item.classList.add('touch-active');
}, { passive: true }); },
{ passive: true }
);
item.addEventListener('touchend', () => { item.addEventListener(
'touchend',
() => {
setTimeout(() => { setTimeout(() => {
item.classList.remove('touch-active'); item.classList.remove('touch-active');
}, 150); }, 150);
}, { passive: true }); },
{ passive: true }
);
// Cancel active state if touch moves away // Cancel active state if touch moves away
item.addEventListener('touchmove', (e) => { item.addEventListener(
'touchmove',
(e) => {
const touch = e.touches[0]; const touch = e.touches[0];
const rect = item.getBoundingClientRect(); const rect = item.getBoundingClientRect();
@@ -284,7 +338,9 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
) { ) {
item.classList.remove('touch-active'); item.classList.remove('touch-active');
} }
}, { passive: true }); },
{ passive: true }
);
}); });
}; };
@@ -295,7 +351,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
<style> <style>
/* Base styles */ /* Base styles */
.tag-cloud { .tag-cloud {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.03), 0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05); 0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d; transform-style: preserve-3d;
@@ -309,7 +366,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
} }
/* Fix for horizontal overflow */ /* Fix for horizontal overflow */
:global(html), :global(body) { :global(html),
:global(body) {
overflow-x: hidden; overflow-x: hidden;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -489,7 +547,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
/* Improved shadow for dark mode */ /* Improved shadow for dark mode */
:global(.dark) .tag-cloud { :global(.dark) .tag-cloud {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05), box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15); 0 4px 8px rgba(0, 0, 0, 0.15);
} }
@@ -512,12 +571,15 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
.touch-active { .touch-active {
transform: scale(0.97) !important; transform: scale(0.97) !important;
opacity: 0.9; opacity: 0.9;
transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out !important; transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
} }
/* Animation for blob */ /* Animation for blob */
@keyframes blob { @keyframes blob {
0%, 100% { 0%,
100% {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
25% { 25% {
@@ -571,11 +633,13 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// Handle SPA transitions for tags index page // Handle SPA transitions for tags index page
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' || link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')) { link.hasAttribute('data-spa-handled')
) {
return; return;
} }
@@ -614,7 +678,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
if (isTouchDevice) { if (isTouchDevice) {
const tagCards = document.querySelectorAll('.tag-cloud a'); const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach(card => { tagCards.forEach((card) => {
card.addEventListener('touchstart', () => { card.addEventListener('touchstart', () => {
card.classList.add('is-touched'); card.classList.add('is-touched');
}); });
@@ -630,9 +694,12 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// Animate tag cards with staggered delay // Animate tag cards with staggered delay
const tagCards = document.querySelectorAll('.tag-cloud a'); const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach((card, index) => { tagCards.forEach((card, index) => {
setTimeout(() => { setTimeout(
() => {
card.classList.add('animate-reveal'); card.classList.add('animate-reveal');
}, 100 + (index * 50)); },
100 + index * 50
);
}); });
} }
@@ -645,4 +712,3 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions); document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>

View File

@@ -5,7 +5,7 @@
@layer base { @layer base {
:root { :root {
font-family: "Inter", sans-serif; font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
--theme-transition: 0.3s ease; --theme-transition: 0.3s ease;
@@ -24,8 +24,11 @@
} }
/* Simple theme transition */ /* Simple theme transition */
body, a, button { body,
transition: background-color var(--theme-transition), a,
button {
transition:
background-color var(--theme-transition),
color var(--theme-transition), color var(--theme-transition),
border-color var(--theme-transition); border-color var(--theme-transition);
} }
@@ -38,31 +41,53 @@
} }
/* Better touch targets on mobile */ /* Better touch targets on mobile */
button, a { button,
a {
@apply min-h-[44px]; @apply min-h-[44px];
} }
} }
/* Add smooth animations */ /* Add smooth animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideUp { @keyframes slideUp {
from { transform: translateY(20px); opacity: 0; } from {
to { transform: translateY(0); opacity: 1; } transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
@keyframes slideDown { @keyframes slideDown {
from { transform: translateY(-20px); opacity: 0; } from {
to { transform: translateY(0); opacity: 1; } transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
@keyframes scaleIn { @keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; } from {
to { transform: scale(1); opacity: 1; } transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
} }
/* Apply animations to elements */ /* Apply animations to elements */
@@ -100,17 +125,21 @@
} }
/* Smooth hover transitions */ /* Smooth hover transitions */
a, button { a,
button {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
a:hover, button:hover { a:hover,
button:hover {
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Smooth page transitions */ /* Smooth page transitions */
.page-transition { .page-transition {
transition: opacity 0.3s ease, transform 0.3s ease; transition:
opacity 0.3s ease,
transform 0.3s ease;
} }
.page-entering { .page-entering {

View File

@@ -1,4 +1,3 @@
export function debugObject(obj: any): string { export function debugObject(obj: any): string {
return JSON.stringify(obj, null, 2) return JSON.stringify(obj, null, 2);
} }

View File

@@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "*.{js,ts,jsx,tsx,mdx}"], content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', '*.{js,ts,jsx,tsx,mdx}'],
darkMode: 'class', darkMode: 'class',
theme: { theme: {
extend: { extend: {
@@ -54,7 +54,5 @@ module.exports = {
}), }),
}, },
}, },
plugins: [ plugins: [require('@tailwindcss/typography')],
require('@tailwindcss/typography'),
],
}; };