Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
06f7546212 | |||
abd1d43f79 | |||
07f2f5f0e1 | |||
91b53a33c2 | |||
b3e23f3e6c | |||
ab68b6248f | |||
37d1f1d1f2 | |||
89e1c59e37 | |||
7153f29022 | |||
51041f6ae9 | |||
67f12ecf72 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
@@ -63,5 +63,5 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d $'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
||||
-d 'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
||||
${{ secrets.NTFY_URL }}
|
||||
|
@@ -61,7 +61,7 @@ jobs:
|
||||
run: |
|
||||
curl \
|
||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
||||
-H "Title: Site-Profile Image Released to Harbor: ${{ steps.meta.outputs.tags }}" \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d $'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
||||
-d 'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
||||
${{ secrets.NTFY_URL }}
|
||||
|
@@ -2,7 +2,7 @@ name: renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "@daily"
|
||||
- cron: '@daily'
|
||||
|
||||
push:
|
||||
branches:
|
||||
|
@@ -15,4 +15,3 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
FROM node:22.15.1-alpine3.20 AS base
|
||||
FROM node:22.16.0-alpine3.22 AS base
|
||||
|
||||
LABEL version="0.7.0"
|
||||
LABEL version="0.8.3"
|
||||
LABEL description="Astro based website to use as a personal site"
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
@@ -12,14 +12,15 @@ WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
FROM base AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --save form-data
|
||||
|
||||
FROM prod-deps AS build-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --save form-data
|
||||
|
||||
FROM build-deps AS build
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
RUN npm prune --production
|
||||
|
||||
FROM base AS runtime
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
import node from '@astrojs/node';
|
||||
|
||||
const getSiteURL = () => {
|
||||
if (process.env.SITE_URL) {
|
||||
return `https://${process.env.SITE_URL}`;
|
||||
@@ -11,8 +13,17 @@ const getSiteURL = () => {
|
||||
|
||||
export default defineConfig({
|
||||
site: getSiteURL(),
|
||||
integrations: [
|
||||
tailwind(),
|
||||
react(),
|
||||
],
|
||||
});
|
||||
integrations: [tailwindcss(), react()],
|
||||
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
})
|
||||
});
|
@@ -1,4 +1,4 @@
|
||||
import { createDirectus, rest, } from '@directus/sdk';
|
||||
import { createDirectus, rest } from '@directus/sdk';
|
||||
|
||||
type Global = {
|
||||
title: string;
|
||||
@@ -10,38 +10,38 @@ type Global = {
|
||||
portrait: string;
|
||||
portrait_alt: string;
|
||||
about: string;
|
||||
}
|
||||
};
|
||||
|
||||
type About = {
|
||||
background: string;
|
||||
experience: string;
|
||||
education: string;
|
||||
certifications: string;
|
||||
}
|
||||
};
|
||||
|
||||
type Links = {
|
||||
github: string;
|
||||
linkedin: string;
|
||||
}
|
||||
};
|
||||
|
||||
type Skill = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
level: string;
|
||||
}
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
level: string;
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
image: string;
|
||||
image_alt: string;
|
||||
published_date: Date;
|
||||
updated_date: Date;
|
||||
tags: string[];
|
||||
}
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
image: string;
|
||||
image_alt: string;
|
||||
published_date: Date;
|
||||
updated_date: Date;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
type Schema = {
|
||||
global: Global;
|
||||
@@ -49,8 +49,10 @@ type Schema = {
|
||||
links: Links;
|
||||
skills: Skill[];
|
||||
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;
|
||||
|
15
package.json
15
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "site-profile",
|
||||
"type": "module",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier . --write",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,22 +16,24 @@
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@directus/sdk": "^19.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"astro": "^5.9.1",
|
||||
"form-data": "4.0.3",
|
||||
"framer-motion": "^12.16.0",
|
||||
"postcss-preset-env": "^10.2.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"tailwindcss": "^3.0.24"
|
||||
"tailwindcss": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.12.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
||||
"prettier-plugin-astro": "^0.14.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
2064
pnpm-lock.yaml
generated
2064
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
'postcss-preset-env': {
|
||||
features: {
|
||||
|
10
renovate.json
Normal file
10
renovate.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", "mergeConfidence:all-badges", ":rebaseStalePrs"],
|
||||
"timezone": "US/Central",
|
||||
"schedule": ["* */1 * * *"],
|
||||
"labels": [],
|
||||
"prHourlyLimit": 0,
|
||||
"prConcurrentLimit": 0,
|
||||
"packageRules": []
|
||||
}
|
@@ -2,16 +2,29 @@
|
||||
// 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 -->
|
||||
<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 -->
|
||||
<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 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>
|
||||
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -19,20 +32,20 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeToggle = document.querySelector('[data-theme-toggle]');
|
||||
const overlay = document.getElementById('theme-transition-overlay');
|
||||
|
||||
|
||||
if (themeToggle && overlay) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
// Add transitioning class to optimize performance
|
||||
document.documentElement.classList.add('theme-transitioning');
|
||||
|
||||
|
||||
// Fade in overlay
|
||||
overlay.style.opacity = '0.15';
|
||||
overlay.style.transition = 'opacity 0.3s ease';
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
// Fade out overlay
|
||||
overlay.style.opacity = '0';
|
||||
|
||||
|
||||
// Remove transitioning class after animation completes
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('theme-transitioning');
|
||||
@@ -50,24 +63,27 @@
|
||||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
|
||||
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
|
||||
|
||||
/* Dark mode version */
|
||||
:global(.dark) .bg-grid-pattern {
|
||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
|
||||
/* Ambient glow animations */
|
||||
.animate-glow {
|
||||
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: 1s;
|
||||
}
|
||||
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
@@ -84,7 +100,7 @@
|
||||
transform: translate(-5%, 5%) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Theme transition overlay */
|
||||
#theme-transition-overlay {
|
||||
transition: opacity 0.3s ease;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
---
|
||||
import directus from "../../lib/directus"
|
||||
import { readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
const global = await directus.request(readSingleton("global"));
|
||||
const links = await directus.request(readSingleton("links"));
|
||||
const global = await directus.request(readSingleton('global'));
|
||||
const links = await directus.request(readSingleton('links'));
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
@@ -18,173 +18,227 @@ const socialLinks = [
|
||||
{
|
||||
name: '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',
|
||||
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">
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
<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="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>
|
||||
<footer
|
||||
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
||||
>
|
||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<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 class="relative pt-16 pb-12 px-4 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="relative px-4 pb-12 pt-16 sm:px-6">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<!-- 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 -->
|
||||
<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="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">
|
||||
<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>
|
||||
<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>
|
||||
<div
|
||||
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
|
||||
>
|
||||
<span
|
||||
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
||||
>{global.initals}</span
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100 theme-transition-color">Blog</span>
|
||||
<span
|
||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||
>Blog</span
|
||||
>
|
||||
</div>
|
||||
</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}
|
||||
</p>
|
||||
|
||||
<!-- Social links -->
|
||||
<div class="mt-6 flex items-center space-x-4">
|
||||
{socialLinks.map(social => (
|
||||
<a
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
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"
|
||||
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>
|
||||
<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">
|
||||
<Fragment set:html={social.icon} />
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
{
|
||||
socialLinks.map((social) => (
|
||||
<a
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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}
|
||||
>
|
||||
<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="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} />
|
||||
</svg>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick links -->
|
||||
<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">
|
||||
{navLinks.map(link => (
|
||||
<li>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<span class="relative overflow-hidden inline-block">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
navLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
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 inline-block overflow-hidden">
|
||||
<span class="relative z-10">{link.text}</span>
|
||||
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 theme-transition-all">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color">
|
||||
© {currentYear} All rights reserved.
|
||||
</p>
|
||||
<!-- Bottom section -->
|
||||
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
|
||||
© {currentYear} All rights reserved.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<a
|
||||
href="https://astro.build"
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-1 text-[#FF5D01] group-hover:animate-pulse" 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 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"/>
|
||||
</svg>
|
||||
<span class="relative">
|
||||
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>
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
||||
>Built with</span
|
||||
>
|
||||
<a
|
||||
href="https://astro.build"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
||||
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>
|
||||
<span class="relative">
|
||||
Astro
|
||||
<span
|
||||
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
|
||||
></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-transition-all {
|
||||
transition-property: background-color, border-color, color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.theme-transition-color {
|
||||
transition-property: color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.theme-transition-bg {
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-slow {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px) translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px) translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(10px) translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.animate-float-slow {
|
||||
animation: float-slow 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animation-delay-1000 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
</style>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.theme-transition-all {
|
||||
transition-property: background-color, border-color, color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.theme-transition-color {
|
||||
transition-property: color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.theme-transition-bg {
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-slow {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px) translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px) translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(10px) translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.animate-float-slow {
|
||||
animation: float-slow 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animation-delay-1000 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -8,12 +8,14 @@ const { date } = Astro.props;
|
||||
const parsedDate = typeof date === 'string' ? new Date(date) : date;
|
||||
---
|
||||
|
||||
{parsedDate && (
|
||||
<time datetime={parsedDate.toISOString()}>
|
||||
{parsedDate.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
{
|
||||
parsedDate && (
|
||||
<time datetime={parsedDate.toISOString()}>
|
||||
{parsedDate.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
const global = await directus.request(readSingleton("global"));
|
||||
const global = await directus.request(readSingleton('global'));
|
||||
|
||||
const navItems = [
|
||||
{ text: 'Home', href: '/' },
|
||||
@@ -15,68 +15,102 @@ const navItems = [
|
||||
];
|
||||
|
||||
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">
|
||||
<div class="max-w-3xl mx-auto px-4 flex items-center justify-between">
|
||||
<header
|
||||
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 -->
|
||||
<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 -->
|
||||
<nav class="hidden sm:flex items-center space-x-6">
|
||||
{navItems.map(item => {
|
||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`text-sm font-medium ${isActive
|
||||
? 'text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
<nav class="hidden items-center space-x-6 sm:flex">
|
||||
{
|
||||
navItems.map((item) => {
|
||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`text-sm font-medium ${
|
||||
isActive
|
||||
? 'text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button id="mobile-menu-button" class="sm:hidden flex items-center" 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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 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 class="flex justify-between items-center p-4 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">JD</a>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
<div
|
||||
id="mobile-menu"
|
||||
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"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 flex flex-col items-center justify-center space-y-6 text-center">
|
||||
{navItems.map((item, index) => {
|
||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`text-lg font-medium mobile-nav-item opacity-0 translate-y-4 ${isActive
|
||||
? 'text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`}
|
||||
style={`transition-delay: ${index * 0.05}s;`}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
<div class="pt-4 mobile-nav-item opacity-0 translate-y-4" style="transition-delay: 0.25s;">
|
||||
|
||||
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
|
||||
{
|
||||
navItems.map((item, index) => {
|
||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
||||
isActive
|
||||
? 'text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
||||
}`}
|
||||
style={`transition-delay: ${index * 0.05}s;`}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
@@ -92,44 +126,44 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
||||
const closeMenuButton = document.getElementById('close-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const navItems = document.querySelectorAll('.mobile-nav-item');
|
||||
|
||||
|
||||
// Open menu with animations
|
||||
mobileMenuButton?.addEventListener('click', () => {
|
||||
if (!mobileMenu) return;
|
||||
|
||||
|
||||
// Prevent body scrolling
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
|
||||
// Show menu with fade in
|
||||
mobileMenu.classList.remove('pointer-events-none');
|
||||
mobileMenu.classList.add('pointer-events-auto');
|
||||
|
||||
|
||||
// Animate opacity
|
||||
setTimeout(() => {
|
||||
mobileMenu.style.opacity = '1';
|
||||
|
||||
|
||||
// Animate each nav item with staggered delay
|
||||
navItems.forEach(item => {
|
||||
navItems.forEach((item) => {
|
||||
setTimeout(() => {
|
||||
item.classList.remove('opacity-0', 'translate-y-4');
|
||||
}, 150);
|
||||
});
|
||||
}, 50);
|
||||
});
|
||||
|
||||
|
||||
// Close menu with animations
|
||||
const closeMenu = () => {
|
||||
if (!mobileMenu) return;
|
||||
|
||||
|
||||
// Fade out nav items first
|
||||
navItems.forEach(item => {
|
||||
navItems.forEach((item) => {
|
||||
item.classList.add('opacity-0', 'translate-y-4');
|
||||
});
|
||||
|
||||
|
||||
// Then fade out the menu
|
||||
setTimeout(() => {
|
||||
mobileMenu.style.opacity = '0';
|
||||
|
||||
|
||||
// After animation completes, hide menu and restore scrolling
|
||||
setTimeout(() => {
|
||||
mobileMenu.classList.remove('pointer-events-auto');
|
||||
@@ -138,39 +172,39 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
||||
}, 300);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
|
||||
// Close button event
|
||||
closeMenuButton?.addEventListener('click', closeMenu);
|
||||
|
||||
|
||||
// Close menu when clicking a link
|
||||
const mobileLinks = mobileMenu?.querySelectorAll('a');
|
||||
mobileLinks?.forEach(link => {
|
||||
mobileLinks?.forEach((link) => {
|
||||
link.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
|
||||
// Close menu on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add smooth animation to header on scroll
|
||||
const header = document.querySelector('header');
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!header) return;
|
||||
|
||||
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
|
||||
// Add shadow on scroll
|
||||
if (currentScrollY > 10) {
|
||||
header.classList.add('shadow-sm');
|
||||
} else {
|
||||
header.classList.remove('shadow-sm');
|
||||
}
|
||||
|
||||
|
||||
// Update last scroll position
|
||||
lastScrollY = currentScrollY;
|
||||
});
|
||||
@@ -180,23 +214,29 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
||||
<style>
|
||||
/* Smooth animations for mobile navigation */
|
||||
.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: 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 {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
#mobile-menu-button:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
||||
/* Mobile menu transition */
|
||||
#mobile-menu {
|
||||
transition: opacity 0.3s ease;
|
||||
|
@@ -13,41 +13,89 @@ const encodedUrl = encodeURIComponent(url);
|
||||
<div class={`flex items-center gap-4 mt-8 ${className}`}>
|
||||
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<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
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<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
|
||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
<button
|
||||
id="copy-link-button"
|
||||
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"
|
||||
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>
|
||||
<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">
|
||||
<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="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!
|
||||
</span>
|
||||
</button>
|
||||
@@ -58,66 +106,69 @@ const encodedUrl = encodeURIComponent(url);
|
||||
// Function to handle copy link button
|
||||
function setupCopyLinkButton() {
|
||||
const copyButtons = document.querySelectorAll('#copy-link-button');
|
||||
|
||||
copyButtons.forEach(button => {
|
||||
|
||||
copyButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
// Get the current URL
|
||||
const url = window.location.href;
|
||||
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
// Show tooltip
|
||||
const tooltip = button.querySelector('#copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.classList.add('opacity-100');
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
tooltip.classList.remove('opacity-100');
|
||||
}, 2000);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
// Show tooltip
|
||||
const tooltip = button.querySelector('#copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.classList.add('opacity-100');
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
setTimeout(() => {
|
||||
tooltip.classList.remove('opacity-100');
|
||||
}, 2000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set up the copy link button when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
||||
|
||||
|
||||
// Also set up when the page content is updated via SPA navigation
|
||||
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
||||
|
||||
|
||||
// For compatibility with the custom page transition system
|
||||
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
||||
|
||||
|
||||
// Handle SPA transitions for share links
|
||||
function setupSpaTransitions() {
|
||||
// Get all share links
|
||||
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
||||
|
||||
|
||||
// Make sure external share links don't trigger page transitions
|
||||
shareLinks.forEach(link => {
|
||||
shareLinks.forEach((link) => {
|
||||
link.setAttribute('data-spa-external', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize SPA transitions
|
||||
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
||||
document.addEventListener('astro:page-load', setupSpaTransitions);
|
||||
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
||||
|
||||
|
||||
// Dispatch custom event when share action is completed
|
||||
function notifyShareComplete() {
|
||||
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
||||
}
|
||||
|
||||
|
||||
// Add analytics tracking for share actions if needed
|
||||
function trackShareAction(platform) {
|
||||
// You can implement analytics tracking here
|
||||
console.log(`Shared on ${platform}`);
|
||||
|
||||
|
||||
// Notify other components that share action is complete
|
||||
notifyShareComplete();
|
||||
}
|
||||
|
@@ -7,15 +7,17 @@ export interface Props {
|
||||
const { tags = [], class: className = '' } = Astro.props;
|
||||
---
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div class={`flex flex-wrap gap-2 mt-3 ${className}`}>
|
||||
{tags.map(tag => (
|
||||
<a
|
||||
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"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
tags.length > 0 && (
|
||||
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
|
||||
{tags.map((tag) => (
|
||||
<a
|
||||
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"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,65 +1,70 @@
|
||||
---
|
||||
---
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
---
|
||||
|
||||
<button
|
||||
id="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"
|
||||
>
|
||||
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
|
||||
<!-- Sun icon -->
|
||||
<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"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<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"/>
|
||||
<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>
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- Moon icon -->
|
||||
<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"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<script>
|
||||
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
||||
function setupThemeToggle() {
|
||||
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
||||
|
||||
|
||||
// Check for dark mode preference at the system level
|
||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
|
||||
// Check for saved theme preference or use the system preference
|
||||
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
||||
|
||||
|
||||
// Apply the theme on initial load
|
||||
if (currentTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
|
||||
// Create theme switch overlay element if it doesn't exist
|
||||
if (!document.querySelector('.theme-switch-overlay')) {
|
||||
const overlay = document.createElement('div');
|
||||
@@ -68,104 +73,119 @@
|
||||
overlay.style.transition = 'opacity 0.3s ease-out';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
|
||||
// Toggle theme when any theme toggle button is clicked
|
||||
themeToggles.forEach(toggle => {
|
||||
themeToggles.forEach((toggle) => {
|
||||
// Add event listeners for both click and touch events
|
||||
['click', 'touchend'].forEach(eventType => {
|
||||
toggle.addEventListener(eventType, (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get click/touch position for radial animation
|
||||
let x, y;
|
||||
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
|
||||
const rect = toggle.getBoundingClientRect();
|
||||
x = e.changedTouches[0].clientX - rect.left;
|
||||
y = e.changedTouches[0].clientY - rect.top;
|
||||
} else {
|
||||
const rect = toggle.getBoundingClientRect();
|
||||
x = e.clientX - rect.left;
|
||||
y = e.clientY - rect.top;
|
||||
}
|
||||
|
||||
// Set the position variables for the radial gradient
|
||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||
|
||||
// Get the overlay element
|
||||
const overlay = document.querySelector('.theme-switch-overlay');
|
||||
|
||||
// Determine the new theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
// Show overlay during transition
|
||||
if (overlay) {
|
||||
overlay.style.backgroundColor = newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
|
||||
overlay.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Add transition class
|
||||
document.documentElement.classList.add('theme-switching');
|
||||
|
||||
// Add ripple effect
|
||||
const ripple = document.createElement('span');
|
||||
ripple.className = 'theme-toggle-ripple';
|
||||
toggle.appendChild(ripple);
|
||||
|
||||
// Force a reflow to ensure all elements update
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Toggle dark mode with a slight delay to allow overlay to appear
|
||||
setTimeout(() => {
|
||||
if (isDark) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
['click', 'touchend'].forEach((eventType) => {
|
||||
toggle.addEventListener(
|
||||
eventType,
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get click/touch position for radial animation
|
||||
let x, y;
|
||||
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
|
||||
const rect = toggle.getBoundingClientRect();
|
||||
x = e.changedTouches[0].clientX - rect.left;
|
||||
y = e.changedTouches[0].clientY - rect.top;
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
const rect = toggle.getBoundingClientRect();
|
||||
x = e.clientX - rect.left;
|
||||
y = e.clientY - rect.top;
|
||||
}
|
||||
|
||||
// Store the preference
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Dispatch a custom event for other components to react to
|
||||
document.dispatchEvent(new CustomEvent('themeChanged', {
|
||||
detail: { isDark: newTheme === 'dark' }
|
||||
}));
|
||||
|
||||
// Force another reflow to ensure all elements update
|
||||
|
||||
// Set the position variables for the radial gradient
|
||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||
|
||||
// Get the overlay element
|
||||
const overlay = document.querySelector('.theme-switch-overlay');
|
||||
|
||||
// Determine the new theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
// Show overlay during transition
|
||||
if (overlay) {
|
||||
overlay.style.backgroundColor =
|
||||
newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
|
||||
overlay.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Add transition class
|
||||
document.documentElement.classList.add('theme-switching');
|
||||
|
||||
// Add ripple effect
|
||||
const ripple = document.createElement('span');
|
||||
ripple.className = 'theme-toggle-ripple';
|
||||
toggle.appendChild(ripple);
|
||||
|
||||
// Force a reflow to ensure all elements update
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Hide overlay after theme has changed
|
||||
|
||||
// Toggle dark mode with a slight delay to allow overlay to appear
|
||||
setTimeout(() => {
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
if (isDark) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
// Remove transition class after animation completes
|
||||
document.documentElement.classList.remove('theme-switching');
|
||||
ripple.remove();
|
||||
}, 300);
|
||||
}, 50);
|
||||
}, { passive: false });
|
||||
|
||||
// Store the preference
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Dispatch a custom event for other components to react to
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('themeChanged', {
|
||||
detail: { isDark: newTheme === 'dark' },
|
||||
})
|
||||
);
|
||||
|
||||
// Force another reflow to ensure all elements update
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Hide overlay after theme has changed
|
||||
setTimeout(() => {
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
}
|
||||
|
||||
// Remove transition class after animation completes
|
||||
document.documentElement.classList.remove('theme-switching');
|
||||
ripple.remove();
|
||||
}, 300);
|
||||
}, 50);
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Add touch feedback
|
||||
toggle.addEventListener('touchstart', () => {
|
||||
toggle.classList.add('active-touch');
|
||||
}, { passive: true });
|
||||
|
||||
toggle.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
toggle.classList.remove('active-touch');
|
||||
}, 150);
|
||||
}, { passive: true });
|
||||
toggle.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
toggle.classList.add('active-touch');
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
toggle.addEventListener(
|
||||
'touchend',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
toggle.classList.remove('active-touch');
|
||||
}, 150);
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Run setup on load
|
||||
document.addEventListener('DOMContentLoaded', setupThemeToggle);
|
||||
|
||||
|
||||
// Also run on page visibility change to ensure theme is consistent
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
@@ -177,7 +197,7 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Listen for system preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
@@ -193,9 +213,11 @@
|
||||
<style>
|
||||
/* Smooth transition for the entire page when theme changes */
|
||||
: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 */
|
||||
:global(.theme-switch-overlay) {
|
||||
position: fixed;
|
||||
@@ -204,13 +226,13 @@
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure theme transitions apply to all elements */
|
||||
:global(.theme-switching *) {
|
||||
transition-duration: 0.5s !important;
|
||||
transition-property: background-color, border-color, color, fill, stroke !important;
|
||||
}
|
||||
|
||||
|
||||
/* Ripple animation */
|
||||
.theme-toggle-ripple {
|
||||
position: absolute;
|
||||
@@ -223,7 +245,7 @@
|
||||
background-color: rgba(161, 161, 170, 0.3);
|
||||
animation: ripple 0.8s ease-out;
|
||||
}
|
||||
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
@@ -234,7 +256,7 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Subtle hover animation */
|
||||
#theme-toggle {
|
||||
transform: translateY(0);
|
||||
@@ -243,47 +265,49 @@
|
||||
min-height: 32px; /* Ensure minimum touch target size */
|
||||
min-width: 32px; /* Ensure minimum touch target size */
|
||||
}
|
||||
|
||||
|
||||
/* Only apply hover effects on non-touch devices */
|
||||
@media (hover: hover) {
|
||||
#theme-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
||||
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
||||
transform: scale(1.1) rotate(15deg);
|
||||
}
|
||||
|
||||
|
||||
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
||||
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
||||
transform: scale(1.1) rotate(-15deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Touch feedback */
|
||||
#theme-toggle.active-touch {
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
/* Optimize animations for mobile */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.icon-light, .icon-dark {
|
||||
.icon-light,
|
||||
.icon-dark {
|
||||
transition: all 0.2s ease-out !important;
|
||||
}
|
||||
|
||||
#theme-toggle, #theme-toggle:hover {
|
||||
|
||||
#theme-toggle,
|
||||
#theme-toggle:hover {
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
|
||||
.theme-toggle-ripple {
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Adjust size for very small screens */
|
||||
@media (max-width: 320px) {
|
||||
#theme-toggle {
|
||||
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference types="astro/content" />
|
||||
|
||||
|
@@ -1,16 +1,15 @@
|
||||
---
|
||||
import Layout from './Layout.astro';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
const global = await directus.request(readSingleton("global"));
|
||||
const global = await directus.request(readSingleton('global'));
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<Layout title={global.title} description={global.description}>
|
||||
|
@@ -1,16 +1,15 @@
|
||||
---
|
||||
import Layout from './Layout.astro';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
const global = await directus.request(readSingleton("global"));
|
||||
const global = await directus.request(readSingleton('global'));
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<Layout title={global.title} description={global.description}>
|
||||
@@ -26,7 +25,7 @@ export interface Props {
|
||||
document.documentElement.classList.add('theme-switching');
|
||||
|
||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
||||
rippleElements.forEach(el => {
|
||||
rippleElements.forEach((el) => {
|
||||
el.classList.add('ripple-active');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('ripple-active');
|
||||
@@ -35,8 +34,8 @@ export interface Props {
|
||||
|
||||
const event = new CustomEvent('themeChange', {
|
||||
detail: {
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||
}
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
@@ -47,8 +46,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
const socialLinks = document.querySelectorAll('.social-link');
|
||||
socialLinks.forEach(link => {
|
||||
|
||||
socialLinks.forEach((link) => {
|
||||
link.addEventListener('mouseenter', () => {
|
||||
link.classList.add('hover-active');
|
||||
});
|
||||
|
@@ -5,13 +5,15 @@ import ShareButtons from '../components/ShareButtons.astro';
|
||||
import TagList from '../components/TagList.astro';
|
||||
import './styles/markdown.css';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readItems } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await directus.request(readItems("posts", {
|
||||
fields: ['*'],
|
||||
}));
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
fields: ['*'],
|
||||
})
|
||||
);
|
||||
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
|
||||
}
|
||||
|
||||
@@ -23,58 +25,64 @@ try {
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
|
||||
} catch (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}>
|
||||
<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">
|
||||
<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}
|
||||
</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} />
|
||||
</div>
|
||||
|
||||
|
||||
<TagList tags={post.tags} class="mt-2" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Hero image -->
|
||||
{post.image && (
|
||||
<div class="relative mb-8 sm:mb-12 overflow-hidden rounded-xl shadow-lg">
|
||||
<div class="aspect-[16/9] w-full">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`}
|
||||
alt={post.image_alt}
|
||||
class="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
/>
|
||||
{
|
||||
post.image && (
|
||||
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
|
||||
<div class="aspect-[16/9] w-full">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
||||
alt={post.image_alt}
|
||||
class="h-full w-full object-cover"
|
||||
loading="eager"
|
||||
/>
|
||||
</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">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add the like button after the content -->
|
||||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
<ShareButtons url={canonicalURL.toString()} title={post.title} /> <!-- Convert URL to string -->
|
||||
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
||||
<!-- Convert URL to string -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post.updated_date && (
|
||||
<div class="mt-8 text-sm text-zinc-500 dark:text-zinc-400 italic">
|
||||
Last updated on <FormattedDate date={post.updated_date} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
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} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
|
||||
|
||||
<slot name="after-article" />
|
||||
</Layout>
|
||||
|
||||
@@ -85,37 +93,40 @@ try {
|
||||
const article = document.querySelector('article');
|
||||
if (article) {
|
||||
article.classList.add('article-entering');
|
||||
|
||||
|
||||
// Remove class after animation completes
|
||||
setTimeout(() => {
|
||||
article.classList.remove('article-entering');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
// Ensure consistent code block styling
|
||||
function updateCodeBlockStyles() {
|
||||
document.querySelectorAll('pre').forEach(pre => {
|
||||
document.querySelectorAll('pre').forEach((pre) => {
|
||||
// Force the background color with !important for both light and dark mode
|
||||
pre.setAttribute('style', 'background-color: #1e293b !important');
|
||||
|
||||
|
||||
// Also apply to any nested code elements
|
||||
const codeElements = pre.querySelectorAll('code');
|
||||
codeElements.forEach(code => {
|
||||
code.setAttribute('style', 'background-color: transparent !important; color: #e5e7eb !important;');
|
||||
codeElements.forEach((code) => {
|
||||
code.setAttribute(
|
||||
'style',
|
||||
'background-color: transparent !important; color: #e5e7eb !important;'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initial application
|
||||
updateCodeBlockStyles();
|
||||
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(() => {
|
||||
updateCodeBlockStyles();
|
||||
});
|
||||
|
||||
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
|
||||
// Also run on any content changes that might add new code blocks
|
||||
const contentObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
@@ -125,71 +136,74 @@ try {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
contentObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
|
||||
// Clean up observers when navigating away
|
||||
document.addEventListener('spa-navigation-start', () => {
|
||||
observer.disconnect();
|
||||
contentObserver.disconnect();
|
||||
});
|
||||
|
||||
|
||||
// Remove the parallax effect for hero image
|
||||
|
||||
|
||||
// Handle prev/next navigation links
|
||||
const navLinks = document.querySelectorAll('.blog-nav-link');
|
||||
navLinks.forEach(link => {
|
||||
navLinks.forEach((link) => {
|
||||
if (!link.hasAttribute('data-spa-handled')) {
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('mouseenter', () => {
|
||||
link.classList.add('nav-link-hover');
|
||||
});
|
||||
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
link.classList.remove('nav-link-hover');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Animate headings when they enter the viewport
|
||||
const animateHeadings = () => {
|
||||
const headings = document.querySelectorAll('article h2, article h3');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('heading-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.2,
|
||||
rootMargin: '0px 0px -100px 0px'
|
||||
});
|
||||
|
||||
headings.forEach(heading => {
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('heading-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.2,
|
||||
rootMargin: '0px 0px -100px 0px',
|
||||
}
|
||||
);
|
||||
|
||||
headings.forEach((heading) => {
|
||||
heading.classList.add('heading-animated');
|
||||
observer.observe(heading);
|
||||
});
|
||||
|
||||
|
||||
return observer;
|
||||
};
|
||||
|
||||
|
||||
// Initialize heading animations
|
||||
const headingObserver = animateHeadings();
|
||||
|
||||
// Enhance code blocks with syntax highlighting and copy button
|
||||
function enhanceCodeBlocks() {
|
||||
|
||||
// Enhance code blocks with syntax highlighting and copy button
|
||||
function enhanceCodeBlocks() {
|
||||
const codeBlocks = document.querySelectorAll('pre code');
|
||||
|
||||
codeBlocks.forEach(codeBlock => {
|
||||
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
// Skip if already processed
|
||||
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
||||
|
||||
|
||||
// Mark as enhanced
|
||||
codeBlock.parentElement.classList.add('enhanced');
|
||||
|
||||
|
||||
// Create copy button
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'copy-code-button';
|
||||
@@ -199,19 +213,19 @@ try {
|
||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
// Add copy functionality
|
||||
copyButton.addEventListener('click', () => {
|
||||
const code = codeBlock.textContent;
|
||||
navigator.clipboard.writeText(code);
|
||||
|
||||
|
||||
// Show copied feedback
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
@@ -221,40 +235,40 @@ try {
|
||||
`;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
|
||||
// Add copy button to pre element
|
||||
codeBlock.parentElement.appendChild(copyButton);
|
||||
|
||||
|
||||
// Fix line numbers implementation
|
||||
const codeText = codeBlock.textContent;
|
||||
const lines = codeText.split('\n');
|
||||
|
||||
|
||||
const lineNumbers = document.createElement('div');
|
||||
lineNumbers.className = 'line-numbers';
|
||||
|
||||
|
||||
// Always include all lines, including empty ones
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineNumber = document.createElement('span');
|
||||
lineNumber.textContent = i + 1;
|
||||
lineNumbers.appendChild(lineNumber);
|
||||
}
|
||||
|
||||
|
||||
codeBlock.parentElement.classList.add('with-line-numbers');
|
||||
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
||||
|
||||
|
||||
// Fix language label detection and display
|
||||
const className = codeBlock.className;
|
||||
const languageMatch = className.match(/language-(\w+)/);
|
||||
|
||||
|
||||
if (languageMatch && languageMatch[1]) {
|
||||
const language = languageMatch[1];
|
||||
|
||||
|
||||
// Add language label at top right
|
||||
const languageLabel = document.createElement('div');
|
||||
languageLabel.className = 'language-label';
|
||||
languageLabel.textContent = language;
|
||||
codeBlock.parentElement.appendChild(languageLabel);
|
||||
|
||||
|
||||
// Add language badge at bottom right with markdown syntax
|
||||
const languageBadge = document.createElement('div');
|
||||
languageBadge.className = 'language-badge';
|
||||
@@ -267,28 +281,29 @@ try {
|
||||
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
||||
languageBadge.style.color = '#e5e7eb';
|
||||
languageBadge.style.borderRadius = '0.25rem';
|
||||
languageBadge.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
languageBadge.style.fontFamily =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
languageBadge.style.zIndex = '10';
|
||||
codeBlock.parentElement.appendChild(languageBadge);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Enhance tables with better styling
|
||||
function enhanceTables() {
|
||||
const tables = document.querySelectorAll('.markdown-content table');
|
||||
|
||||
tables.forEach(table => {
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.classList.contains('enhanced-table')) return;
|
||||
|
||||
|
||||
table.classList.add('enhanced-table');
|
||||
|
||||
|
||||
// Wrap table in responsive container
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'table-container';
|
||||
table.parentNode.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
|
||||
|
||||
// Add zebra striping to rows
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach((row, index) => {
|
||||
@@ -300,16 +315,16 @@ try {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Enhance blockquotes with icons
|
||||
function enhanceBlockquotes() {
|
||||
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
||||
|
||||
blockquotes.forEach(blockquote => {
|
||||
|
||||
blockquotes.forEach((blockquote) => {
|
||||
if (blockquote.classList.contains('enhanced-quote')) return;
|
||||
|
||||
|
||||
blockquote.classList.add('enhanced-quote');
|
||||
|
||||
|
||||
// Add quote icon
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'quote-icon';
|
||||
@@ -318,16 +333,16 @@ try {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
blockquote.insertBefore(icon, blockquote.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Run all enhancements
|
||||
enhanceCodeBlocks();
|
||||
enhanceTables();
|
||||
enhanceBlockquotes();
|
||||
|
||||
|
||||
// Clean up observers when navigating away
|
||||
document.addEventListener('spa-navigation-start', () => {
|
||||
if (headingObserver) {
|
||||
@@ -335,16 +350,16 @@ try {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
||||
|
||||
|
||||
// Also initialize when SPA navigation completes
|
||||
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
||||
</script>
|
||||
@@ -353,19 +368,21 @@ try {
|
||||
/* Enhanced hero image styling */
|
||||
article img:first-of-type {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
article img:first-of-type:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
/* Article entrance animation */
|
||||
.article-entering {
|
||||
animation: article-fade-in 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
|
||||
@keyframes article-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -376,6 +393,6 @@ try {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Rest of the styles remain unchanged... */
|
||||
</style>
|
||||
|
@@ -5,14 +5,14 @@ import Background from '../components/Background.astro';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -21,33 +21,41 @@ const { title, description } = Astro.props;
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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 rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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"
|
||||
/>
|
||||
</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 -->
|
||||
<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>
|
||||
|
||||
|
||||
<!-- Background component with dot pattern and ambient glow -->
|
||||
<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 />
|
||||
<main class="py-12">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
|
||||
<script>
|
||||
// SPA transition system with history API
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
const mainContent = document.querySelector('main');
|
||||
|
||||
|
||||
// Initialize content with entrance animation
|
||||
if (mainContent) {
|
||||
mainContent.classList.add('content-entering');
|
||||
@@ -55,7 +63,7 @@ const { title, description } = Astro.props;
|
||||
mainContent.classList.remove('content-entering');
|
||||
}, 800);
|
||||
}
|
||||
|
||||
|
||||
// Function to load content via fetch
|
||||
async function loadContent(url) {
|
||||
try {
|
||||
@@ -64,52 +72,55 @@ const { title, description } = Astro.props;
|
||||
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
}
|
||||
|
||||
|
||||
// Fade out current content
|
||||
if (mainContent) {
|
||||
mainContent.style.opacity = '0';
|
||||
mainContent.style.transform = 'translateY(10px)';
|
||||
}
|
||||
|
||||
|
||||
// Fetch the new page content
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
||||
const html = await response.text();
|
||||
|
||||
|
||||
// Create a temporary element to parse the HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
|
||||
// Extract the main content
|
||||
const newContent = doc.querySelector('main');
|
||||
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
||||
|
||||
|
||||
// Extract the title
|
||||
const newTitle = doc.querySelector('title');
|
||||
if (newTitle) {
|
||||
document.title = newTitle.textContent;
|
||||
}
|
||||
|
||||
|
||||
// Extract meta description
|
||||
const newDescription = doc.querySelector('meta[name="description"]');
|
||||
if (newDescription) {
|
||||
const currentDescription = document.querySelector('meta[name="description"]');
|
||||
if (currentDescription) {
|
||||
currentDescription.setAttribute('content', newDescription.getAttribute('content') || '');
|
||||
currentDescription.setAttribute(
|
||||
'content',
|
||||
newDescription.getAttribute('content') || ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait a bit for transition effect
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Replace the content
|
||||
if (mainContent && newContent) {
|
||||
mainContent.innerHTML = newContent.innerHTML;
|
||||
|
||||
|
||||
// Run scripts in the new content
|
||||
Array.from(newContent.querySelectorAll('script')).forEach(oldScript => {
|
||||
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
|
||||
const newScript = document.createElement('script');
|
||||
Array.from(oldScript.attributes).forEach(attr => {
|
||||
Array.from(oldScript.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
newScript.textContent = oldScript.textContent;
|
||||
@@ -118,17 +129,17 @@ const { title, description } = Astro.props;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Fade in new content with animation
|
||||
if (mainContent) {
|
||||
mainContent.style.opacity = '0';
|
||||
mainContent.style.transform = 'translateY(10px)';
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||
mainContent.style.opacity = '1';
|
||||
mainContent.style.transform = 'translateY(0)';
|
||||
|
||||
|
||||
// Add entrance animation class
|
||||
mainContent.classList.add('content-entering');
|
||||
setTimeout(() => {
|
||||
@@ -136,7 +147,7 @@ const { title, description } = Astro.props;
|
||||
}, 800);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
|
||||
// Hide transition overlay
|
||||
if (pageTransition) {
|
||||
setTimeout(() => {
|
||||
@@ -144,33 +155,34 @@ const { title, description } = Astro.props;
|
||||
pageTransition.classList.remove('opacity-100');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
||||
// Dispatch custom event for content loaded
|
||||
document.dispatchEvent(new CustomEvent('spa-content-loaded', {
|
||||
detail: { url }
|
||||
}));
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('spa-content-loaded', {
|
||||
detail: { url },
|
||||
})
|
||||
);
|
||||
|
||||
// Scroll to top or to saved position
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
|
||||
// Re-attach event listeners to new content
|
||||
attachLinkListeners();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading content:', error);
|
||||
|
||||
|
||||
// Fallback to traditional navigation on error
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function to attach event listeners to all links
|
||||
function attachLinkListeners() {
|
||||
document.querySelectorAll('a').forEach(link => {
|
||||
document.querySelectorAll('a').forEach((link) => {
|
||||
// Skip links that are already handled, anchor links, external links, or have special attributes
|
||||
if (
|
||||
link.hasAttribute('data-spa-handled') ||
|
||||
!link.href.startsWith(window.location.origin) ||
|
||||
!link.href.startsWith(window.location.origin) ||
|
||||
link.href.includes('#') ||
|
||||
link.hasAttribute('target') ||
|
||||
link.hasAttribute('download') ||
|
||||
@@ -179,36 +191,36 @@ const { title, description } = Astro.props;
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.href;
|
||||
|
||||
|
||||
// Don't transition if clicking the current page
|
||||
if (targetHref === window.location.href) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update browser history
|
||||
window.history.pushState({ path: targetHref }, '', targetHref);
|
||||
|
||||
|
||||
// Load the new content
|
||||
loadContent(targetHref);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initial attachment of link listeners
|
||||
attachLinkListeners();
|
||||
|
||||
|
||||
// Handle browser back/forward navigation
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (e.state && e.state.path) {
|
||||
@@ -217,7 +229,7 @@ const { title, description } = Astro.props;
|
||||
loadContent(window.location.href);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check RSS feed availability
|
||||
const checkAndGenerateRSS = async () => {
|
||||
try {
|
||||
@@ -229,33 +241,36 @@ const { title, description } = Astro.props;
|
||||
console.warn('Could not check RSS feed status.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Check RSS feed availability
|
||||
checkAndGenerateRSS();
|
||||
});
|
||||
|
||||
|
||||
// Theme handling with transition effects
|
||||
function setupThemeHandling() {
|
||||
// Apply theme from localStorage or system preference
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
if (
|
||||
theme === 'dark' ||
|
||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
|
||||
// Listen for theme changes
|
||||
document.addEventListener('themeChanged', () => {
|
||||
// Add transition class to body
|
||||
document.body.classList.add('theme-transitioning');
|
||||
|
||||
|
||||
// Remove class after transition completes
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('theme-transitioning');
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize theme handling
|
||||
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
||||
</script>
|
||||
@@ -268,7 +283,7 @@ const { title, description } = Astro.props;
|
||||
transition: opacity 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
|
||||
/* Transition spinner animation */
|
||||
.transition-spinner {
|
||||
width: 30px;
|
||||
@@ -278,27 +293,31 @@ const { title, description } = Astro.props;
|
||||
border-top-color: #3b82f6;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
:global(.dark) .transition-spinner {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
border-top-color: #60a5fa;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Content entrance animation */
|
||||
main {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
transform 0.5s ease;
|
||||
}
|
||||
|
||||
|
||||
main.content-entering {
|
||||
animation: content-fade-in 0.6s ease forwards;
|
||||
}
|
||||
|
||||
|
||||
@keyframes content-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -309,7 +328,7 @@ const { title, description } = Astro.props;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Theme transition effect */
|
||||
body.theme-transitioning * {
|
||||
transition-duration: 0.3s !important;
|
||||
|
@@ -7,7 +7,7 @@ const { title, description } = Astro.props;
|
||||
|
||||
<BaseLayout title={title} description={description}>
|
||||
<ViewTransitions fallback="swap" />
|
||||
|
||||
|
||||
<div transition:animate="slide">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ const { title, description } = Astro.props;
|
||||
::view-transition-old(root) {
|
||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
||||
}
|
||||
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,54 +3,100 @@ import Layout from '../layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<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 -->
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<!-- Main content with animation -->
|
||||
<div class="relative z-10 max-w-xl mx-auto">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<h2 class="mt-6 text-2xl sm:text-3xl font-bold text-zinc-800 dark:text-zinc-200">Page Not Found</h2>
|
||||
|
||||
<p class="mt-6 text-zinc-600 dark:text-zinc-400 max-w-md mx-auto text-lg">
|
||||
<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>
|
||||
|
||||
<!-- Main content with animation -->
|
||||
<div class="relative z-10 mx-auto max-w-xl">
|
||||
<div class="glitch-wrapper">
|
||||
<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>
|
||||
|
||||
<h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl">
|
||||
Page Not Found
|
||||
</h2>
|
||||
|
||||
<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.
|
||||
</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
|
||||
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>
|
||||
<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">
|
||||
<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
|
||||
class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
||||
></span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
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>
|
||||
<span class="font-medium relative z-10">Return Home</span>
|
||||
<span class="relative z-10 font-medium">Return Home</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
<svg
|
||||
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>
|
||||
<span class="font-medium">Go Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Did you know?</h3>
|
||||
<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.
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,55 +108,57 @@ import Layout from '../layouts/Layout.astro';
|
||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||
window.history.back();
|
||||
});
|
||||
|
||||
|
||||
// Array of fun 404 facts
|
||||
const funFacts = [
|
||||
"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.",
|
||||
"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.",
|
||||
"Many companies use creative 404 pages as a way to showcase their brand personality and humor.",
|
||||
'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.',
|
||||
"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%.",
|
||||
"The most common cause of 404 errors is mistyped URLs."
|
||||
'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.',
|
||||
];
|
||||
|
||||
|
||||
// Display a random fun fact
|
||||
const funFactElement = document.getElementById('fun-fact');
|
||||
if (funFactElement) {
|
||||
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
|
||||
funFactElement.textContent = randomFact;
|
||||
}
|
||||
|
||||
|
||||
// Handle SPA transitions for 404 page
|
||||
function setupSPATransitions() {
|
||||
// Handle all internal links for SPA transitions
|
||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||
// Skip links that are anchor links, external links, or already processed
|
||||
if (link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')) {
|
||||
if (
|
||||
link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.getAttribute('href');
|
||||
|
||||
|
||||
// Trigger page transition animation
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
if (pageTransition) {
|
||||
pageTransition.classList.remove('opacity-0');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
|
||||
|
||||
// Navigate after transition effect
|
||||
setTimeout(() => {
|
||||
window.location.href = targetHref;
|
||||
@@ -121,7 +169,7 @@ import Layout from '../layouts/Layout.astro';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Re-initialize back button after SPA navigation
|
||||
const backButton = document.getElementById('back-button');
|
||||
if (backButton) {
|
||||
@@ -130,13 +178,13 @@ import Layout from '../layouts/Layout.astro';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||
</script>
|
||||
@@ -157,31 +205,31 @@ import Layout from '../layouts/Layout.astro';
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
|
||||
/* Glitch effect for 404 text */
|
||||
.glitch-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.glitch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
animation: glitch-skew 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
|
||||
.glitch::before,
|
||||
.glitch::after {
|
||||
content: attr(data-text);
|
||||
@@ -191,20 +239,22 @@ import Layout from '../layouts/Layout.astro';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.glitch::before {
|
||||
left: 2px;
|
||||
text-shadow: -2px 0 #ff00c1;
|
||||
clip: rect(44px, 450px, 56px, 0);
|
||||
animation: glitch-anim 5s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
|
||||
.glitch::after {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@keyframes glitch-anim {
|
||||
0% {
|
||||
clip: rect(31px, 9999px, 94px, 0);
|
||||
@@ -291,7 +341,7 @@ import Layout from '../layouts/Layout.astro';
|
||||
transform: skew(0.6deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes glitch-anim2 {
|
||||
0% {
|
||||
clip: rect(65px, 9999px, 65px, 0);
|
||||
@@ -378,7 +428,7 @@ import Layout from '../layouts/Layout.astro';
|
||||
transform: skew(0.74deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes glitch-skew {
|
||||
0% {
|
||||
transform: skew(-1deg);
|
||||
@@ -414,4 +464,4 @@ import Layout from '../layouts/Layout.astro';
|
||||
transform: skew(0deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@@ -3,54 +3,73 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
||||
import { SiTypescript, SiAstro } from 'react-icons/si';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readSingleton, readItems } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readSingleton, readItems } from '@directus/sdk';
|
||||
|
||||
const global = await directus.request(readSingleton("global"));
|
||||
const about = await directus.request(readSingleton("about"));
|
||||
const global = await directus.request(readSingleton('global'));
|
||||
const about = await directus.request(readSingleton('about'));
|
||||
|
||||
const skills = await directus.request(
|
||||
readItems("skills", {
|
||||
fields: ['*']
|
||||
readItems('skills', {
|
||||
fields: ['*'],
|
||||
})
|
||||
);
|
||||
|
||||
---
|
||||
|
||||
<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 -->
|
||||
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
||||
<!-- 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 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>
|
||||
<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="order-2 md:order-1 text-center 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">
|
||||
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>
|
||||
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
||||
<div class="order-2 text-center md:order-1 md:text-left">
|
||||
<h1
|
||||
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>
|
||||
|
||||
<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}
|
||||
</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 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-1 md:order-2 relative">
|
||||
<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="relative order-1 md:order-2">
|
||||
<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
|
||||
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
||||
alt={global.portrait_alt}
|
||||
class="w-full h-full object-cover"
|
||||
class="h-full w-full object-cover"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,87 +77,131 @@ const skills = await directus.request(
|
||||
</div>
|
||||
|
||||
<!-- About Section -->
|
||||
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<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">
|
||||
<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>
|
||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<h2
|
||||
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 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
|
||||
<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>
|
||||
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none theme-transition-all">
|
||||
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
|
||||
<div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert">
|
||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
||||
{about.experience}
|
||||
</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}
|
||||
</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}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Section -->
|
||||
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
|
||||
<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>
|
||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
||||
<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">
|
||||
<!-- Main slider container -->
|
||||
<div class="slider-track flex animate-slide">
|
||||
{ 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">
|
||||
<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 class="slider-track animate-slide flex">
|
||||
{
|
||||
skills.map((skill, index) => (
|
||||
<div
|
||||
key={`${skill.title}-${index}`}
|
||||
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]"
|
||||
>
|
||||
<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>
|
||||
<h3 class="text-base sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 theme-transition-color">{skill.title}</h3>
|
||||
<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>
|
||||
<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
|
||||
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"
|
||||
style={`width: ${skill.level}%`}
|
||||
></div>
|
||||
</div>
|
||||
<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="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">
|
||||
<span>Beginner</span>
|
||||
<span>Advanced</span>
|
||||
<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>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- 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 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>
|
||||
<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>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<div class="max-w-3xl mx-auto text-center theme-transition-all">
|
||||
<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>
|
||||
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 theme-transition-color">
|
||||
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.
|
||||
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
||||
<h2
|
||||
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl"
|
||||
>
|
||||
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>
|
||||
|
||||
<a
|
||||
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">
|
||||
<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" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
Say Hello
|
||||
</a>
|
||||
@@ -157,7 +220,8 @@ const skills = await directus.request(
|
||||
}
|
||||
|
||||
@keyframes blob-bounce {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
@@ -218,7 +282,9 @@ const skills = await directus.request(
|
||||
/* Reduce animation complexity on mobile for better performance */
|
||||
@media (max-width: 640px) {
|
||||
.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 {
|
||||
@@ -234,7 +300,11 @@ const skills = await directus.request(
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
pointer-events: none;
|
||||
@@ -256,7 +326,7 @@ const skills = await directus.request(
|
||||
left: -100%;
|
||||
width: 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;
|
||||
}
|
||||
|
||||
@@ -271,7 +341,8 @@ const skills = await directus.request(
|
||||
|
||||
/* Improved touch targets for mobile */
|
||||
@media (max-width: 640px) {
|
||||
a, button {
|
||||
a,
|
||||
button {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -290,11 +361,12 @@ const skills = await directus.request(
|
||||
|
||||
/* Smooth card transition during theme switch */
|
||||
.skill-card.theme-transition-element {
|
||||
transition: background-color var(--theme-transition),
|
||||
border-color var(--theme-transition),
|
||||
color var(--theme-transition),
|
||||
box-shadow var(--theme-transition),
|
||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
transition:
|
||||
background-color var(--theme-transition),
|
||||
border-color var(--theme-transition),
|
||||
color var(--theme-transition),
|
||||
box-shadow var(--theme-transition),
|
||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -350,7 +422,7 @@ const skills = await directus.request(
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
|
||||
if (!isTouchDevice) {
|
||||
cards.forEach(card => {
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener('mousemove', (e) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
@@ -380,7 +452,7 @@ const skills = await directus.request(
|
||||
});
|
||||
} else {
|
||||
// Simpler effects for touch devices
|
||||
cards.forEach(card => {
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener('touchstart', () => {
|
||||
card.classList.add('is-touched');
|
||||
});
|
||||
@@ -413,11 +485,13 @@ const skills = await directus.request(
|
||||
// Handle SPA transitions for about page
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')) {
|
||||
if (
|
||||
link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -455,9 +529,12 @@ const skills = await directus.request(
|
||||
// Animate hero section elements
|
||||
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
||||
heroElements.forEach((el, index) => {
|
||||
setTimeout(() => {
|
||||
el.classList.add('animate-reveal');
|
||||
}, 100 + (index * 150));
|
||||
setTimeout(
|
||||
() => {
|
||||
el.classList.add('animate-reveal');
|
||||
},
|
||||
100 + index * 150
|
||||
);
|
||||
});
|
||||
|
||||
// Animate profile image
|
||||
@@ -471,17 +548,23 @@ const skills = await directus.request(
|
||||
// Animate skill bars with staggered delay
|
||||
const skillBars = document.querySelectorAll('.skill-bar');
|
||||
skillBars.forEach((bar, index) => {
|
||||
setTimeout(() => {
|
||||
bar.classList.add('animate-skill');
|
||||
}, 500 + (index * 100));
|
||||
setTimeout(
|
||||
() => {
|
||||
bar.classList.add('animate-skill');
|
||||
},
|
||||
500 + index * 100
|
||||
);
|
||||
});
|
||||
|
||||
// Animate sections with staggered delay
|
||||
const sections = document.querySelectorAll('section');
|
||||
sections.forEach((section, index) => {
|
||||
setTimeout(() => {
|
||||
section.classList.add('animate-reveal');
|
||||
}, 300 + (index * 200));
|
||||
setTimeout(
|
||||
() => {
|
||||
section.classList.add('animate-reveal');
|
||||
},
|
||||
300 + index * 200
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,97 +1,144 @@
|
||||
---
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
|
||||
import directus from "../../../lib/directus"
|
||||
import { readItems } from "@directus/sdk";
|
||||
import directus from '../../../lib/directus';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await directus.request(readItems("posts", {
|
||||
fields: ['*'],
|
||||
}));
|
||||
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
fields: ['*'],
|
||||
})
|
||||
);
|
||||
|
||||
const sortedEntries = [...posts].sort(
|
||||
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
||||
);
|
||||
|
||||
|
||||
return sortedEntries.map((post, index) => {
|
||||
return {
|
||||
params: { slug: post.slug },
|
||||
props: {
|
||||
props: {
|
||||
post,
|
||||
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;
|
||||
|
||||
---
|
||||
|
||||
<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 -->
|
||||
<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 set:html={post.content} />
|
||||
</div>
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
{prevPost && (
|
||||
<a
|
||||
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"
|
||||
<!-- Next/Previous Navigation - Improved responsive design -->
|
||||
<div
|
||||
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
|
||||
href={`/blog/${prevPost.slug}`}
|
||||
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>
|
||||
<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">
|
||||
<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">
|
||||
<path d="m15 18-6-6 6-6"></path>
|
||||
<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 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="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>
|
||||
Previous Article
|
||||
</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}
|
||||
</h3>
|
||||
</a>
|
||||
)}
|
||||
{nextPost && (
|
||||
<a
|
||||
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"
|
||||
)
|
||||
}
|
||||
{
|
||||
nextPost && (
|
||||
<a
|
||||
href={`/blog/${nextPost.slug}`}
|
||||
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>
|
||||
<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">
|
||||
<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 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
|
||||
<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">
|
||||
<path d="m9 18 6-6-6-6"></path>
|
||||
<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="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>
|
||||
</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}
|
||||
</h3>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BlogPost>
|
||||
|
||||
<script>
|
||||
// Removing TOC-related functions
|
||||
|
||||
|
||||
// Add copy buttons to code blocks
|
||||
function initializeCodeCopyButtons() {
|
||||
const codeBlocks = document.querySelectorAll('pre');
|
||||
|
||||
codeBlocks.forEach(block => {
|
||||
|
||||
codeBlocks.forEach((block) => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
block.classList.add('code-block-processed');
|
||||
|
||||
|
||||
// Create wrapper if not already wrapped
|
||||
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;
|
||||
} else {
|
||||
wrapper = document.createElement('div');
|
||||
@@ -99,28 +146,29 @@ const { post, nextPost, prevPost } = Astro.props;
|
||||
block.parentNode.insertBefore(wrapper, block);
|
||||
wrapper.appendChild(block);
|
||||
}
|
||||
|
||||
|
||||
// Add copy button if not already present
|
||||
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-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 = `
|
||||
<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" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
copyButton.addEventListener('click', () => {
|
||||
const code = block.querySelector('code').innerText;
|
||||
navigator.clipboard.writeText(code);
|
||||
|
||||
|
||||
// Show copied feedback
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
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">
|
||||
@@ -129,39 +177,39 @@ const { post, nextPost, prevPost } = Astro.props;
|
||||
`;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
|
||||
wrapper.appendChild(copyButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Handle SPA transitions for blog post navigation
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.getAttribute('href');
|
||||
|
||||
|
||||
// Trigger page transition animation
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
if (pageTransition) {
|
||||
pageTransition.classList.remove('opacity-0');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
|
||||
|
||||
// Navigate after transition effect
|
||||
setTimeout(() => {
|
||||
window.location.href = targetHref;
|
||||
@@ -173,13 +221,13 @@ const { post, nextPost, prevPost } = Astro.props;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Main initialization function
|
||||
function initializeBlogPost() {
|
||||
// Initialize remaining components
|
||||
initializeCodeCopyButtons();
|
||||
setupSPATransitions();
|
||||
|
||||
|
||||
// Scroll to hash if present in URL
|
||||
if (window.location.hash) {
|
||||
setTimeout(() => {
|
||||
@@ -190,100 +238,105 @@ const { post, nextPost, prevPost } = Astro.props;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', initializeBlogPost);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', initializeBlogPost);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Removing TOC-related styles */
|
||||
|
||||
|
||||
/* Language badge styling */
|
||||
.language-badge {
|
||||
font-family: 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;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
|
||||
/* Extra small screens */
|
||||
@media (min-width: 480px) {
|
||||
.xs\:inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
.xs\:hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Enhanced typography for blog content - Responsive adjustments */
|
||||
.prose {
|
||||
@apply text-zinc-800 dark:text-zinc-200;
|
||||
@reference text-zinc-800 dark:text-zinc-200;
|
||||
}
|
||||
|
||||
.prose h1, .prose h2, .prose h3, .prose h4 {
|
||||
@apply text-zinc-900 dark:text-zinc-100 font-semibold;
|
||||
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
@reference font-semibold text-zinc-900 dark:text-zinc-100;
|
||||
}
|
||||
|
||||
|
||||
.prose h1 {
|
||||
@apply text-2xl sm:text-3xl md:text-4xl;
|
||||
@reference text-2xl sm:text-3xl md:text-4xl;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
@reference 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 {
|
||||
@apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3;
|
||||
@reference mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
|
||||
}
|
||||
|
||||
|
||||
.prose p {
|
||||
@apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base;
|
||||
@reference mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
@reference 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 {
|
||||
@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;
|
||||
@reference 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 {
|
||||
@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;
|
||||
@reference 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 {
|
||||
@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;
|
||||
@reference 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 {
|
||||
@apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
||||
@reference bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.prose img {
|
||||
@apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto;
|
||||
@reference mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
|
||||
}
|
||||
|
||||
.prose ul, .prose ol {
|
||||
@apply my-4 sm:my-6 pl-5 sm:pl-6;
|
||||
|
||||
.prose ul,
|
||||
.prose ol {
|
||||
@reference my-4 pl-5 sm:my-6 sm:pl-6;
|
||||
}
|
||||
|
||||
|
||||
.prose li {
|
||||
@apply mb-1 sm:mb-2 text-sm sm:text-base;
|
||||
@reference mb-1 text-sm sm:mb-2 sm:text-base;
|
||||
}
|
||||
|
||||
|
||||
.prose hr {
|
||||
@apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800;
|
||||
@reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
|
||||
}
|
||||
|
||||
|
||||
/* Line clamp for truncating text */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
@@ -291,4 +344,4 @@ const { post, nextPost, prevPost } = Astro.props;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@@ -1,19 +1,17 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
import directus from "../../../lib/directus"
|
||||
import { readItems } from "@directus/sdk";
|
||||
import directus from '../../../lib/directus';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
const posts = await directus.request(
|
||||
readItems("posts", {
|
||||
readItems('posts', {
|
||||
fields: ['*'],
|
||||
sort: ["-published_date"],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const sortedPosts = posts.sort(
|
||||
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
||||
);
|
||||
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
|
||||
|
||||
// Group posts by year for timeline effect
|
||||
const postsByYear = sortedPosts.reduce((acc, post) => {
|
||||
@@ -29,176 +27,206 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
|
||||
const totalPosts = sortedPosts.length;
|
||||
|
||||
// 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">
|
||||
<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 -->
|
||||
<div class="relative mb-12 sm:mb-20">
|
||||
<!-- 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 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>
|
||||
|
||||
<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">
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 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) -->
|
||||
{sortedPosts.length > 0 && (
|
||||
<div class="md:col-span-12 mb-8 sm:mb-12">
|
||||
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8">
|
||||
<div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8">
|
||||
{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">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`}
|
||||
alt={sortedPosts[0].title}
|
||||
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex-1 flex 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">
|
||||
<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>
|
||||
{sortedPosts[0].published_date && (
|
||||
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
|
||||
{sortedPosts[0].published_date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 class="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">
|
||||
<a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0">
|
||||
{sortedPosts[0].title}
|
||||
</a>
|
||||
</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">
|
||||
{sortedPosts[0].description}
|
||||
</p>
|
||||
|
||||
<!-- Improved mobile layout for featured post metadata -->
|
||||
<div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">
|
||||
{sortedPosts[0].tags && (
|
||||
<div class="flex flex-wrap gap-2 justify-center md:justify-start">
|
||||
{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">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Improved sidebar for mobile -->
|
||||
<div class="md:col-span-3 relative">
|
||||
<div class="md:sticky md:top-24 space-y-4 mb-8 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>
|
||||
|
||||
<!-- 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">
|
||||
{years.map((year, index) => (
|
||||
<a
|
||||
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' : ''}`}
|
||||
>
|
||||
<span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span>
|
||||
<span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Improved post grid for mobile -->
|
||||
<div class="md:col-span-9">
|
||||
{years.map((year) => (
|
||||
<div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16">
|
||||
<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">
|
||||
{year}
|
||||
</h2>
|
||||
|
||||
<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) => (
|
||||
<article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md">
|
||||
{post.image && (
|
||||
<div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
|
||||
alt={post.title}
|
||||
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex-1 flex 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">
|
||||
{post.pubDate && (
|
||||
<time datetime={post.published_date.toLocaleString()} class="flex items-center">
|
||||
{post.published_date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</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">
|
||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
||||
{post.title}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
{post.tags && (
|
||||
<div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start">
|
||||
{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">
|
||||
{
|
||||
sortedPosts.length > 0 && (
|
||||
<div class="mb-8 sm:mb-12 md:col-span-12">
|
||||
<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 && (
|
||||
<div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
|
||||
alt={sortedPosts[0].title}
|
||||
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 flex-col justify-center">
|
||||
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 sm:text-sm md:justify-start">
|
||||
<span class="font-medium uppercase tracking-wider">Featured</span>
|
||||
<span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
|
||||
{sortedPosts[0].published_date && (
|
||||
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
|
||||
{sortedPosts[0].published_date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 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"
|
||||
>
|
||||
{sortedPosts[0].title}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<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}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
|
||||
{sortedPosts[0].tags && (
|
||||
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
|
||||
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
|
||||
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{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">
|
||||
+{post.tags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Improved sidebar for mobile -->
|
||||
<div class="relative md:col-span-3">
|
||||
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
|
||||
<h3
|
||||
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 -->
|
||||
<div
|
||||
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
|
||||
>
|
||||
{
|
||||
years.map((year, index) => (
|
||||
<a
|
||||
href={`#year-${year}`}
|
||||
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 font-medium text-zinc-900 dark:text-zinc-100 md:text-lg">
|
||||
{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' : ''}
|
||||
</span>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Improved post grid for mobile -->
|
||||
<div class="md:col-span-9">
|
||||
{
|
||||
years.map((year) => (
|
||||
<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}
|
||||
</h2>
|
||||
|
||||
<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) => (
|
||||
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
|
||||
{post.image && (
|
||||
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
||||
alt={post.title}
|
||||
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 flex-col">
|
||||
<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 && (
|
||||
<time
|
||||
datetime={post.published_date.toLocaleString()}
|
||||
class="flex items-center"
|
||||
>
|
||||
{post.published_date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{post.title}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<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}
|
||||
</p>
|
||||
|
||||
{post.tags && (
|
||||
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
|
||||
{post.tags.slice(0, 2).map((tag) => (
|
||||
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{post.tags.length > 2 && (
|
||||
<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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,13 +237,14 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
.animate-blob {
|
||||
animation: blob-bounce 8s infinite ease;
|
||||
}
|
||||
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
|
||||
@keyframes blob-bounce {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
@@ -228,36 +257,37 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
transform: translate(-5%, 5%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Search container hover effect */
|
||||
.search-container:hover .search-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Input focus animation */
|
||||
input:focus + div .search-pulse {
|
||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Line clamp for descriptions */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
@@ -265,17 +295,18 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* Improved touch targets for mobile */
|
||||
@media (max-width: 640px) {
|
||||
a, button {
|
||||
a,
|
||||
button {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -287,7 +318,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
// Script không thay đổi - giữ nguyên chức năng
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const backToTopButton = document.getElementById('back-to-top');
|
||||
|
||||
|
||||
if (backToTopButton) {
|
||||
// Show button when scrolled down
|
||||
const toggleBackToTopButton = () => {
|
||||
@@ -299,50 +330,50 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
backToTopButton.classList.add('opacity-0', 'invisible');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Scroll to top when clicked
|
||||
backToTopButton.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Check scroll position
|
||||
window.addEventListener('scroll', toggleBackToTopButton);
|
||||
toggleBackToTopButton(); // Initial check
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
|
||||
if (targetElement) {
|
||||
window.scrollTo({
|
||||
top: targetElement.offsetTop - 100,
|
||||
behavior: 'smooth'
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
|
||||
// Update URL hash without jumping
|
||||
history.pushState(null, null, targetId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Add touch support for hover effects
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
|
||||
if (isTouchDevice) {
|
||||
const articles = document.querySelectorAll('article');
|
||||
|
||||
articles.forEach(article => {
|
||||
|
||||
articles.forEach((article) => {
|
||||
article.addEventListener('touchstart', () => {
|
||||
article.classList.add('is-touched');
|
||||
});
|
||||
|
||||
|
||||
article.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
article.classList.remove('is-touched');
|
||||
@@ -351,34 +382,34 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// SPA transition handling
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.getAttribute('href');
|
||||
|
||||
|
||||
// Trigger page transition animation
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
if (pageTransition) {
|
||||
pageTransition.classList.remove('opacity-0');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
|
||||
|
||||
// Navigate after transition effect
|
||||
setTimeout(() => {
|
||||
window.location.href = targetHref;
|
||||
@@ -389,19 +420,19 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||
</script>
|
||||
</script>
|
||||
|
@@ -2,15 +2,15 @@
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readItems,readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
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(
|
||||
readItems("posts", {
|
||||
readItems('posts', {
|
||||
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())
|
||||
.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}`>
|
||||
<!-- Hero Section with improved mobile responsiveness -->
|
||||
<section class="py-10 sm:py-16 md:py-20 px-4 sm:px-6 theme-transition-all">
|
||||
<div class="max-w-2xl mx-auto relative">
|
||||
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
|
||||
<div class="relative mx-auto max-w-2xl">
|
||||
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
||||
<div class="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 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>
|
||||
|
||||
<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">
|
||||
<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 mt-1">development, and</span>
|
||||
<span class="block mt-1 relative">
|
||||
<span class="mt-1 block">development, and</span>
|
||||
<span class="relative mt-1 block">
|
||||
<span class="relative inline-block">
|
||||
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>
|
||||
</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}
|
||||
</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">
|
||||
<a
|
||||
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]"
|
||||
<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
|
||||
href="/about"
|
||||
class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
||||
>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
<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="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,139 +86,194 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
</section>
|
||||
|
||||
<!-- 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">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<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>
|
||||
<a
|
||||
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"
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
href="/blog"
|
||||
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300 sm:self-auto"
|
||||
>
|
||||
<span class="flex items-center gap-1">
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
<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="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 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">
|
||||
{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">
|
||||
<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>
|
||||
|
||||
{post.image && (
|
||||
<div class="relative z-10 w-full aspect-video mb-4 overflow-hidden rounded-lg">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
|
||||
alt={post.title}
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading={index === 0 ? "eager" : "lazy"}
|
||||
width="400"
|
||||
height="225"
|
||||
/>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
||||
{
|
||||
recentPosts.map((post, index) => (
|
||||
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
||||
<div class="theme-transition-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 && (
|
||||
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
||||
alt={post.title}
|
||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
width="400"
|
||||
height="225"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 sm:justify-start sm:gap-x-4">
|
||||
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
||||
<FormattedDate date={post.published_date} />
|
||||
</time>
|
||||
</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">
|
||||
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
||||
<FormattedDate date={post.published_date} />
|
||||
</time>
|
||||
</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">
|
||||
<a href={`/blog/${post.slug}`} class="min-h-[44px] flex 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"></span>
|
||||
{post.title}
|
||||
</a>
|
||||
</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">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
{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">
|
||||
{post.tags.slice(0, 3).map(tag => (
|
||||
<a
|
||||
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]"
|
||||
>
|
||||
#{tag}
|
||||
</a>
|
||||
))}
|
||||
{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]">
|
||||
+{post.tags.length - 3} more
|
||||
|
||||
<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="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}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<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}
|
||||
</p>
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
|
||||
{post.tags.slice(0, 3).map((tag) => (
|
||||
<a
|
||||
href={`/topics/${tag}`}
|
||||
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 sm:px-3"
|
||||
>
|
||||
#{tag}
|
||||
</a>
|
||||
))}
|
||||
{post.tags.length > 3 && (
|
||||
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||
+{post.tags.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100 sm:mx-0 sm:mt-4"
|
||||
>
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
||||
Read article
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<a
|
||||
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]"
|
||||
>
|
||||
<span class="relative overflow-hidden inline-block">
|
||||
<span class="group-hover:-translate-y-full block transition-transform duration-300">Read article</span>
|
||||
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">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"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
||||
Explore now
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 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>
|
||||
</a>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<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>
|
||||
|
||||
<!-- Improved grid layout for mobile -->
|
||||
<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 => {
|
||||
const tagCount = posts.filter(post => post.tags && post.tags.includes(tag)).length;
|
||||
return (
|
||||
<a
|
||||
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]"
|
||||
{
|
||||
allTags.length > 0 && (
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
||||
{allTags.map((tag) => {
|
||||
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
||||
return (
|
||||
<a
|
||||
href={`/topics/${tag}`}
|
||||
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/70 sm:min-h-[90px] sm:p-4 md:p-6"
|
||||
>
|
||||
<div class="mb-2 flex items-start justify-between">
|
||||
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
#{tag}
|
||||
</span>
|
||||
<span class="theme-transition-all 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'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
Explore articles about {tag}
|
||||
</p>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center sm:mt-8">
|
||||
<a
|
||||
href="/tags"
|
||||
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
||||
>
|
||||
<span>View all topics</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100 theme-transition-color mr-2">#{tag}</span>
|
||||
<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">
|
||||
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1 theme-transition-color">
|
||||
Explore articles about {tag}
|
||||
</p>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-8 text-center">
|
||||
<a
|
||||
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]"
|
||||
>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
@@ -201,69 +281,69 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if it's a touch device
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
|
||||
if (isTouchDevice) {
|
||||
const cards = document.querySelectorAll('.hover-3d');
|
||||
|
||||
cards.forEach(card => {
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener('touchstart', () => {
|
||||
card.classList.add('is-touched');
|
||||
});
|
||||
|
||||
|
||||
card.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
card.classList.remove('is-touched');
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Disable hover animations on touch devices for better performance
|
||||
document.documentElement.classList.add('touch-device');
|
||||
}
|
||||
|
||||
|
||||
// Improved viewport height fix for mobile browsers
|
||||
const setVh = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
|
||||
// Set initial value
|
||||
setVh();
|
||||
|
||||
|
||||
// Update on resize and scroll to prevent content shifting
|
||||
window.addEventListener('resize', setVh);
|
||||
|
||||
|
||||
// Use a debounced scroll handler to prevent performance issues
|
||||
let scrollTimeout;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (scrollTimeout) {
|
||||
window.cancelAnimationFrame(scrollTimeout);
|
||||
}
|
||||
|
||||
|
||||
scrollTimeout = window.requestAnimationFrame(() => {
|
||||
// Lock width during scroll
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.overflowX = 'hidden';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Fix for iOS Safari address bar height changes
|
||||
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
|
||||
// Force the layout to use the initial viewport size
|
||||
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
|
||||
|
||||
|
||||
// Apply fixed height to sections to prevent resizing
|
||||
const sections = document.querySelectorAll('section');
|
||||
sections.forEach(section => {
|
||||
sections.forEach((section) => {
|
||||
section.style.width = '100%';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Improved theme change handler that preserves scroll position and provides smoother transitions
|
||||
document.addEventListener('themeChanged', () => {
|
||||
// Store current scroll position
|
||||
const scrollPosition = window.scrollY;
|
||||
|
||||
|
||||
// Create a temporary overlay for smoother transition
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = `
|
||||
@@ -276,19 +356,22 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
|
||||
// Fade in overlay
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.opacity = '0.5';
|
||||
|
||||
|
||||
// Update theme-transition elements without forcing reflow of entire page
|
||||
requestAnimationFrame(() => {
|
||||
document.querySelectorAll('.theme-transition-all, .theme-transition-bg, .theme-transition-color')
|
||||
.forEach(el => {
|
||||
document
|
||||
.querySelectorAll(
|
||||
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
|
||||
)
|
||||
.forEach((el) => {
|
||||
// Apply a subtle animation instead of a hard reset
|
||||
el.style.transition = 'all 0.5s ease';
|
||||
});
|
||||
|
||||
|
||||
// Fade out overlay after transition completes
|
||||
setTimeout(() => {
|
||||
overlay.style.opacity = '0';
|
||||
@@ -298,24 +381,24 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Restore scroll position (prevents jumping to top)
|
||||
if (scrollPosition > 0) {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: scrollPosition,
|
||||
behavior: 'auto' // Use 'auto' to prevent animation
|
||||
behavior: 'auto', // Use 'auto' to prevent animation
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Fix theme inconsistency issues by checking theme on visibility change
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
const currentThemeIsDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
|
||||
if (storedTheme === 'dark' && !currentThemeIsDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (storedTheme === 'light' && currentThemeIsDark) {
|
||||
@@ -323,34 +406,45 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add smooth reveal animations for content after loading
|
||||
const animateContent = () => {
|
||||
// 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) => {
|
||||
setTimeout(() => {
|
||||
el.classList.add('animate-reveal');
|
||||
}, 100 + (index * 150));
|
||||
setTimeout(
|
||||
() => {
|
||||
el.classList.add('animate-reveal');
|
||||
},
|
||||
100 + index * 150
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Animate posts with staggered delay
|
||||
const articles = document.querySelectorAll('article.group');
|
||||
articles.forEach((article, index) => {
|
||||
setTimeout(() => {
|
||||
article.classList.add('animate-reveal');
|
||||
}, 500 + (index * 150));
|
||||
setTimeout(
|
||||
() => {
|
||||
article.classList.add('animate-reveal');
|
||||
},
|
||||
500 + index * 150
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Animate topic cards with staggered delay
|
||||
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
|
||||
topicCards.forEach((card, index) => {
|
||||
setTimeout(() => {
|
||||
card.classList.add('animate-reveal');
|
||||
}, 800 + (index * 100));
|
||||
setTimeout(
|
||||
() => {
|
||||
card.classList.add('animate-reveal');
|
||||
},
|
||||
800 + index * 100
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Run animations after the loading screen is hidden
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) {
|
||||
@@ -361,18 +455,20 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
// Wait for loading screen to hide
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.target === loadingScreen &&
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'style' &&
|
||||
loadingScreen.style.display === 'none') {
|
||||
if (
|
||||
mutation.target === loadingScreen &&
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'style' &&
|
||||
loadingScreen.style.display === 'none'
|
||||
) {
|
||||
animateContent();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
observer.observe(loadingScreen, { attributes: true });
|
||||
|
||||
|
||||
// Fallback
|
||||
setTimeout(animateContent, 3500);
|
||||
}
|
||||
@@ -381,36 +477,38 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
animateContent();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// SPA transition handling for homepage
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')) {
|
||||
if (
|
||||
link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.getAttribute('href');
|
||||
|
||||
|
||||
// Trigger page transition animation
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
if (pageTransition) {
|
||||
pageTransition.classList.remove('opacity-0');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
|
||||
|
||||
// Navigate after transition effect
|
||||
setTimeout(() => {
|
||||
window.location.href = targetHref;
|
||||
@@ -422,13 +520,13 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||
</script>
|
||||
@@ -439,51 +537,55 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
||||
--theme-transition-duration: 0.5s;
|
||||
--theme-transition-timing: ease;
|
||||
}
|
||||
|
||||
:global(html), :global(body) {
|
||||
|
||||
:global(html),
|
||||
:global(body) {
|
||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||
}
|
||||
|
||||
|
||||
:global(.theme-transition-all) {
|
||||
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
|
||||
}
|
||||
|
||||
|
||||
:global(.theme-transition-bg) {
|
||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||
}
|
||||
|
||||
|
||||
:global(.theme-transition-color) {
|
||||
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||
}
|
||||
|
||||
|
||||
/* Ensure transitions apply to all theme-related properties */
|
||||
:global(*) {
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
||||
transition-duration: var(--theme-transition-duration);
|
||||
transition-timing-function: var(--theme-transition-timing);
|
||||
}
|
||||
|
||||
|
||||
/* Remove the forced transition disabling which causes flickering */
|
||||
:global(.theme-switching), :global(.theme-switching *) {
|
||||
:global(.theme-switching),
|
||||
:global(.theme-switching *) {
|
||||
/* Use a subtle transition instead of none */
|
||||
transition-duration: 0.3s !important;
|
||||
}
|
||||
|
||||
|
||||
/* Content reveal animations */
|
||||
.hero-text span,
|
||||
.hero-text + p,
|
||||
.hero-text span,
|
||||
.hero-text + p,
|
||||
.hero-text ~ div,
|
||||
article.group,
|
||||
a.group.flex.flex-col {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||
transition:
|
||||
opacity 0.8s ease,
|
||||
transform 0.8s ease;
|
||||
}
|
||||
|
||||
|
||||
.animate-reveal {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Rest of your existing styles... */
|
||||
</style>
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import rss from '@astrojs/rss';
|
||||
|
||||
import directus from "../../lib/directus"
|
||||
import { readItems,readSingleton } from "@directus/sdk";
|
||||
import directus from '../../lib/directus';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
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(
|
||||
readItems("posts", {
|
||||
readItems('posts', {
|
||||
fields: ['*'],
|
||||
sort: ["-published_date"],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
return rss({
|
||||
title: `${global.name}`,
|
||||
description: `${global.description}`,
|
||||
|
@@ -2,24 +2,26 @@
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FormattedDate from '../../components/FormattedDate.astro';
|
||||
|
||||
import directus from "../../../lib/directus"
|
||||
import { readItems } from "@directus/sdk";
|
||||
import directus from '../../../lib/directus';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await directus.request(readItems("posts", {
|
||||
fields: ['*'],
|
||||
}));
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
fields: ['*'],
|
||||
})
|
||||
);
|
||||
|
||||
// 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
|
||||
return uniqueTags.map(tag => {
|
||||
return uniqueTags.map((tag) => {
|
||||
// Make tag matching case-insensitive
|
||||
const filteredPosts = posts.filter(post =>
|
||||
post.tags?.some(t => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
|
||||
const filteredPosts = posts.filter(
|
||||
(post) => post.tags?.some((t) => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
|
||||
);
|
||||
return {
|
||||
params: { tag },
|
||||
@@ -33,184 +35,295 @@ const { posts = [] } = Astro.props;
|
||||
|
||||
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
|
||||
|
||||
const sortedPosts = posts && posts.length > 0
|
||||
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
: [];
|
||||
const sortedPosts =
|
||||
posts && posts.length > 0
|
||||
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
: [];
|
||||
console.log(`Sorted posts length: ${sortedPosts.length}`);
|
||||
|
||||
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
||||
const relatedTags = [...new Set(
|
||||
sortedPosts.flatMap(post => post.tags || [])
|
||||
.filter(t => t !== tag)
|
||||
)].slice(0, 5);
|
||||
|
||||
const relatedTags = [
|
||||
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
||||
].slice(0, 5);
|
||||
---
|
||||
|
||||
<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 -->
|
||||
<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 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>
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
<a
|
||||
href="/tags"
|
||||
class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<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>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-4 mb-2 justify-center sm:justify-start">
|
||||
<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">
|
||||
<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" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
||||
<div
|
||||
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
</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">
|
||||
#{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 w-1/2 h-1 bg-zinc-900 dark:bg-zinc-100 opacity-70 animate-expand"></span>
|
||||
<span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
|
||||
></span>
|
||||
<span
|
||||
class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100"
|
||||
></span>
|
||||
</span>
|
||||
</h1>
|
||||
</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">
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related tags section -->
|
||||
{relatedTags.length > 0 && (
|
||||
<div class="mb-8 sm:mb-12 overflow-x-auto pb-4 hide-scrollbar">
|
||||
<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="flex gap-2 flex-nowrap justify-center sm:justify-start">
|
||||
{relatedTags.map(relatedTag => (
|
||||
<a
|
||||
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"
|
||||
>
|
||||
#{relatedTag}
|
||||
</a>
|
||||
))}
|
||||
{
|
||||
relatedTags.length > 0 && (
|
||||
<div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
|
||||
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 dark:text-zinc-100 sm:text-left">
|
||||
Related topics
|
||||
</h2>
|
||||
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
|
||||
{relatedTags.map((relatedTag) => (
|
||||
<a
|
||||
href={`/topics/${relatedTag}`}
|
||||
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}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Posts list -->
|
||||
<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">
|
||||
{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">
|
||||
<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>
|
||||
{
|
||||
sortedPosts.map((post) => (
|
||||
<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">
|
||||
{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">
|
||||
<img
|
||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`}
|
||||
alt={post.image_alt}
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
||||
{post.image && (
|
||||
<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
|
||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
||||
alt={post.image_alt}
|
||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">
|
||||
{post.published_date && (
|
||||
<time datetime={post.published_date.toLocaleString()} class="flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 sm:w-4 sm:h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
||||
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
<FormattedDate date={post.published_date} />
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<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 && (
|
||||
<time
|
||||
datetime={post.published_date.toLocaleString()}
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
||||
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedDate date={post.published_date} />
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 class="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">
|
||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
||||
{post.title}
|
||||
</a>
|
||||
</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">
|
||||
{post.description}
|
||||
</p>
|
||||
</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">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mb-3 sm:mb-0 justify-center sm:justify-start">
|
||||
{post.tags.slice(0, 3).map(postTag => (
|
||||
<a
|
||||
href={`/topics/${postTag}`}
|
||||
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
||||
postTag === tag
|
||||
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
||||
: 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
#{postTag}
|
||||
<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">
|
||||
{post.title}
|
||||
</a>
|
||||
))}
|
||||
{post.tags.length > 3 && (
|
||||
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||
+{post.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div class="mx-auto sm:ml-auto sm:mr-0">
|
||||
<a
|
||||
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"
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="relative overflow-hidden inline-block">
|
||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">Read article</span>
|
||||
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">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="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</a>
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<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 && (
|
||||
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
|
||||
{post.tags.slice(0, 3).map((postTag) => (
|
||||
<a
|
||||
href={`/topics/${postTag}`}
|
||||
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
||||
postTag === tag
|
||||
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
||||
: 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
#{postTag}
|
||||
</a>
|
||||
))}
|
||||
{post.tags.length > 3 && (
|
||||
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||
+{post.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mx-auto sm:ml-auto sm:mr-0">
|
||||
<a
|
||||
href={`/blog/${post.slug}/`}
|
||||
class="inline-flex items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
||||
Read article
|
||||
</span>
|
||||
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
||||
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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state với màu zinc -->
|
||||
{sortedPosts.length === 0 && (
|
||||
<div class="text-center py-12 sm:py-20">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
{
|
||||
sortedPosts.length === 0 && (
|
||||
<div class="py-12 text-center sm:py-20">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<a
|
||||
href="/blog"
|
||||
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>
|
||||
<span>Browse all articles</span>
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">No posts found</h2>
|
||||
<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">
|
||||
<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="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
||||
</svg>
|
||||
<span>Browse all articles</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -237,8 +350,12 @@ const relatedTags = [...new Set(
|
||||
|
||||
/* Animated underline */
|
||||
@keyframes expand {
|
||||
from { width: 0; }
|
||||
to { width: 50%; }
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-expand {
|
||||
@@ -272,7 +389,10 @@ const relatedTags = [...new Set(
|
||||
/* Hover card effect */
|
||||
.hover-card {
|
||||
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) {
|
||||
@@ -308,11 +428,13 @@ const relatedTags = [...new Set(
|
||||
// Handle SPA transitions for tag pages
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')) {
|
||||
if (
|
||||
link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,25 +472,34 @@ const relatedTags = [...new Set(
|
||||
// Animate header elements
|
||||
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
|
||||
headerElements.forEach((el, index) => {
|
||||
setTimeout(() => {
|
||||
el.classList.add('animate-reveal');
|
||||
}, 100 + (index * 150));
|
||||
setTimeout(
|
||||
() => {
|
||||
el.classList.add('animate-reveal');
|
||||
},
|
||||
100 + index * 150
|
||||
);
|
||||
});
|
||||
|
||||
// Animate posts with staggered delay
|
||||
const articles = document.querySelectorAll('article');
|
||||
articles.forEach((article, index) => {
|
||||
setTimeout(() => {
|
||||
article.classList.add('animate-reveal');
|
||||
}, 400 + (index * 100));
|
||||
setTimeout(
|
||||
() => {
|
||||
article.classList.add('animate-reveal');
|
||||
},
|
||||
400 + index * 100
|
||||
);
|
||||
});
|
||||
|
||||
// Animate related tags
|
||||
const relatedTags = document.querySelectorAll('.related-tags a');
|
||||
relatedTags.forEach((tag, index) => {
|
||||
setTimeout(() => {
|
||||
tag.classList.add('animate-reveal');
|
||||
}, 600 + (index * 50));
|
||||
setTimeout(
|
||||
() => {
|
||||
tag.classList.add('animate-reveal');
|
||||
},
|
||||
600 + index * 50
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,4 +518,3 @@ const relatedTags = [...new Set(
|
||||
</script>
|
||||
|
||||
<!-- Add this at the end of your page -->
|
||||
</BaseLayout>
|
@@ -1,110 +1,147 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
import directus from "../../../lib/directus"
|
||||
import { readItems } from "@directus/sdk";
|
||||
import directus from '../../../lib/directus';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
const posts = await directus.request(
|
||||
readItems("posts", {
|
||||
readItems('posts', {
|
||||
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
|
||||
const tagObjects = tags.map(tag => {
|
||||
const count = posts.filter(post => post.tags?.includes(tag)).length;
|
||||
const tagObjects = tags.map((tag) => {
|
||||
const count = posts.filter((post) => post.tags?.includes(tag)).length;
|
||||
// Generate a consistent but random-looking hue for each tag
|
||||
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
||||
return {
|
||||
name: tag,
|
||||
return {
|
||||
name: tag,
|
||||
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);
|
||||
|
||||
---
|
||||
|
||||
<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 -->
|
||||
<div class="relative mb-8 sm:mb-12 md:mb-16 text-center theme-transition-element">
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<span class="inline-block relative">
|
||||
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
|
||||
<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>
|
||||
<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="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="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>
|
||||
{" "}
|
||||
{' '}
|
||||
<span class="relative inline-block">
|
||||
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>
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{tags.length === 0 ? (
|
||||
<div class="text-center py-8 sm:py-12 md:py-16 theme-transition-element">
|
||||
<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">
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
||||
</svg>
|
||||
|
||||
{
|
||||
tags.length === 0 ? (
|
||||
<div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
|
||||
<div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner dark:bg-zinc-800 sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24">
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="theme-transition-color text-lg font-medium text-zinc-800 dark:text-zinc-200 sm:text-xl md:text-2xl">
|
||||
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>
|
||||
<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="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>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-center w-full">
|
||||
<!-- Featured Tags Section - ultra-responsive design -->
|
||||
<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="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 theme-transition-bg"></div>
|
||||
<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="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>
|
||||
|
||||
<!-- Ultra-responsive grid layout with fallbacks -->
|
||||
<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) => (
|
||||
<a
|
||||
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"
|
||||
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="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="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">
|
||||
<span class="text-xs xxxs:text-xs xxs:text-xs xs:text-sm sm:text-base md:text-lg font-semibold">#</span>
|
||||
) : (
|
||||
<div class="flex w-full justify-center">
|
||||
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-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="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
|
||||
<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="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" />
|
||||
|
||||
<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>
|
||||
|
||||
<div class="xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 xxxs:gap-2 xxs:gap-2 xs:gap-2 grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 sm:gap-3 md:grid-cols-4 md:gap-4 lg:grid-cols-5">
|
||||
{sortedTags.map((tag) => (
|
||||
<a
|
||||
href={`/topics/${tag.name}`}
|
||||
class="theme-transition-element theme-ripple group relative min-w-0 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};`}
|
||||
>
|
||||
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
|
||||
|
||||
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
|
||||
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 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="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
|
||||
#
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 overflow-hidden">
|
||||
<h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate 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}
|
||||
</h3>
|
||||
<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 class="flex-1 min-w-0 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">
|
||||
{tag.name}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -121,46 +158,50 @@ 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';
|
||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||
} 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
|
||||
document.body.style.overflowX = 'hidden';
|
||||
document.documentElement.style.overflowX = 'hidden';
|
||||
document.documentElement.style.width = '100%';
|
||||
document.body.style.width = '100%';
|
||||
};
|
||||
|
||||
|
||||
fixViewportWidth();
|
||||
|
||||
|
||||
// Adjust tag items based on screen size with extreme precision
|
||||
const adjustTagItems = () => {
|
||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
||||
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
const width =
|
||||
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
const isVerySmall = width < 360;
|
||||
const isExtremelySmall = width < 280;
|
||||
const isMicroScreen = width < 240;
|
||||
|
||||
|
||||
// Fix container width to match viewport exactly
|
||||
const container = document.querySelector('.tag-cloud');
|
||||
if (container) {
|
||||
container.style.maxWidth = '100vw';
|
||||
container.style.width = '100%';
|
||||
container.style.boxSizing = 'border-box';
|
||||
|
||||
|
||||
// Remove any margins that might cause overflow
|
||||
container.style.marginLeft = '0';
|
||||
container.style.marginRight = '0';
|
||||
}
|
||||
|
||||
|
||||
// Fix grid width
|
||||
const grid = document.querySelector('.grid');
|
||||
if (grid) {
|
||||
grid.style.width = '100%';
|
||||
grid.style.maxWidth = '100%';
|
||||
}
|
||||
|
||||
tagItems.forEach(item => {
|
||||
|
||||
tagItems.forEach((item) => {
|
||||
// Set appropriate classes based on screen size
|
||||
if (isMicroScreen) {
|
||||
item.classList.add('micro-screen');
|
||||
@@ -174,15 +215,15 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
} else {
|
||||
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
|
||||
}
|
||||
|
||||
|
||||
// Ensure text doesn't overflow on small screens
|
||||
const tagName = item.querySelector('h3');
|
||||
const tagCount = item.querySelector('p');
|
||||
|
||||
|
||||
if (tagName) {
|
||||
// Set title for accessibility
|
||||
tagName.title = tagName.textContent.trim();
|
||||
|
||||
|
||||
// Adjust text length based on screen size
|
||||
if (isMicroScreen && tagName.textContent.length > 6) {
|
||||
tagName.dataset.fullText = tagName.textContent;
|
||||
@@ -198,32 +239,32 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
delete tagName.dataset.fullText;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the tag hue for hover effects
|
||||
const hue = item.style.getPropertyValue('--tag-hue');
|
||||
item.style.setProperty('--hover-hue', hue);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Run on load
|
||||
adjustTagItems();
|
||||
|
||||
|
||||
// Run on resize with optimized debounce
|
||||
let resizeTimer;
|
||||
const handleResize = () => {
|
||||
if (resizeTimer) {
|
||||
window.cancelAnimationFrame(resizeTimer);
|
||||
}
|
||||
|
||||
|
||||
resizeTimer = window.requestAnimationFrame(() => {
|
||||
fixViewportWidth();
|
||||
adjustTagItems();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
|
||||
|
||||
// Ensure layout is recalculated after page is fully loaded
|
||||
window.addEventListener('load', () => {
|
||||
fixViewportWidth();
|
||||
@@ -234,17 +275,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
adjustTagItems();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
// Fix for iOS Safari and other mobile browsers
|
||||
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
|
||||
document.documentElement.style.setProperty('--safe-area-inset-bottom', 'env(safe-area-inset-bottom)');
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
'--safe-area-inset-bottom',
|
||||
'env(safe-area-inset-bottom)'
|
||||
);
|
||||
|
||||
// Fix for mobile viewport height issues
|
||||
const setVh = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
|
||||
setVh();
|
||||
window.addEventListener('resize', setVh);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
@@ -255,39 +299,51 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add touch support for mobile devices
|
||||
const addTouchSupport = () => {
|
||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
||||
|
||||
tagItems.forEach(item => {
|
||||
item.addEventListener('touchstart', () => {
|
||||
item.classList.add('touch-active');
|
||||
}, { passive: true });
|
||||
|
||||
item.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
item.classList.remove('touch-active');
|
||||
}, 150);
|
||||
}, { passive: true });
|
||||
|
||||
|
||||
tagItems.forEach((item) => {
|
||||
item.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
item.classList.add('touch-active');
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
item.addEventListener(
|
||||
'touchend',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
item.classList.remove('touch-active');
|
||||
}, 150);
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
// Cancel active state if touch moves away
|
||||
item.addEventListener('touchmove', (e) => {
|
||||
const touch = e.touches[0];
|
||||
const rect = item.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
touch.clientX < rect.left ||
|
||||
touch.clientX > rect.right ||
|
||||
touch.clientY < rect.top ||
|
||||
touch.clientY > rect.bottom
|
||||
) {
|
||||
item.classList.remove('touch-active');
|
||||
}
|
||||
}, { passive: true });
|
||||
item.addEventListener(
|
||||
'touchmove',
|
||||
(e) => {
|
||||
const touch = e.touches[0];
|
||||
const rect = item.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
touch.clientX < rect.left ||
|
||||
touch.clientX > rect.right ||
|
||||
touch.clientY < rect.top ||
|
||||
touch.clientY > rect.bottom
|
||||
) {
|
||||
item.classList.remove('touch-active');
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
addTouchSupport();
|
||||
});
|
||||
</script>
|
||||
@@ -295,9 +351,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
<style>
|
||||
/* Base styles */
|
||||
.tag-cloud {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03),
|
||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||
0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.03),
|
||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||
0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
transform-style: preserve-3d;
|
||||
perspective: 1000px;
|
||||
transition: all var(--theme-transition);
|
||||
@@ -307,19 +364,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Fix for horizontal overflow */
|
||||
:global(html), :global(body) {
|
||||
:global(html),
|
||||
:global(body) {
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
:global(.max-w-6xl) {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
/* Ultra-responsive breakpoints for extreme reliability */
|
||||
/* Micro screens (below 240px) */
|
||||
@media (max-width: 239px) {
|
||||
@@ -329,146 +387,146 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
border-radius: 0.25rem !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
.tag-cloud h2 {
|
||||
font-size: 0.875rem !important;
|
||||
margin-bottom: 0.375rem !important;
|
||||
}
|
||||
|
||||
|
||||
.grid {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
||||
gap: 0.375rem !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
.micro-screen .flex {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
|
||||
.micro-screen h3 {
|
||||
font-size: 0.625rem !important;
|
||||
}
|
||||
|
||||
|
||||
.micro-screen p {
|
||||
font-size: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Extra extra extra small screens (240px-279px) */
|
||||
@media (min-width: 240px) and (max-width: 279px) {
|
||||
.xxxs\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:h-6 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxxs\:text-\[9px\] {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Extra extra small screens (280px-359px) */
|
||||
@media (min-width: 280px) {
|
||||
.xxs\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
|
||||
.xxs\:px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:h-6 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xxs\:text-\[9px\] {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Extra small screens (360px-639px) */
|
||||
@media (min-width: 360px) {
|
||||
.xs\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
|
||||
.xs\:px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:w-7 {
|
||||
width: 1.75rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:h-7 {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.xs\:text-\[10px\] {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Ensure text doesn't overflow on small screens */
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
@@ -476,7 +534,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure proper word breaking for long tag names */
|
||||
.break-words {
|
||||
word-break: break-word;
|
||||
@@ -486,38 +544,42 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
.hyphens-auto {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Improved shadow for dark mode */
|
||||
:global(.dark) .tag-cloud {
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
||||
0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
||||
0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Prevent layout shifts */
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
.min-w-0 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure container doesn't overflow */
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* Touch support for mobile */
|
||||
.touch-active {
|
||||
transform: scale(0.97) !important;
|
||||
opacity: 0.9;
|
||||
transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out !important;
|
||||
transition:
|
||||
transform 0.15s ease-in-out,
|
||||
opacity 0.15s ease-in-out !important;
|
||||
}
|
||||
|
||||
|
||||
/* Animation for blob */
|
||||
@keyframes blob {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
@@ -530,19 +592,19 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
transform: translate(-10px, -10px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
|
||||
/* Animation for underline */
|
||||
@keyframes underline {
|
||||
0% {
|
||||
@@ -552,11 +614,11 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animate-underline {
|
||||
animation: underline 1.5s ease-out forwards;
|
||||
}
|
||||
|
||||
|
||||
/* Fix for iOS Safari notch */
|
||||
@supports (padding: max(0px)) {
|
||||
.tag-cloud {
|
||||
@@ -571,32 +633,34 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
// Handle SPA transitions for tags index page
|
||||
function setupSPATransitions() {
|
||||
// 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
|
||||
if (link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')) {
|
||||
if (
|
||||
link.getAttribute('href').includes('#') ||
|
||||
link.getAttribute('target') === '_blank' ||
|
||||
link.hasAttribute('data-spa-handled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Mark as handled to avoid duplicate listeners
|
||||
link.setAttribute('data-spa-handled', 'true');
|
||||
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
const targetHref = link.getAttribute('href');
|
||||
|
||||
|
||||
// Trigger page transition animation
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
if (pageTransition) {
|
||||
pageTransition.classList.remove('opacity-0');
|
||||
pageTransition.classList.add('opacity-100');
|
||||
|
||||
|
||||
// Navigate after transition effect
|
||||
setTimeout(() => {
|
||||
window.location.href = targetHref;
|
||||
@@ -607,18 +671,18 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Add hover effect for tag cards on touch devices
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
|
||||
if (isTouchDevice) {
|
||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
||||
|
||||
tagCards.forEach(card => {
|
||||
|
||||
tagCards.forEach((card) => {
|
||||
card.addEventListener('touchstart', () => {
|
||||
card.classList.add('is-touched');
|
||||
});
|
||||
|
||||
|
||||
card.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
card.classList.remove('is-touched');
|
||||
@@ -626,23 +690,25 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Animate tag cards with staggered delay
|
||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
||||
tagCards.forEach((card, index) => {
|
||||
setTimeout(() => {
|
||||
card.classList.add('animate-reveal');
|
||||
}, 100 + (index * 50));
|
||||
setTimeout(
|
||||
() => {
|
||||
card.classList.add('animate-reveal');
|
||||
},
|
||||
100 + index * 50
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize on first load
|
||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||
|
||||
|
||||
// Re-initialize when content changes via Astro's view transitions
|
||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||
|
||||
|
||||
// For compatibility with custom transition system
|
||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||
</script>
|
||||
|
||||
|
@@ -1,11 +1,9 @@
|
||||
/* Remove all the complex mobile menu styles and keep only what's necessary */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--theme-transition: 0.3s ease;
|
||||
@@ -17,17 +15,20 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
|
||||
@reference min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Simple theme transition */
|
||||
body, a, button {
|
||||
transition: background-color var(--theme-transition),
|
||||
color var(--theme-transition),
|
||||
border-color var(--theme-transition);
|
||||
body,
|
||||
a,
|
||||
button {
|
||||
transition:
|
||||
background-color var(--theme-transition),
|
||||
color var(--theme-transition),
|
||||
border-color var(--theme-transition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,33 +37,55 @@
|
||||
html {
|
||||
scroll-padding-top: 4rem;
|
||||
}
|
||||
|
||||
|
||||
/* Better touch targets on mobile */
|
||||
button, a {
|
||||
@apply min-h-[44px];
|
||||
button,
|
||||
a {
|
||||
@reference min-h-[44px];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Add smooth animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply animations to elements */
|
||||
@@ -100,17 +123,21 @@
|
||||
}
|
||||
|
||||
/* Smooth hover transitions */
|
||||
a, button {
|
||||
a,
|
||||
button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
a:hover,
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Smooth page transitions */
|
||||
.page-transition {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.page-entering {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
export function debugObject(obj: any): string {
|
||||
return JSON.stringify(obj, null, 2)
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
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',
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -54,7 +54,5 @@ module.exports = {
|
||||
}),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
};
|
||||
|
Reference in New Issue
Block a user