Compare commits

...

21 Commits
0.7.0 ... 0.8.6

Author SHA1 Message Date
1ffe933d6e change build arg
Some checks failed
renovate / renovate (push) Successful in 1m26s
release-image-gitea / release (push) Failing after 1m24s
2025-06-08 20:45:47 -05:00
90318aad14 change build arg
Some checks failed
renovate / renovate (push) Successful in 1m15s
release-image-gitea / release (push) Failing after 1m19s
2025-06-08 20:39:46 -05:00
e454a510c6 change
All checks were successful
renovate / renovate (push) Successful in 1m4s
2025-06-08 20:35:06 -05:00
a6d3ec5052 change
All checks were successful
renovate / renovate (push) Successful in 1m8s
2025-06-08 20:33:24 -05:00
1d134d43da remove oci
All checks were successful
renovate / renovate (push) Successful in 1m11s
2025-06-08 20:30:25 -05:00
54c7c9e259 remove oci
All checks were successful
renovate / renovate (push) Successful in 1m9s
2025-06-08 20:26:29 -05:00
0d8cf28be4 modify daemon
All checks were successful
renovate / renovate (push) Successful in 1m16s
2025-06-08 20:24:03 -05:00
d78a8d8c45 use different registry
All checks were successful
renovate / renovate (push) Successful in 1m7s
2025-06-08 20:19:32 -05:00
5b6abeb9f9 use registry argument
Some checks failed
renovate / renovate (push) Successful in 1m34s
release-image-gitea / release (push) Failing after 23s
2025-06-08 20:14:29 -05:00
a3b0301d23 merge workflows 2025-06-08 20:09:45 -05:00
06f7546212 bump tag
Some checks failed
release-image-gitea / release (push) Failing after 22s
release-image-harbor / release (push) Failing after 26s
renovate / renovate (push) Successful in 1m19s
2025-06-08 19:49:37 -05:00
abd1d43f79 remove command 2025-06-08 19:49:19 -05:00
07f2f5f0e1 add node render
Some checks failed
release-image-gitea / release (push) Failing after 1m5s
renovate / renovate (push) Successful in 1m14s
release-image-harbor / release (push) Failing after 1m15s
2025-06-08 19:46:11 -05:00
91b53a33c2 update tags
Some checks failed
renovate / renovate (push) Successful in 1m43s
release-image-gitea / release (push) Failing after 5m13s
release-image-harbor / release (push) Failing after 5m16s
2025-06-08 19:10:25 -05:00
b3e23f3e6c update tags
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 19:09:57 -05:00
ab68b6248f fix tag 2025-06-08 19:01:01 -05:00
37d1f1d1f2 change workflows
Some checks failed
release-image-gitea / release (push) Failing after 7m21s
release-image-harbor / release (push) Failing after 7m20s
renovate / renovate (push) Successful in 2m6s
2025-06-08 17:15:25 -05:00
89e1c59e37 upgrade to tailwind 4
All checks were successful
renovate / renovate (push) Successful in 2m35s
2025-06-08 17:10:20 -05:00
7153f29022 update docker base
All checks were successful
renovate / renovate (push) Successful in 4m11s
2025-06-08 16:49:57 -05:00
51041f6ae9 apply prettier formatting
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 16:45:36 -05:00
67f12ecf72 add missing config
All checks were successful
renovate / renovate (push) Successful in 4m14s
2025-06-08 16:28:14 -05:00
37 changed files with 4815 additions and 3192 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
node_modules
dist

View File

@@ -1,67 +0,0 @@
name: release-image-harbor
on:
push:
tags:
- 0.*
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create Kubeconfig
run: |
mkdir $HOME/.kube
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver: kubernetes
driver-opts: |
namespace=gitea
qemu.install=true
- name: Available Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_HOST }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_SECRET }}
- name: Extract Metadata
id: meta
uses: docker/metadata-action@v5
with:
tags: |
type=ref,event=branch
type=ref,event=tag
images: ${{ vars.REGISTRY_HOST }}/images/site-profile
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
- name: Actions Ntfy
run: |
curl \
-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}}' \
${{ secrets.NTFY_URL }}

View File

@@ -14,6 +14,20 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REPOSITORY_HOST }}
username: ${{ gitea.actor }}
password: ${{ secrets.REPOSITORY_TOKEN }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_HOST }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_SECRET }}
- name: Create Kubeconfig - name: Create Kubeconfig
run: | run: |
mkdir $HOME/.kube mkdir $HOME/.kube
@@ -27,25 +41,41 @@ jobs:
driver-opts: | driver-opts: |
namespace=gitea namespace=gitea
qemu.install=true qemu.install=true
buildkitd-config-inline: |
[registry."hub.docker"]
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
- name: Available Platforms - name: Available Platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${{ steps.buildx.outputs.platforms }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REPOSITORY_HOST }}
username: ${{ gitea.actor }}
password: ${{ secrets.REPOSITORY_TOKEN }}
- name: Extract Metadata - name: Extract Metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
tags: | tags: |
type=schedule
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
images: ${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }} type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
images: |
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
${{ vars.REGISTRY_HOST }}/images/site-profile
- name: Build and Export to Docker
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: ${{ env.TEST_TAG }}
file: ./Dockerfile
- name: Test
run: |
docker run --rm ${{ env.TEST_TAG }}
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -63,5 +93,5 @@ jobs:
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \ -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 Gitea: ${{ steps.meta.outputs.tags }}" \
-H "Content-Type: text/plain" \ -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 }} ${{ secrets.NTFY_URL }}

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
FROM node:22.15.1-alpine3.20 AS base ARG REGISTRY=hub.docker
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base
LABEL version="0.7.0" LABEL version="0.8.6"
LABEL description="Astro based website to use as a personal site" LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
@@ -12,14 +13,15 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
FROM base AS prod-deps 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 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 FROM build-deps AS build
COPY . . COPY . .
RUN pnpm run build RUN pnpm run build
RUN pnpm prune --production
FROM base AS runtime FROM base AS runtime
COPY --from=prod-deps /app/node_modules /app/node_modules COPY --from=prod-deps /app/node_modules /app/node_modules
@@ -29,5 +31,6 @@ ENV HOST=0.0.0.0
ENV SITE_URL=https://www.alexlebens.dev ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.dev ENV DIRECTUS_URL=https://directus.alexlebens.dev
ENV PORT=4321 ENV PORT=4321
EXPOSE $PORT EXPOSE $PORT
CMD node ./dist/server/entry.mjs CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,7 +1,9 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind'; import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react'; import react from '@astrojs/react';
import node from '@astrojs/node';
const getSiteURL = () => { const getSiteURL = () => {
if (process.env.SITE_URL) { if (process.env.SITE_URL) {
return `https://${process.env.SITE_URL}`; return `https://${process.env.SITE_URL}`;
@@ -11,8 +13,17 @@ const getSiteURL = () => {
export default defineConfig({ export default defineConfig({
site: getSiteURL(), site: getSiteURL(),
integrations: [ integrations: [tailwindcss(), react()],
tailwind(),
react(), plugins: {
], "@tailwindcss/postcss": {},
}); },
vite: {
plugins: [tailwindcss()]
},
adapter: node({
mode: 'standalone'
})
});

View File

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

View File

@@ -1,12 +1,13 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.7.0", "version": "0.8.6",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"format": "prettier . --write",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
@@ -15,22 +16,24 @@
"@astrojs/react": "^4.3.0", "@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1", "@astrojs/sitemap": "^3.4.1",
"@astrojs/tailwind": "^6.0.2",
"@directus/sdk": "^19.1.0", "@directus/sdk": "^19.1.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"astro": "^5.9.1", "astro": "^5.9.1",
"form-data": "4.0.3",
"framer-motion": "^12.16.0", "framer-motion": "^12.16.0",
"postcss-preset-env": "^10.2.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hotkeys-hook": "^5.1.0", "react-hotkeys-hook": "^5.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"reading-time": "^1.5.0",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"tailwindcss": "^3.0.24" "tailwindcss": "^4.1.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-astro": "^0.12.3", "prettier-plugin-astro": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.14" "prettier-plugin-tailwindcss": "^0.6.0"
} }
} }

2064
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {}, autoprefixer: {},
'postcss-preset-env': { 'postcss-preset-env': {
features: { features: {

10
renovate.json Normal file
View 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": []
}

View File

@@ -2,16 +2,29 @@
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions // Background.astro - Dot pattern and ambient glow background with smooth theme transitions
--- ---
<div class="fixed inset-0 -z-10 overflow-hidden theme-transition-all"> <div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
<!-- Dot pattern background --> <!-- Dot pattern background -->
<div class="absolute inset-0 bg-grid-pattern bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)] theme-transition-bg"></div> <div
class="bg-grid-pattern theme-transition-bg absolute inset-0 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]"
>
</div>
<!-- Ambient glow effects --> <!-- Ambient glow effects -->
<div class="absolute left-1/4 top-1/4 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-zinc-400/20 dark:bg-zinc-500/20 rounded-full blur-3xl opacity-50 animate-glow theme-transition-bg"></div> <div
<div class="absolute right-1/4 bottom-1/3 translate-x-1/2 translate-y-1/2 w-64 h-64 bg-zinc-300/20 dark:bg-zinc-600/20 rounded-full blur-3xl opacity-40 animate-glow animation-delay-1000 theme-transition-bg"></div> class="animate-glow theme-transition-bg absolute left-1/4 top-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
>
</div>
<div
class="animate-glow animation-delay-1000 theme-transition-bg absolute bottom-1/3 right-1/4 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
>
</div>
<!-- Theme transition overlay --> <!-- Theme transition overlay -->
<div id="theme-transition-overlay" class="absolute inset-0 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none"></div> <div
id="theme-transition-overlay"
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
>
</div>
</div> </div>
<script> <script>
@@ -19,20 +32,20 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.querySelector('[data-theme-toggle]'); const themeToggle = document.querySelector('[data-theme-toggle]');
const overlay = document.getElementById('theme-transition-overlay'); const overlay = document.getElementById('theme-transition-overlay');
if (themeToggle && overlay) { if (themeToggle && overlay) {
themeToggle.addEventListener('click', () => { themeToggle.addEventListener('click', () => {
// Add transitioning class to optimize performance // Add transitioning class to optimize performance
document.documentElement.classList.add('theme-transitioning'); document.documentElement.classList.add('theme-transitioning');
// Fade in overlay // Fade in overlay
overlay.style.opacity = '0.15'; overlay.style.opacity = '0.15';
overlay.style.transition = 'opacity 0.3s ease'; overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => { setTimeout(() => {
// Fade out overlay // Fade out overlay
overlay.style.opacity = '0'; overlay.style.opacity = '0';
// Remove transitioning class after animation completes // Remove transitioning class after animation completes
setTimeout(() => { setTimeout(() => {
document.documentElement.classList.remove('theme-transitioning'); document.documentElement.classList.remove('theme-transitioning');
@@ -50,24 +63,27 @@
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px); background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1); transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
} }
/* Dark mode version */ /* Dark mode version */
:global(.dark) .bg-grid-pattern { :global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px); background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
} }
/* Ambient glow animations */ /* Ambient glow animations */
.animate-glow { .animate-glow {
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transition: background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1); transition:
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
} }
.animation-delay-1000 { .animation-delay-1000 {
animation-delay: 1s; animation-delay: 1s;
} }
@keyframes glow { @keyframes glow {
0%, 100% { 0%,
100% {
opacity: 0.4; opacity: 0.4;
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
@@ -84,7 +100,7 @@
transform: translate(-5%, 5%) scale(1.05); transform: translate(-5%, 5%) scale(1.05);
} }
} }
/* Theme transition overlay */ /* Theme transition overlay */
#theme-transition-overlay { #theme-transition-overlay {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;

View File

@@ -1,9 +1,9 @@
--- ---
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton("links")); const links = await directus.request(readSingleton('links'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@@ -18,173 +18,227 @@ const socialLinks = [
{ {
name: 'GitHub', name: 'GitHub',
href: links.github, href: links.github,
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>` icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
}, },
{ {
name: 'LinkedIn', name: 'LinkedIn',
href: links.linkedin, href: links.linkedin,
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>` icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
} },
]; ];
--- ---
<footer class="relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> <footer
<div class="absolute inset-0 pointer-events-none overflow-hidden"> class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
<div class="absolute -top-40 -right-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow"></div> >
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow animation-delay-2000"></div> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute top-20 left-1/4 w-40 h-40 bg-zinc-200/50 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-30 theme-transition-all animate-float-slow animation-delay-1000"></div> <div
class="theme-transition-all animate-float-slow absolute -right-40 -top-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute left-1/4 top-20 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
>
</div>
</div> </div>
<div class="relative pt-16 pb-12 px-4 sm:px-6"> <div class="relative px-4 pb-12 pt-16 sm:px-6">
<div class="max-w-4xl mx-auto"> <div class="mx-auto max-w-4xl">
<!-- Main footer content --> <!-- Main footer content -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-10"> <div class="grid grid-cols-1 gap-10 md:grid-cols-12">
<!-- Brand section --> <!-- Brand section -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<a href="/" class="inline-block group"> <a href="/" class="group inline-block">
<div class="flex items-center"> <div class="flex items-center">
<div class="relative w-10 h-10 rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 dark:from-zinc-200 dark:to-zinc-400 flex items-center justify-center overflow-hidden shadow-lg transform transition-transform group-hover:scale-105"> <div
<span class="text-white dark:text-zinc-900 text-xl font-bold theme-transition-all group-hover:scale-110 transition-transform duration-300">{global.initals}</span> class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
<div class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> >
<span
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
>{global.initals}</span
>
<div
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
>
</div>
</div> </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> </div>
</a> </a>
<p class="mt-4 text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color leading-relaxed"> <p
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{global.description} {global.description}
</p> </p>
<!-- Social links --> <!-- Social links -->
<div class="mt-6 flex items-center space-x-4"> <div class="mt-6 flex items-center space-x-4">
{socialLinks.map(social => ( {
<a socialLinks.map((social) => (
href={social.href} <a
target="_blank" href={social.href}
rel="noopener noreferrer" target="_blank"
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" rel="noopener noreferrer"
aria-label={social.name} 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 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"> <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" />
<Fragment set:html={social.icon} /> <svg
</svg> class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
</a> fill="currentColor"
))} viewBox="0 0 24 24"
aria-hidden="true"
>
<Fragment set:html={social.icon} />
</svg>
</a>
))
}
</div> </div>
</div> </div>
<!-- Quick links --> <!-- Quick links -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 uppercase tracking-wider theme-transition-color relative inline-block after:content-[''] after:absolute after:w-8 after:h-0.5 after:bg-zinc-300 dark:after:bg-zinc-700 after:bottom-0 after:left-0 pb-2">Navigation</h3> <h3
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
>
Navigation
</h3>
<ul class="mt-4 space-y-3"> <ul class="mt-4 space-y-3">
{navLinks.map(link => ( {
<li> navLinks.map((link) => (
<a <li>
href={link.href} <a
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" 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 overflow-hidden inline-block"> >
<span class="relative z-10">{link.text}</span> <span class="relative inline-block overflow-hidden">
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full"></span> <span class="relative z-10">{link.text}</span>
</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" />
</a> </span>
</li> </a>
))} </li>
))
}
</ul> </ul>
</div> </div>
<!-- Bottom section --> <!-- Bottom section -->
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 theme-transition-all"> <div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col md:flex-row items-center justify-between gap-4"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<p class="text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color"> <p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
&copy; {currentYear} All rights reserved. &copy; {currentYear} All rights reserved.
</p> </p>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color">Built with</span> <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
<a >Built with</span
href="https://astro.build" >
target="_blank" <a
rel="noopener noreferrer" href="https://astro.build"
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" target="_blank"
> rel="noopener noreferrer"
<svg class="h-4 w-4 mr-1 text-[#FF5D01] group-hover:animate-pulse" viewBox="0 0 36 36" fill="none"> 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"
<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
</svg> class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
<span class="relative"> viewBox="0 0 36 36"
Astro fill="none"
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"></span> >
</span> <path
</a> 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> </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> </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>

View File

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

View File

@@ -1,10 +1,10 @@
--- ---
import ThemeToggle from './ThemeToggle.astro'; import ThemeToggle from './ThemeToggle.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readSingleton } from "@directus/sdk"; import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const navItems = [ const navItems = [
{ text: 'Home', href: '/' }, { text: 'Home', href: '/' },
@@ -15,68 +15,102 @@ const navItems = [
]; ];
const pathname = new URL(Astro.request.url).pathname; const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1); // remove the first "/" const currentPath = pathname.slice(1);
--- ---
<header class="py-4 fixed top-0 left-0 right-0 z-40 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-800"> <header
<div class="max-w-3xl mx-auto px-4 flex items-center justify-between"> class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
>
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
<!-- Logo --> <!-- Logo -->
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">{global.initals}</a> <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav class="hidden sm:flex items-center space-x-6"> <nav class="hidden items-center space-x-6 sm:flex">
{navItems.map(item => { {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1)); navItems.map((item) => {
return ( const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
<a return (
href={item.href} <a
class={`text-sm font-medium ${isActive href={item.href}
? 'text-zinc-900 dark:text-white' class={`text-sm font-medium ${
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`} isActive
> ? 'text-zinc-900 dark:text-white'
{item.text} : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
</a> }`}
) >
})} {item.text}
</a>
);
})
}
<ThemeToggle /> <ThemeToggle />
</nav> </nav>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<button id="mobile-menu-button" class="sm:hidden flex items-center" aria-label="Menu"> <button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-zinc-900 dark:text-white"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-900 dark:text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
</svg> </svg>
</button> </button>
</div> </div>
</header> </header>
<!-- Mobile menu overlay --> <!-- Mobile menu overlay -->
<div id="mobile-menu" class="fixed inset-0 z-50 bg-white dark:bg-zinc-900 flex flex-col opacity-0 pointer-events-none transition-all duration-300 ease-in-out"> <div
<div class="flex justify-between items-center p-4 border-b border-zinc-100 dark:border-zinc-800"> id="mobile-menu"
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">JD</a> class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
<button id="close-menu-button" class="text-zinc-900 dark:text-white p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" aria-label="Close menu"> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a>
<button
id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>
</button> </button>
</div> </div>
<nav class="flex-1 flex flex-col items-center justify-center space-y-6 text-center"> <nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
{navItems.map((item, index) => { {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1)); navItems.map((item, index) => {
return ( const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
<a return (
href={item.href} <a
class={`text-lg font-medium mobile-nav-item opacity-0 translate-y-4 ${isActive href={item.href}
? 'text-zinc-900 dark:text-white' class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`} isActive
style={`transition-delay: ${index * 0.05}s;`} ? 'text-zinc-900 dark:text-white'
> : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
{item.text} }`}
</a> style={`transition-delay: ${index * 0.05}s;`}
) >
})} {item.text}
<div class="pt-4 mobile-nav-item opacity-0 translate-y-4" style="transition-delay: 0.25s;"> </a>
);
})
}
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
<ThemeToggle /> <ThemeToggle />
</div> </div>
</nav> </nav>
@@ -92,44 +126,44 @@ const currentPath = pathname.slice(1); // remove the first "/"
const closeMenuButton = document.getElementById('close-menu-button'); const closeMenuButton = document.getElementById('close-menu-button');
const mobileMenu = document.getElementById('mobile-menu'); const mobileMenu = document.getElementById('mobile-menu');
const navItems = document.querySelectorAll('.mobile-nav-item'); const navItems = document.querySelectorAll('.mobile-nav-item');
// Open menu with animations // Open menu with animations
mobileMenuButton?.addEventListener('click', () => { mobileMenuButton?.addEventListener('click', () => {
if (!mobileMenu) return; if (!mobileMenu) return;
// Prevent body scrolling // Prevent body scrolling
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
// Show menu with fade in // Show menu with fade in
mobileMenu.classList.remove('pointer-events-none'); mobileMenu.classList.remove('pointer-events-none');
mobileMenu.classList.add('pointer-events-auto'); mobileMenu.classList.add('pointer-events-auto');
// Animate opacity // Animate opacity
setTimeout(() => { setTimeout(() => {
mobileMenu.style.opacity = '1'; mobileMenu.style.opacity = '1';
// Animate each nav item with staggered delay // Animate each nav item with staggered delay
navItems.forEach(item => { navItems.forEach((item) => {
setTimeout(() => { setTimeout(() => {
item.classList.remove('opacity-0', 'translate-y-4'); item.classList.remove('opacity-0', 'translate-y-4');
}, 150); }, 150);
}); });
}, 50); }, 50);
}); });
// Close menu with animations // Close menu with animations
const closeMenu = () => { const closeMenu = () => {
if (!mobileMenu) return; if (!mobileMenu) return;
// Fade out nav items first // Fade out nav items first
navItems.forEach(item => { navItems.forEach((item) => {
item.classList.add('opacity-0', 'translate-y-4'); item.classList.add('opacity-0', 'translate-y-4');
}); });
// Then fade out the menu // Then fade out the menu
setTimeout(() => { setTimeout(() => {
mobileMenu.style.opacity = '0'; mobileMenu.style.opacity = '0';
// After animation completes, hide menu and restore scrolling // After animation completes, hide menu and restore scrolling
setTimeout(() => { setTimeout(() => {
mobileMenu.classList.remove('pointer-events-auto'); mobileMenu.classList.remove('pointer-events-auto');
@@ -138,39 +172,39 @@ const currentPath = pathname.slice(1); // remove the first "/"
}, 300); }, 300);
}, 100); }, 100);
}; };
// Close button event // Close button event
closeMenuButton?.addEventListener('click', closeMenu); closeMenuButton?.addEventListener('click', closeMenu);
// Close menu when clicking a link // Close menu when clicking a link
const mobileLinks = mobileMenu?.querySelectorAll('a'); const mobileLinks = mobileMenu?.querySelectorAll('a');
mobileLinks?.forEach(link => { mobileLinks?.forEach((link) => {
link.addEventListener('click', closeMenu); link.addEventListener('click', closeMenu);
}); });
// Close menu on escape key // Close menu on escape key
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) { if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
closeMenu(); closeMenu();
} }
}); });
// Add smooth animation to header on scroll // Add smooth animation to header on scroll
const header = document.querySelector('header'); const header = document.querySelector('header');
let lastScrollY = window.scrollY; let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (!header) return; if (!header) return;
const currentScrollY = window.scrollY; const currentScrollY = window.scrollY;
// Add shadow on scroll // Add shadow on scroll
if (currentScrollY > 10) { if (currentScrollY > 10) {
header.classList.add('shadow-sm'); header.classList.add('shadow-sm');
} else { } else {
header.classList.remove('shadow-sm'); header.classList.remove('shadow-sm');
} }
// Update last scroll position // Update last scroll position
lastScrollY = currentScrollY; lastScrollY = currentScrollY;
}); });
@@ -180,23 +214,29 @@ const currentPath = pathname.slice(1); // remove the first "/"
<style> <style>
/* Smooth animations for mobile navigation */ /* Smooth animations for mobile navigation */
.mobile-nav-item { .mobile-nav-item {
transition: opacity 0.5s ease, transform 0.5s ease, color 0.3s ease; transition:
opacity 0.5s ease,
transform 0.5s ease,
color 0.3s ease;
} }
/* Header transition */ /* Header transition */
header { header {
transition: box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease; transition:
box-shadow 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
} }
/* Mobile menu button hover effect */ /* Mobile menu button hover effect */
#mobile-menu-button { #mobile-menu-button {
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
#mobile-menu-button:hover { #mobile-menu-button:hover {
transform: scale(1.05); transform: scale(1.05);
} }
/* Mobile menu transition */ /* Mobile menu transition */
#mobile-menu { #mobile-menu {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;

View File

@@ -13,41 +13,89 @@ const encodedUrl = encodeURIComponent(url);
<div class={`flex items-center gap-4 mt-8 ${className}`}> <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> <span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
<div class="flex gap-2"> <div class="flex gap-2">
<a <a
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`} href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Twitter" aria-label="Share on Twitter"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
></path></svg
>
</a> </a>
<a <a
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Facebook" aria-label="Share on Facebook"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
>
</a> </a>
<a <a
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300" class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on LinkedIn" aria-label="Share on LinkedIn"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
></circle></svg
>
</a> </a>
<button <button
id="copy-link-button" id="copy-link-button"
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 relative" class="relative rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Copy link" aria-label="Copy link"
title="Copy link to clipboard" title="Copy link to clipboard"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> <svg
<span id="copy-tooltip" class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-zinc-800 dark:bg-zinc-700 text-white text-xs py-1 px-2 rounded opacity-0 transition-opacity duration-300 whitespace-nowrap"> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
>
<span
id="copy-tooltip"
class="absolute -top-8 left-1/2 -translate-x-1/2 transform whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
>
Copied! Copied!
</span> </span>
</button> </button>
@@ -58,66 +106,69 @@ const encodedUrl = encodeURIComponent(url);
// Function to handle copy link button // Function to handle copy link button
function setupCopyLinkButton() { function setupCopyLinkButton() {
const copyButtons = document.querySelectorAll('#copy-link-button'); const copyButtons = document.querySelectorAll('#copy-link-button');
copyButtons.forEach(button => { copyButtons.forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
// Get the current URL // Get the current URL
const url = window.location.href; const url = window.location.href;
// Copy to clipboard // Copy to clipboard
navigator.clipboard.writeText(url).then(() => { navigator.clipboard
// Show tooltip .writeText(url)
const tooltip = button.querySelector('#copy-tooltip'); .then(() => {
if (tooltip) { // Show tooltip
tooltip.classList.add('opacity-100'); const tooltip = button.querySelector('#copy-tooltip');
if (tooltip) {
// Hide tooltip after 2 seconds tooltip.classList.add('opacity-100');
setTimeout(() => {
tooltip.classList.remove('opacity-100'); // Hide tooltip after 2 seconds
}, 2000); setTimeout(() => {
} tooltip.classList.remove('opacity-100');
}).catch(err => { }, 2000);
console.error('Failed to copy: ', err); }
}); })
.catch((err) => {
console.error('Failed to copy: ', err);
});
}); });
}); });
} }
// Set up the copy link button when the DOM is loaded // Set up the copy link button when the DOM is loaded
document.addEventListener('DOMContentLoaded', setupCopyLinkButton); document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
// Also set up when the page content is updated via SPA navigation // Also set up when the page content is updated via SPA navigation
document.addEventListener('astro:page-load', setupCopyLinkButton); document.addEventListener('astro:page-load', setupCopyLinkButton);
// For compatibility with the custom page transition system // For compatibility with the custom page transition system
document.addEventListener('page-transition-complete', setupCopyLinkButton); document.addEventListener('page-transition-complete', setupCopyLinkButton);
// Handle SPA transitions for share links // Handle SPA transitions for share links
function setupSpaTransitions() { function setupSpaTransitions() {
// Get all share links // Get all share links
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]'); const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
// Make sure external share links don't trigger page transitions // Make sure external share links don't trigger page transitions
shareLinks.forEach(link => { shareLinks.forEach((link) => {
link.setAttribute('data-spa-external', 'true'); link.setAttribute('data-spa-external', 'true');
}); });
} }
// Initialize SPA transitions // Initialize SPA transitions
document.addEventListener('DOMContentLoaded', setupSpaTransitions); document.addEventListener('DOMContentLoaded', setupSpaTransitions);
document.addEventListener('astro:page-load', setupSpaTransitions); document.addEventListener('astro:page-load', setupSpaTransitions);
document.addEventListener('page-transition-complete', setupSpaTransitions); document.addEventListener('page-transition-complete', setupSpaTransitions);
// Dispatch custom event when share action is completed // Dispatch custom event when share action is completed
function notifyShareComplete() { function notifyShareComplete() {
document.dispatchEvent(new CustomEvent('share-action-complete')); document.dispatchEvent(new CustomEvent('share-action-complete'));
} }
// Add analytics tracking for share actions if needed // Add analytics tracking for share actions if needed
function trackShareAction(platform) { function trackShareAction(platform) {
// You can implement analytics tracking here // You can implement analytics tracking here
console.log(`Shared on ${platform}`); console.log(`Shared on ${platform}`);
// Notify other components that share action is complete // Notify other components that share action is complete
notifyShareComplete(); notifyShareComplete();
} }

View File

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

View File

@@ -1,65 +1,70 @@
--- ---
---
<button ---
id="theme-toggle"
<button
id="theme-toggle"
data-theme-toggle data-theme-toggle
class="relative overflow-hidden rounded-full p-1.5 sm:p-2 transition-all duration-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:focus:ring-zinc-700 group touch-manipulation" class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2"
aria-label="Toggle dark mode" aria-label="Toggle dark mode"
> >
<div class="relative z-10 flex h-5 w-5 items-center justify-center"> <div class="relative z-10 flex h-5 w-5 items-center justify-center">
<!-- Sun icon --> <!-- Sun icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-light absolute h-5 w-5 rotate-0 scale-100 transition-all duration-500 dark:-rotate-90 dark:scale-0 text-zinc-800 dark:text-zinc-200" class="icon-light absolute h-5 w-5 rotate-0 scale-100 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<circle cx="12" cy="12" r="5"/> <circle cx="12" cy="12" r="5"></circle>
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/> <path
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
></path>
</svg> </svg>
<!-- Moon icon --> <!-- Moon icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 transition-all duration-500 dark:rotate-0 dark:scale-100 text-zinc-800 dark:text-zinc-200" class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg> </svg>
</div> </div>
<!-- Ripple effect --> <!-- Ripple effect -->
<span class="absolute inset-0 h-full w-full bg-zinc-200 dark:bg-zinc-700 opacity-0 transition-opacity duration-300 group-active:opacity-20"></span> <span
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
></span>
</button> </button>
<script> <script>
// Use a function to handle theme toggle to ensure it can be called from anywhere // Use a function to handle theme toggle to ensure it can be called from anywhere
function setupThemeToggle() { function setupThemeToggle() {
const themeToggles = document.querySelectorAll('[data-theme-toggle]'); const themeToggles = document.querySelectorAll('[data-theme-toggle]');
// Check for dark mode preference at the system level // Check for dark mode preference at the system level
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Check for saved theme preference or use the system preference // Check for saved theme preference or use the system preference
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light'); const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
// Apply the theme on initial load // Apply the theme on initial load
if (currentTheme === 'dark') { if (currentTheme === 'dark') {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
// Create theme switch overlay element if it doesn't exist // Create theme switch overlay element if it doesn't exist
if (!document.querySelector('.theme-switch-overlay')) { if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -68,104 +73,119 @@
overlay.style.transition = 'opacity 0.3s ease-out'; overlay.style.transition = 'opacity 0.3s ease-out';
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
// Toggle theme when any theme toggle button is clicked // Toggle theme when any theme toggle button is clicked
themeToggles.forEach(toggle => { themeToggles.forEach((toggle) => {
// Add event listeners for both click and touch events // Add event listeners for both click and touch events
['click', 'touchend'].forEach(eventType => { ['click', 'touchend'].forEach((eventType) => {
toggle.addEventListener(eventType, (e) => { toggle.addEventListener(
e.preventDefault(); eventType,
e.stopPropagation(); (e) => {
e.preventDefault();
// Get click/touch position for radial animation e.stopPropagation();
let x, y;
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) { // Get click/touch position for radial animation
const rect = toggle.getBoundingClientRect(); let x, y;
x = e.changedTouches[0].clientX - rect.left; if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
y = e.changedTouches[0].clientY - rect.top; const rect = toggle.getBoundingClientRect();
} else { x = e.changedTouches[0].clientX - rect.left;
const rect = toggle.getBoundingClientRect(); y = e.changedTouches[0].clientY - rect.top;
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');
} else { } else {
document.documentElement.classList.add('dark'); const rect = toggle.getBoundingClientRect();
x = e.clientX - rect.left;
y = e.clientY - rect.top;
} }
// Store the preference // Set the position variables for the radial gradient
localStorage.setItem('theme', newTheme); document.documentElement.style.setProperty('--x', `${x}px`);
document.documentElement.style.setProperty('--y', `${y}px`);
// Dispatch a custom event for other components to react to
document.dispatchEvent(new CustomEvent('themeChanged', { // Get the overlay element
detail: { isDark: newTheme === 'dark' } const overlay = document.querySelector('.theme-switch-overlay');
}));
// Determine the new theme
// Force another reflow to ensure all elements update 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; document.body.offsetHeight;
// Hide overlay after theme has changed // Toggle dark mode with a slight delay to allow overlay to appear
setTimeout(() => { setTimeout(() => {
if (overlay) { if (isDark) {
overlay.style.opacity = '0'; document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
} }
// Remove transition class after animation completes // Store the preference
document.documentElement.classList.remove('theme-switching'); localStorage.setItem('theme', newTheme);
ripple.remove();
}, 300); // Dispatch a custom event for other components to react to
}, 50); document.dispatchEvent(
}, { passive: false }); 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 // Add touch feedback
toggle.addEventListener('touchstart', () => { toggle.addEventListener(
toggle.classList.add('active-touch'); 'touchstart',
}, { passive: true }); () => {
toggle.classList.add('active-touch');
toggle.addEventListener('touchend', () => { },
setTimeout(() => { { passive: true }
toggle.classList.remove('active-touch'); );
}, 150);
}, { passive: true }); toggle.addEventListener(
'touchend',
() => {
setTimeout(() => {
toggle.classList.remove('active-touch');
}, 150);
},
{ passive: true }
);
}); });
} }
// Run setup on load // Run setup on load
document.addEventListener('DOMContentLoaded', setupThemeToggle); document.addEventListener('DOMContentLoaded', setupThemeToggle);
// Also run on page visibility change to ensure theme is consistent // Also run on page visibility change to ensure theme is consistent
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
@@ -177,7 +197,7 @@
} }
} }
}); });
// Listen for system preference changes // Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
if (!localStorage.getItem('theme')) { if (!localStorage.getItem('theme')) {
@@ -193,9 +213,11 @@
<style> <style>
/* Smooth transition for the entire page when theme changes */ /* Smooth transition for the entire page when theme changes */
:global(body) { :global(body) {
transition: background-color 0.5s ease, color 0.5s ease; transition:
background-color 0.5s ease,
color 0.5s ease;
} }
/* Theme transition overlay */ /* Theme transition overlay */
:global(.theme-switch-overlay) { :global(.theme-switch-overlay) {
position: fixed; position: fixed;
@@ -204,13 +226,13 @@
pointer-events: none; pointer-events: none;
transition: opacity 0.3s ease-out; transition: opacity 0.3s ease-out;
} }
/* Ensure theme transitions apply to all elements */ /* Ensure theme transitions apply to all elements */
:global(.theme-switching *) { :global(.theme-switching *) {
transition-duration: 0.5s !important; transition-duration: 0.5s !important;
transition-property: background-color, border-color, color, fill, stroke !important; transition-property: background-color, border-color, color, fill, stroke !important;
} }
/* Ripple animation */ /* Ripple animation */
.theme-toggle-ripple { .theme-toggle-ripple {
position: absolute; position: absolute;
@@ -223,7 +245,7 @@
background-color: rgba(161, 161, 170, 0.3); background-color: rgba(161, 161, 170, 0.3);
animation: ripple 0.8s ease-out; animation: ripple 0.8s ease-out;
} }
@keyframes ripple { @keyframes ripple {
0% { 0% {
transform: translate(-50%, -50%) scale(0); transform: translate(-50%, -50%) scale(0);
@@ -234,7 +256,7 @@
opacity: 0; opacity: 0;
} }
} }
/* Subtle hover animation */ /* Subtle hover animation */
#theme-toggle { #theme-toggle {
transform: translateY(0); transform: translateY(0);
@@ -243,47 +265,49 @@
min-height: 32px; /* Ensure minimum touch target size */ min-height: 32px; /* Ensure minimum touch target size */
min-width: 32px; /* Ensure minimum touch target size */ min-width: 32px; /* Ensure minimum touch target size */
} }
/* Only apply hover effects on non-touch devices */ /* Only apply hover effects on non-touch devices */
@media (hover: hover) { @media (hover: hover) {
#theme-toggle:hover { #theme-toggle:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
#theme-toggle:hover .icon-light:not(.dark .icon-light) { #theme-toggle:hover .icon-light:not(.dark .icon-light) {
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6)); filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
transform: scale(1.1) rotate(15deg); transform: scale(1.1) rotate(15deg);
} }
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) { #theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6)); filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
transform: scale(1.1) rotate(-15deg); transform: scale(1.1) rotate(-15deg);
} }
} }
/* Touch feedback */ /* Touch feedback */
#theme-toggle.active-touch { #theme-toggle.active-touch {
transform: scale(0.95); transform: scale(0.95);
transition: transform 0.15s ease-in-out; transition: transform 0.15s ease-in-out;
} }
/* Optimize animations for mobile */ /* Optimize animations for mobile */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.icon-light, .icon-dark { .icon-light,
.icon-dark {
transition: all 0.2s ease-out !important; transition: all 0.2s ease-out !important;
} }
#theme-toggle, #theme-toggle:hover { #theme-toggle,
#theme-toggle:hover {
transform: none; transform: none;
transition: none; transition: none;
} }
.theme-toggle-ripple { .theme-toggle-ripple {
animation-duration: 0.4s; animation-duration: 0.4s;
} }
} }
/* Adjust size for very small screens */ /* Adjust size for very small screens */
@media (max-width: 320px) { @media (max-width: 320px) {
#theme-toggle { #theme-toggle {

1
src/env.d.ts vendored
View File

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

View File

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

View File

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

View File

@@ -5,13 +5,15 @@ import ShareButtons from '../components/ShareButtons.astro';
import TagList from '../components/TagList.astro'; import TagList from '../components/TagList.astro';
import './styles/markdown.css'; import './styles/markdown.css';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", { const posts = await directus.request(
fields: ['*'], readItems('posts', {
})); fields: ['*'],
})
);
return posts.map((post) => ({ params: { slug: post.slug }, props: post })); 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); canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
} catch (error) { } catch (error) {
console.error('Error creating canonical URL:', error); console.error('Error creating canonical URL:', error);
canonicalURL = new URL("https://www.example.com"); canonicalURL = new URL('https://www.example.com');
} }
--- ---
<Layout title={post.title} description={post.description}> <Layout title={post.title} description={post.description}>
<article class="prose dark:prose-invert prose-zinc lg:prose-lg mx-auto max-w-4xl"> <article class="prose prose-zinc mx-auto max-w-4xl dark:prose-invert lg:prose-lg">
<div class="mb-12"> <div class="mb-12">
<h1 class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"> <h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"
>
{post.title} {post.title}
</h1> </h1>
<div class="flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400 mb-6"> <div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
<FormattedDate date={published_date} /> <FormattedDate date={published_date} />
</div> </div>
<TagList tags={post.tags} class="mt-2" /> <TagList tags={post.tags} class="mt-2" />
</div> </div>
<!-- Hero image --> <!-- Hero image -->
{post.image && ( {
<div class="relative mb-8 sm:mb-12 overflow-hidden rounded-xl shadow-lg"> post.image && (
<div class="aspect-[16/9] w-full"> <div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
<img <div class="aspect-[16/9] w-full">
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`} <img
alt={post.image_alt} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
class="w-full h-full object-cover" alt={post.image_alt}
loading="eager" 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>
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div> )
</div> }
)}
<div class="markdown-content"> <div class="markdown-content">
<slot /> <slot />
</div> </div>
<!-- Add the like button after the content --> <!-- Add the like button after the content -->
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800"> <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col sm:flex-row items-center justify-between gap-6"> <div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<ShareButtons url={canonicalURL.toString()} title={post.title} /> <!-- Convert URL to string --> <ShareButtons url={canonicalURL.toString()} title={post.title} />
<!-- Convert URL to string -->
</div> </div>
</div> </div>
{post.updated_date && ( {
<div class="mt-8 text-sm text-zinc-500 dark:text-zinc-400 italic"> post.updated_date && (
Last updated on <FormattedDate date={post.updated_date} /> <div class="mt-8 text-sm italic text-zinc-500 dark:text-zinc-400">
</div> Last updated on <FormattedDate date={post.updated_date} />
)} </div>
)
}
</article> </article>
<slot name="after-article" /> <slot name="after-article" />
</Layout> </Layout>
@@ -85,37 +93,40 @@ try {
const article = document.querySelector('article'); const article = document.querySelector('article');
if (article) { if (article) {
article.classList.add('article-entering'); article.classList.add('article-entering');
// Remove class after animation completes // Remove class after animation completes
setTimeout(() => { setTimeout(() => {
article.classList.remove('article-entering'); article.classList.remove('article-entering');
}, 1000); }, 1000);
} }
// Ensure consistent code block styling // Ensure consistent code block styling
function updateCodeBlockStyles() { function updateCodeBlockStyles() {
document.querySelectorAll('pre').forEach(pre => { document.querySelectorAll('pre').forEach((pre) => {
// Force the background color with !important for both light and dark mode // Force the background color with !important for both light and dark mode
pre.setAttribute('style', 'background-color: #1e293b !important'); pre.setAttribute('style', 'background-color: #1e293b !important');
// Also apply to any nested code elements // Also apply to any nested code elements
const codeElements = pre.querySelectorAll('code'); const codeElements = pre.querySelectorAll('code');
codeElements.forEach(code => { codeElements.forEach((code) => {
code.setAttribute('style', 'background-color: transparent !important; color: #e5e7eb !important;'); code.setAttribute(
'style',
'background-color: transparent !important; color: #e5e7eb !important;'
);
}); });
}); });
} }
// Initial application // Initial application
updateCodeBlockStyles(); updateCodeBlockStyles();
// Watch for theme changes // Watch for theme changes
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
updateCodeBlockStyles(); updateCodeBlockStyles();
}); });
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// Also run on any content changes that might add new code blocks // Also run on any content changes that might add new code blocks
const contentObserver = new MutationObserver((mutations) => { const contentObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) { for (const mutation of mutations) {
@@ -125,71 +136,74 @@ try {
} }
} }
}); });
contentObserver.observe(document.body, { childList: true, subtree: true }); contentObserver.observe(document.body, { childList: true, subtree: true });
// Clean up observers when navigating away // Clean up observers when navigating away
document.addEventListener('spa-navigation-start', () => { document.addEventListener('spa-navigation-start', () => {
observer.disconnect(); observer.disconnect();
contentObserver.disconnect(); contentObserver.disconnect();
}); });
// Remove the parallax effect for hero image // Remove the parallax effect for hero image
// Handle prev/next navigation links // Handle prev/next navigation links
const navLinks = document.querySelectorAll('.blog-nav-link'); const navLinks = document.querySelectorAll('.blog-nav-link');
navLinks.forEach(link => { navLinks.forEach((link) => {
if (!link.hasAttribute('data-spa-handled')) { if (!link.hasAttribute('data-spa-handled')) {
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('mouseenter', () => { link.addEventListener('mouseenter', () => {
link.classList.add('nav-link-hover'); link.classList.add('nav-link-hover');
}); });
link.addEventListener('mouseleave', () => { link.addEventListener('mouseleave', () => {
link.classList.remove('nav-link-hover'); link.classList.remove('nav-link-hover');
}); });
} }
}); });
// Animate headings when they enter the viewport // Animate headings when they enter the viewport
const animateHeadings = () => { const animateHeadings = () => {
const headings = document.querySelectorAll('article h2, article h3'); const headings = document.querySelectorAll('article h2, article h3');
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver(
entries.forEach(entry => { (entries) => {
if (entry.isIntersecting) { entries.forEach((entry) => {
entry.target.classList.add('heading-visible'); if (entry.isIntersecting) {
observer.unobserve(entry.target); entry.target.classList.add('heading-visible');
} observer.unobserve(entry.target);
}); }
}, { });
threshold: 0.2, },
rootMargin: '0px 0px -100px 0px' {
}); threshold: 0.2,
rootMargin: '0px 0px -100px 0px',
headings.forEach(heading => { }
);
headings.forEach((heading) => {
heading.classList.add('heading-animated'); heading.classList.add('heading-animated');
observer.observe(heading); observer.observe(heading);
}); });
return observer; return observer;
}; };
// Initialize heading animations // Initialize heading animations
const headingObserver = animateHeadings(); const headingObserver = animateHeadings();
// Enhance code blocks with syntax highlighting and copy button // Enhance code blocks with syntax highlighting and copy button
function enhanceCodeBlocks() { function enhanceCodeBlocks() {
const codeBlocks = document.querySelectorAll('pre code'); const codeBlocks = document.querySelectorAll('pre code');
codeBlocks.forEach(codeBlock => { codeBlocks.forEach((codeBlock) => {
// Skip if already processed // Skip if already processed
if (codeBlock.parentElement.classList.contains('enhanced')) return; if (codeBlock.parentElement.classList.contains('enhanced')) return;
// Mark as enhanced // Mark as enhanced
codeBlock.parentElement.classList.add('enhanced'); codeBlock.parentElement.classList.add('enhanced');
// Create copy button // Create copy button
const copyButton = document.createElement('button'); const copyButton = document.createElement('button');
copyButton.className = 'copy-code-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" /> <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> </svg>
`; `;
// Add copy functionality // Add copy functionality
copyButton.addEventListener('click', () => { copyButton.addEventListener('click', () => {
const code = codeBlock.textContent; const code = codeBlock.textContent;
navigator.clipboard.writeText(code); navigator.clipboard.writeText(code);
// Show copied feedback // Show copied feedback
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <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" /> <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> </svg>
`; `;
setTimeout(() => { setTimeout(() => {
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <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); }, 2000);
}); });
// Add copy button to pre element // Add copy button to pre element
codeBlock.parentElement.appendChild(copyButton); codeBlock.parentElement.appendChild(copyButton);
// Fix line numbers implementation // Fix line numbers implementation
const codeText = codeBlock.textContent; const codeText = codeBlock.textContent;
const lines = codeText.split('\n'); const lines = codeText.split('\n');
const lineNumbers = document.createElement('div'); const lineNumbers = document.createElement('div');
lineNumbers.className = 'line-numbers'; lineNumbers.className = 'line-numbers';
// Always include all lines, including empty ones // Always include all lines, including empty ones
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const lineNumber = document.createElement('span'); const lineNumber = document.createElement('span');
lineNumber.textContent = i + 1; lineNumber.textContent = i + 1;
lineNumbers.appendChild(lineNumber); lineNumbers.appendChild(lineNumber);
} }
codeBlock.parentElement.classList.add('with-line-numbers'); codeBlock.parentElement.classList.add('with-line-numbers');
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock); codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
// Fix language label detection and display // Fix language label detection and display
const className = codeBlock.className; const className = codeBlock.className;
const languageMatch = className.match(/language-(\w+)/); const languageMatch = className.match(/language-(\w+)/);
if (languageMatch && languageMatch[1]) { if (languageMatch && languageMatch[1]) {
const language = languageMatch[1]; const language = languageMatch[1];
// Add language label at top right // Add language label at top right
const languageLabel = document.createElement('div'); const languageLabel = document.createElement('div');
languageLabel.className = 'language-label'; languageLabel.className = 'language-label';
languageLabel.textContent = language; languageLabel.textContent = language;
codeBlock.parentElement.appendChild(languageLabel); codeBlock.parentElement.appendChild(languageLabel);
// Add language badge at bottom right with markdown syntax // Add language badge at bottom right with markdown syntax
const languageBadge = document.createElement('div'); const languageBadge = document.createElement('div');
languageBadge.className = 'language-badge'; languageBadge.className = 'language-badge';
@@ -267,28 +281,29 @@ try {
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)'; languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
languageBadge.style.color = '#e5e7eb'; languageBadge.style.color = '#e5e7eb';
languageBadge.style.borderRadius = '0.25rem'; languageBadge.style.borderRadius = '0.25rem';
languageBadge.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; languageBadge.style.fontFamily =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
languageBadge.style.zIndex = '10'; languageBadge.style.zIndex = '10';
codeBlock.parentElement.appendChild(languageBadge); codeBlock.parentElement.appendChild(languageBadge);
} }
}); });
} }
// Enhance tables with better styling // Enhance tables with better styling
function enhanceTables() { function enhanceTables() {
const tables = document.querySelectorAll('.markdown-content table'); const tables = document.querySelectorAll('.markdown-content table');
tables.forEach(table => { tables.forEach((table) => {
if (table.classList.contains('enhanced-table')) return; if (table.classList.contains('enhanced-table')) return;
table.classList.add('enhanced-table'); table.classList.add('enhanced-table');
// Wrap table in responsive container // Wrap table in responsive container
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'table-container'; wrapper.className = 'table-container';
table.parentNode.insertBefore(wrapper, table); table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table); wrapper.appendChild(table);
// Add zebra striping to rows // Add zebra striping to rows
const rows = table.querySelectorAll('tbody tr'); const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => { rows.forEach((row, index) => {
@@ -300,16 +315,16 @@ try {
}); });
}); });
} }
// Enhance blockquotes with icons // Enhance blockquotes with icons
function enhanceBlockquotes() { function enhanceBlockquotes() {
const blockquotes = document.querySelectorAll('.markdown-content blockquote'); const blockquotes = document.querySelectorAll('.markdown-content blockquote');
blockquotes.forEach(blockquote => { blockquotes.forEach((blockquote) => {
if (blockquote.classList.contains('enhanced-quote')) return; if (blockquote.classList.contains('enhanced-quote')) return;
blockquote.classList.add('enhanced-quote'); blockquote.classList.add('enhanced-quote');
// Add quote icon // Add quote icon
const icon = document.createElement('div'); const icon = document.createElement('div');
icon.className = 'quote-icon'; 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" /> <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> </svg>
`; `;
blockquote.insertBefore(icon, blockquote.firstChild); blockquote.insertBefore(icon, blockquote.firstChild);
}); });
} }
// Run all enhancements // Run all enhancements
enhanceCodeBlocks(); enhanceCodeBlocks();
enhanceTables(); enhanceTables();
enhanceBlockquotes(); enhanceBlockquotes();
// Clean up observers when navigating away // Clean up observers when navigating away
document.addEventListener('spa-navigation-start', () => { document.addEventListener('spa-navigation-start', () => {
if (headingObserver) { if (headingObserver) {
@@ -335,16 +350,16 @@ try {
} }
}); });
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions); document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupBlogPostTransitions); document.addEventListener('astro:page-load', setupBlogPostTransitions);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupBlogPostTransitions); document.addEventListener('page-transition-complete', setupBlogPostTransitions);
// Also initialize when SPA navigation completes // Also initialize when SPA navigation completes
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions); document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
</script> </script>
@@ -353,19 +368,21 @@ try {
/* Enhanced hero image styling */ /* Enhanced hero image styling */
article img:first-of-type { article img:first-of-type {
border-radius: 1rem; border-radius: 1rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
article img:first-of-type:hover { article img:first-of-type:hover {
transform: scale(1.01); transform: scale(1.01);
} }
/* Article entrance animation */ /* Article entrance animation */
.article-entering { .article-entering {
animation: article-fade-in 0.8s ease-out forwards; animation: article-fade-in 0.8s ease-out forwards;
} }
@keyframes article-fade-in { @keyframes article-fade-in {
from { from {
opacity: 0; opacity: 0;
@@ -376,6 +393,6 @@ try {
transform: translateY(0); transform: translateY(0);
} }
} }
/* Rest of the styles remain unchanged... */ /* Rest of the styles remain unchanged... */
</style> </style>

View File

@@ -5,14 +5,14 @@ import Background from '../components/Background.astro';
import '../styles/global.css'; import '../styles/global.css';
interface Props { interface Props {
title?: string | undefined; title?: string | undefined;
description?: string | undefined; description?: string | undefined;
} }
const { title, description } = Astro.props; const { title, description } = Astro.props;
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -21,33 +21,41 @@ const { title, description } = Astro.props;
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<title>{title}</title> <title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</head> </head>
<body class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 min-h-screen flex flex-col"> <body
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
>
<!-- Page transition overlay - for smooth transitions between pages --> <!-- Page transition overlay - for smooth transitions between pages -->
<div id="page-transition" class="fixed inset-0 z-40 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none transition-opacity duration-300 flex items-center justify-center"> <div
id="page-transition"
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
>
<div class="transition-spinner"></div> <div class="transition-spinner"></div>
</div> </div>
<!-- Background component with dot pattern and ambient glow --> <!-- Background component with dot pattern and ambient glow -->
<Background /> <Background />
<div class="max-w-3xl mx-auto px-4 sm:px-6 w-full flex-grow"> <div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6">
<Navigation /> <Navigation />
<main class="py-12"> <main class="py-12">
<slot /> <slot />
</main> </main>
</div> </div>
<Footer /> <Footer />
<script> <script>
// SPA transition system with history API // SPA transition system with history API
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
const mainContent = document.querySelector('main'); const mainContent = document.querySelector('main');
// Initialize content with entrance animation // Initialize content with entrance animation
if (mainContent) { if (mainContent) {
mainContent.classList.add('content-entering'); mainContent.classList.add('content-entering');
@@ -55,7 +63,7 @@ const { title, description } = Astro.props;
mainContent.classList.remove('content-entering'); mainContent.classList.remove('content-entering');
}, 800); }, 800);
} }
// Function to load content via fetch // Function to load content via fetch
async function loadContent(url) { async function loadContent(url) {
try { try {
@@ -64,52 +72,55 @@ const { title, description } = Astro.props;
pageTransition.classList.remove('opacity-0', 'pointer-events-none'); pageTransition.classList.remove('opacity-0', 'pointer-events-none');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
} }
// Fade out current content // Fade out current content
if (mainContent) { if (mainContent) {
mainContent.style.opacity = '0'; mainContent.style.opacity = '0';
mainContent.style.transform = 'translateY(10px)'; mainContent.style.transform = 'translateY(10px)';
} }
// Fetch the new page content // Fetch the new page content
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch ${url}`); if (!response.ok) throw new Error(`Failed to fetch ${url}`);
const html = await response.text(); const html = await response.text();
// Create a temporary element to parse the HTML // Create a temporary element to parse the HTML
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html'); const doc = parser.parseFromString(html, 'text/html');
// Extract the main content // Extract the main content
const newContent = doc.querySelector('main'); const newContent = doc.querySelector('main');
if (!newContent) throw new Error('Could not find main content in the fetched page'); if (!newContent) throw new Error('Could not find main content in the fetched page');
// Extract the title // Extract the title
const newTitle = doc.querySelector('title'); const newTitle = doc.querySelector('title');
if (newTitle) { if (newTitle) {
document.title = newTitle.textContent; document.title = newTitle.textContent;
} }
// Extract meta description // Extract meta description
const newDescription = doc.querySelector('meta[name="description"]'); const newDescription = doc.querySelector('meta[name="description"]');
if (newDescription) { if (newDescription) {
const currentDescription = document.querySelector('meta[name="description"]'); const currentDescription = document.querySelector('meta[name="description"]');
if (currentDescription) { if (currentDescription) {
currentDescription.setAttribute('content', newDescription.getAttribute('content') || ''); currentDescription.setAttribute(
'content',
newDescription.getAttribute('content') || ''
);
} }
} }
// Wait a bit for transition effect // Wait a bit for transition effect
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
// Replace the content // Replace the content
if (mainContent && newContent) { if (mainContent && newContent) {
mainContent.innerHTML = newContent.innerHTML; mainContent.innerHTML = newContent.innerHTML;
// Run scripts in the new content // Run scripts in the new content
Array.from(newContent.querySelectorAll('script')).forEach(oldScript => { Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
const newScript = document.createElement('script'); const newScript = document.createElement('script');
Array.from(oldScript.attributes).forEach(attr => { Array.from(oldScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value); newScript.setAttribute(attr.name, attr.value);
}); });
newScript.textContent = oldScript.textContent; newScript.textContent = oldScript.textContent;
@@ -118,17 +129,17 @@ const { title, description } = Astro.props;
} }
}); });
} }
// Fade in new content with animation // Fade in new content with animation
if (mainContent) { if (mainContent) {
mainContent.style.opacity = '0'; mainContent.style.opacity = '0';
mainContent.style.transform = 'translateY(10px)'; mainContent.style.transform = 'translateY(10px)';
setTimeout(() => { setTimeout(() => {
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
mainContent.style.opacity = '1'; mainContent.style.opacity = '1';
mainContent.style.transform = 'translateY(0)'; mainContent.style.transform = 'translateY(0)';
// Add entrance animation class // Add entrance animation class
mainContent.classList.add('content-entering'); mainContent.classList.add('content-entering');
setTimeout(() => { setTimeout(() => {
@@ -136,7 +147,7 @@ const { title, description } = Astro.props;
}, 800); }, 800);
}, 50); }, 50);
} }
// Hide transition overlay // Hide transition overlay
if (pageTransition) { if (pageTransition) {
setTimeout(() => { setTimeout(() => {
@@ -144,33 +155,34 @@ const { title, description } = Astro.props;
pageTransition.classList.remove('opacity-100'); pageTransition.classList.remove('opacity-100');
}, 200); }, 200);
} }
// Dispatch custom event for content loaded // Dispatch custom event for content loaded
document.dispatchEvent(new CustomEvent('spa-content-loaded', { document.dispatchEvent(
detail: { url } new CustomEvent('spa-content-loaded', {
})); detail: { url },
})
);
// Scroll to top or to saved position // Scroll to top or to saved position
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Re-attach event listeners to new content // Re-attach event listeners to new content
attachLinkListeners(); attachLinkListeners();
} catch (error) { } catch (error) {
console.error('Error loading content:', error); console.error('Error loading content:', error);
// Fallback to traditional navigation on error // Fallback to traditional navigation on error
window.location.href = url; window.location.href = url;
} }
} }
// Function to attach event listeners to all links // Function to attach event listeners to all links
function attachLinkListeners() { function attachLinkListeners() {
document.querySelectorAll('a').forEach(link => { document.querySelectorAll('a').forEach((link) => {
// Skip links that are already handled, anchor links, external links, or have special attributes // Skip links that are already handled, anchor links, external links, or have special attributes
if ( if (
link.hasAttribute('data-spa-handled') || link.hasAttribute('data-spa-handled') ||
!link.href.startsWith(window.location.origin) || !link.href.startsWith(window.location.origin) ||
link.href.includes('#') || link.href.includes('#') ||
link.hasAttribute('target') || link.hasAttribute('target') ||
link.hasAttribute('download') || link.hasAttribute('download') ||
@@ -179,36 +191,36 @@ const { title, description } = Astro.props;
) { ) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.href; const targetHref = link.href;
// Don't transition if clicking the current page // Don't transition if clicking the current page
if (targetHref === window.location.href) { if (targetHref === window.location.href) {
return; return;
} }
// Update browser history // Update browser history
window.history.pushState({ path: targetHref }, '', targetHref); window.history.pushState({ path: targetHref }, '', targetHref);
// Load the new content // Load the new content
loadContent(targetHref); loadContent(targetHref);
}); });
}); });
} }
// Initial attachment of link listeners // Initial attachment of link listeners
attachLinkListeners(); attachLinkListeners();
// Handle browser back/forward navigation // Handle browser back/forward navigation
window.addEventListener('popstate', (e) => { window.addEventListener('popstate', (e) => {
if (e.state && e.state.path) { if (e.state && e.state.path) {
@@ -217,7 +229,7 @@ const { title, description } = Astro.props;
loadContent(window.location.href); loadContent(window.location.href);
} }
}); });
// Check RSS feed availability // Check RSS feed availability
const checkAndGenerateRSS = async () => { const checkAndGenerateRSS = async () => {
try { try {
@@ -229,33 +241,36 @@ const { title, description } = Astro.props;
console.warn('Could not check RSS feed status.'); console.warn('Could not check RSS feed status.');
} }
}; };
// Check RSS feed availability // Check RSS feed availability
checkAndGenerateRSS(); checkAndGenerateRSS();
}); });
// Theme handling with transition effects // Theme handling with transition effects
function setupThemeHandling() { function setupThemeHandling() {
// Apply theme from localStorage or system preference // Apply theme from localStorage or system preference
const theme = localStorage.getItem('theme'); const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
// Listen for theme changes // Listen for theme changes
document.addEventListener('themeChanged', () => { document.addEventListener('themeChanged', () => {
// Add transition class to body // Add transition class to body
document.body.classList.add('theme-transitioning'); document.body.classList.add('theme-transitioning');
// Remove class after transition completes // Remove class after transition completes
setTimeout(() => { setTimeout(() => {
document.body.classList.remove('theme-transitioning'); document.body.classList.remove('theme-transitioning');
}, 500); }, 500);
}); });
} }
// Initialize theme handling // Initialize theme handling
document.addEventListener('DOMContentLoaded', setupThemeHandling); document.addEventListener('DOMContentLoaded', setupThemeHandling);
</script> </script>
@@ -268,7 +283,7 @@ const { title, description } = Astro.props;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
/* Transition spinner animation */ /* Transition spinner animation */
.transition-spinner { .transition-spinner {
width: 30px; width: 30px;
@@ -278,27 +293,31 @@ const { title, description } = Astro.props;
border-top-color: #3b82f6; border-top-color: #3b82f6;
animation: spin 0.7s linear infinite; animation: spin 0.7s linear infinite;
} }
:global(.dark) .transition-spinner { :global(.dark) .transition-spinner {
border-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.1);
border-top-color: #60a5fa; border-top-color: #60a5fa;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Content entrance animation */ /* Content entrance animation */
main { main {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
transition: opacity 0.5s ease, transform 0.5s ease; transition:
opacity 0.5s ease,
transform 0.5s ease;
} }
main.content-entering { main.content-entering {
animation: content-fade-in 0.6s ease forwards; animation: content-fade-in 0.6s ease forwards;
} }
@keyframes content-fade-in { @keyframes content-fade-in {
from { from {
opacity: 0; opacity: 0;
@@ -309,7 +328,7 @@ const { title, description } = Astro.props;
transform: translateY(0); transform: translateY(0);
} }
} }
/* Theme transition effect */ /* Theme transition effect */
body.theme-transitioning * { body.theme-transitioning * {
transition-duration: 0.3s !important; transition-duration: 0.3s !important;

View File

@@ -7,7 +7,7 @@ const { title, description } = Astro.props;
<BaseLayout title={title} description={description}> <BaseLayout title={title} description={description}>
<ViewTransitions fallback="swap" /> <ViewTransitions fallback="swap" />
<div transition:animate="slide"> <div transition:animate="slide">
<slot /> <slot />
</div> </div>
@@ -18,7 +18,7 @@ const { title, description } = Astro.props;
::view-transition-old(root) { ::view-transition-old(root) {
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left; animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
} }
::view-transition-new(root) { ::view-transition-new(root) {
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right; 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

View File

@@ -3,54 +3,100 @@ import Layout from '../layouts/Layout.astro';
--- ---
<Layout title="404 - Page Not Found"> <Layout title="404 - Page Not Found">
<div class="relative flex flex-col items-center justify-center min-h-[80vh] py-20 text-center px-4 overflow-hidden"> <div
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
>
<!-- Animated background elements --> <!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-20 -left-20 w-64 h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob"></div> <div
<div class="absolute top-1/2 right-1/4 w-96 h-96 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div> class="animate-blob absolute -left-20 -top-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
<div class="absolute bottom-20 left-1/3 w-72 h-72 bg-zinc-100 dark:bg-zinc-800/40 rounded-full blur-3xl opacity-40 animate-blob animation-delay-4000"></div> >
</div>
<!-- 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> </div>
<div
<h2 class="mt-6 text-2xl sm:text-3xl font-bold text-zinc-800 dark:text-zinc-200">Page Not Found</h2> 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"
>
<p class="mt-6 text-zinc-600 dark:text-zinc-400 max-w-md mx-auto text-lg"> </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. The page you're looking for does not exist.
</p> </p>
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4"> <div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a <a
href="/" href="/"
class="group relative inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-xl" class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
> >
<span class="absolute inset-0 bg-gradient-to-r from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-0"></span> <span
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 relative z-10"> class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> ></span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="relative z-10 h-5 w-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
></path>
</svg> </svg>
<span class="font-medium relative z-10">Return Home</span> <span class="relative z-10 font-medium">Return Home</span>
</a> </a>
<button <button
id="back-button" id="back-button"
class="group inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 shadow-sm hover:shadow-md" class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-sm transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
</svg> </svg>
<span class="font-medium">Go Back</span> <span class="font-medium">Go Back</span>
</button> </button>
</div> </div>
<!-- Random fun fact --> <!-- Random fun fact -->
<div class="mt-16 p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl shadow-sm max-w-md mx-auto backdrop-blur-sm border border-zinc-100 dark:border-zinc-700/50"> <div
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Did you know?</h3> class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50"
<p class="mt-2 text-zinc-700 dark:text-zinc-300 text-sm" id="fun-fact"> >
The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found. <h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
Did you know?
</h3>
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found.
</p> </p>
</div> </div>
</div> </div>
@@ -62,55 +108,57 @@ import Layout from '../layouts/Layout.astro';
document.getElementById('back-button')?.addEventListener('click', () => { document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back(); window.history.back();
}); });
// Array of fun 404 facts // Array of fun 404 facts
const funFacts = [ 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.", "The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.", "In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.", "Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
"The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.", 'The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.',
"Many companies use creative 404 pages as a way to showcase their brand personality and humor.", 'Many companies use creative 404 pages as a way to showcase their brand personality and humor.',
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.", "The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
"Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.", 'Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.',
"The most common cause of 404 errors is mistyped URLs." 'The most common cause of 404 errors is mistyped URLs.',
]; ];
// Display a random fun fact // Display a random fun fact
const funFactElement = document.getElementById('fun-fact'); const funFactElement = document.getElementById('fun-fact');
if (funFactElement) { if (funFactElement) {
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
funFactElement.textContent = randomFact; funFactElement.textContent = randomFact;
} }
// Handle SPA transitions for 404 page // Handle SPA transitions for 404 page
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('target') === '_blank' || link.getAttribute('href').includes('#') ||
link.hasAttribute('data-spa-handled')) { link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.getAttribute('href'); const targetHref = link.getAttribute('href');
// Trigger page transition animation // Trigger page transition animation
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
if (pageTransition) { if (pageTransition) {
pageTransition.classList.remove('opacity-0'); pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
// Navigate after transition effect // Navigate after transition effect
setTimeout(() => { setTimeout(() => {
window.location.href = targetHref; window.location.href = targetHref;
@@ -121,7 +169,7 @@ import Layout from '../layouts/Layout.astro';
} }
}); });
}); });
// Re-initialize back button after SPA navigation // Re-initialize back button after SPA navigation
const backButton = document.getElementById('back-button'); const backButton = document.getElementById('back-button');
if (backButton) { if (backButton) {
@@ -130,13 +178,13 @@ import Layout from '../layouts/Layout.astro';
}); });
} }
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions); document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions); document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions); document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>
@@ -157,31 +205,31 @@ import Layout from '../layouts/Layout.astro';
transform: translate(0px, 0px) scale(1); transform: translate(0px, 0px) scale(1);
} }
} }
.animate-blob { .animate-blob {
animation: blob 7s infinite; animation: blob 7s infinite;
} }
.animation-delay-2000 { .animation-delay-2000 {
animation-delay: 2s; animation-delay: 2s;
} }
.animation-delay-4000 { .animation-delay-4000 {
animation-delay: 4s; animation-delay: 4s;
} }
/* Glitch effect for 404 text */ /* Glitch effect for 404 text */
.glitch-wrapper { .glitch-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-block;
} }
.glitch { .glitch {
position: relative; position: relative;
display: inline-block; display: inline-block;
animation: glitch-skew 1s infinite linear alternate-reverse; animation: glitch-skew 1s infinite linear alternate-reverse;
} }
.glitch::before, .glitch::before,
.glitch::after { .glitch::after {
content: attr(data-text); content: attr(data-text);
@@ -191,20 +239,22 @@ import Layout from '../layouts/Layout.astro';
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.glitch::before { .glitch::before {
left: 2px; left: 2px;
text-shadow: -2px 0 #ff00c1; text-shadow: -2px 0 #ff00c1;
clip: rect(44px, 450px, 56px, 0); clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim 5s infinite linear alternate-reverse; animation: glitch-anim 5s infinite linear alternate-reverse;
} }
.glitch::after { .glitch::after {
left: -2px; left: -2px;
text-shadow: -2px 0 #00fff9, 2px 2px #ff00c1; text-shadow:
-2px 0 #00fff9,
2px 2px #ff00c1;
animation: glitch-anim2 1s infinite linear alternate-reverse; animation: glitch-anim2 1s infinite linear alternate-reverse;
} }
@keyframes glitch-anim { @keyframes glitch-anim {
0% { 0% {
clip: rect(31px, 9999px, 94px, 0); clip: rect(31px, 9999px, 94px, 0);
@@ -291,7 +341,7 @@ import Layout from '../layouts/Layout.astro';
transform: skew(0.6deg); transform: skew(0.6deg);
} }
} }
@keyframes glitch-anim2 { @keyframes glitch-anim2 {
0% { 0% {
clip: rect(65px, 9999px, 65px, 0); clip: rect(65px, 9999px, 65px, 0);
@@ -378,7 +428,7 @@ import Layout from '../layouts/Layout.astro';
transform: skew(0.74deg); transform: skew(0.74deg);
} }
} }
@keyframes glitch-skew { @keyframes glitch-skew {
0% { 0% {
transform: skew(-1deg); transform: skew(-1deg);
@@ -414,4 +464,4 @@ import Layout from '../layouts/Layout.astro';
transform: skew(0deg); transform: skew(0deg);
} }
} }
</style> </style>

View File

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

View File

@@ -1,97 +1,144 @@
--- ---
import BlogPost from '../../layouts/BlogPost.astro'; import BlogPost from '../../layouts/BlogPost.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems("posts", { const posts = await directus.request(
fields: ['*'], readItems('posts', {
})); fields: ['*'],
})
);
const sortedEntries = [...posts].sort( const sortedEntries = [...posts].sort(
(a, b) => b.published_date.valueOf() - a.published_date.valueOf() (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
); );
return sortedEntries.map((post, index) => { return sortedEntries.map((post, index) => {
return { return {
params: { slug: post.slug }, params: { slug: post.slug },
props: { props: {
post, post,
nextPost: index > 0 ? sortedEntries[index - 1] : null, nextPost: index > 0 ? sortedEntries[index - 1] : null,
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
}, },
}; };
}); });
} }
const { post, nextPost, prevPost } = Astro.props; const { post, nextPost, prevPost } = Astro.props;
--- ---
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}> <BlogPost
<!-- Main Content - Enhanced with better typography and spacing --> slug={post.slug}
<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"> title={post.title}
<div set:html={post.content} /> description={post.description}
</div> 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 --> <!-- Next/Previous Navigation - Improved responsive design -->
<div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12"> <div
{prevPost && ( class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2"
<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" 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> <div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2"> <span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1"> <svg
<path d="m15 18-6-6 6-6"></path> xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
>
<path d="m15 18-6-6 6-6" />
</svg> </svg>
Previous Article Previous Article
</span> </span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors"> <h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{prevPost.title} {prevPost.title}
</h3> </h3>
</a> </a>
)} )
{nextPost && ( }
<a {
href={`/blog/${nextPost.slug}`} nextPost && (
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" <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> <div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end"> <span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
Next Article Next Article
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path d="m9 18 6-6-6-6"></path> xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
>
<path d="m9 18 6-6-6-6" />
</svg> </svg>
</span> </span>
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors"> <h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{nextPost.title} {nextPost.title}
</h3> </h3>
</a> </a>
)} )
</div> }
</div>
</BlogPost> </BlogPost>
<script> <script>
// Removing TOC-related functions // Removing TOC-related functions
// Add copy buttons to code blocks // Add copy buttons to code blocks
function initializeCodeCopyButtons() { function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre'); const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach(block => { codeBlocks.forEach((block) => {
// Skip if already processed by either method // Skip if already processed by either method
if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) { if (
block.classList.contains('code-block-processed') ||
block.classList.contains('enhanced')
) {
return; return;
} }
block.classList.add('code-block-processed'); block.classList.add('code-block-processed');
// Create wrapper if not already wrapped // Create wrapper if not already wrapped
let wrapper; let wrapper;
if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) { if (
block.parentNode.classList.contains('relative') &&
block.parentNode.classList.contains('group')
) {
wrapper = block.parentNode; wrapper = block.parentNode;
} else { } else {
wrapper = document.createElement('div'); wrapper = document.createElement('div');
@@ -99,28 +146,29 @@ const { post, nextPost, prevPost } = Astro.props;
block.parentNode.insertBefore(wrapper, block); block.parentNode.insertBefore(wrapper, block);
wrapper.appendChild(block); wrapper.appendChild(block);
} }
// Add copy button if not already present // Add copy button if not already present
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) { if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
const copyButton = document.createElement('button'); const copyButton = document.createElement('button');
copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200'; copyButton.className =
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg> </svg>
`; `;
copyButton.addEventListener('click', () => { copyButton.addEventListener('click', () => {
const code = block.querySelector('code').innerText; const code = block.querySelector('code').innerText;
navigator.clipboard.writeText(code); navigator.clipboard.writeText(code);
// Show copied feedback // Show copied feedback
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg> </svg>
`; `;
setTimeout(() => { setTimeout(() => {
copyButton.innerHTML = ` copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
@@ -129,39 +177,39 @@ const { post, nextPost, prevPost } = Astro.props;
`; `;
}, 2000); }, 2000);
}); });
wrapper.appendChild(copyButton); wrapper.appendChild(copyButton);
} }
}); });
} }
// Handle SPA transitions for blog post navigation // Handle SPA transitions for blog post navigation
function setupSPATransitions() { function setupSPATransitions() {
// Handle prev/next navigation links // Handle prev/next navigation links
document.querySelectorAll('a[href^="/blog/"]').forEach(link => { document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed // Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) { if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.getAttribute('href'); const targetHref = link.getAttribute('href');
// Trigger page transition animation // Trigger page transition animation
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
if (pageTransition) { if (pageTransition) {
pageTransition.classList.remove('opacity-0'); pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
// Navigate after transition effect // Navigate after transition effect
setTimeout(() => { setTimeout(() => {
window.location.href = targetHref; window.location.href = targetHref;
@@ -173,13 +221,13 @@ const { post, nextPost, prevPost } = Astro.props;
}); });
}); });
} }
// Main initialization function // Main initialization function
function initializeBlogPost() { function initializeBlogPost() {
// Initialize remaining components // Initialize remaining components
initializeCodeCopyButtons(); initializeCodeCopyButtons();
setupSPATransitions(); setupSPATransitions();
// Scroll to hash if present in URL // Scroll to hash if present in URL
if (window.location.hash) { if (window.location.hash) {
setTimeout(() => { setTimeout(() => {
@@ -190,100 +238,105 @@ const { post, nextPost, prevPost } = Astro.props;
}, 100); }, 100);
} }
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', initializeBlogPost); document.addEventListener('DOMContentLoaded', initializeBlogPost);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', initializeBlogPost); document.addEventListener('astro:page-load', initializeBlogPost);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', initializeBlogPost); document.addEventListener('page-transition-complete', initializeBlogPost);
</script> </script>
<style> <style>
/* Removing TOC-related styles */ /* Removing TOC-related styles */
/* Language badge styling */ /* Language badge styling */
.language-badge { .language-badge {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
text-transform: lowercase; text-transform: lowercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
/* Extra small screens */ /* Extra small screens */
@media (min-width: 480px) { @media (min-width: 480px) {
.xs\:inline { .xs\:inline {
display: inline; display: inline;
} }
.xs\:hidden { .xs\:hidden {
display: none; display: none;
} }
} }
/* Enhanced typography for blog content - Responsive adjustments */ /* Enhanced typography for blog content - Responsive adjustments */
.prose { .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 { .prose h1,
@apply text-zinc-900 dark:text-zinc-100 font-semibold; .prose h2,
.prose h3,
.prose h4 {
@reference font-semibold text-zinc-900 dark:text-zinc-100;
} }
.prose h1 { .prose h1 {
@apply text-2xl sm:text-3xl md:text-4xl; @reference text-2xl sm:text-3xl md:text-4xl;
} }
.prose h2 { .prose h2 {
@apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800; @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 { .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 { .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 { .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 { .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 { .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 { .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 { .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 { .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 { .prose ul,
@apply my-4 sm:my-6 pl-5 sm:pl-6; .prose ol {
@reference my-4 pl-5 sm:my-6 sm:pl-6;
} }
.prose li { .prose li {
@apply mb-1 sm:mb-2 text-sm sm:text-base; @reference mb-1 text-sm sm:mb-2 sm:text-base;
} }
.prose hr { .prose hr {
@apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800; @reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
} }
/* Line clamp for truncating text */ /* Line clamp for truncating text */
.line-clamp-2 { .line-clamp-2 {
display: -webkit-box; display: -webkit-box;
@@ -291,4 +344,4 @@ const { post, nextPost, prevPost } = Astro.props;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@@ -1,19 +1,17 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
const sortedPosts = posts.sort( const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
);
// Group posts by year for timeline effect // Group posts by year for timeline effect
const postsByYear = sortedPosts.reduce((acc, post) => { const postsByYear = sortedPosts.reduce((acc, post) => {
@@ -29,176 +27,206 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
const totalPosts = sortedPosts.length; const totalPosts = sortedPosts.length;
// Get unique tags for search suggestions // Get unique tags for search suggestions
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))]; const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
--- ---
<BaseLayout title="Blog"> <BaseLayout title="Blog">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16"> <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
<!-- Header with search --> <!-- Header with search -->
<div class="relative mb-12 sm:mb-20"> <div class="relative mb-12 sm:mb-20">
<!-- Decorative elements --> <!-- Decorative elements -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div> <div
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div> class="animate-blob absolute -left-10 -top-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-72 sm:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-72 sm:w-72"
>
</div>
<div class="relative text-center"> <div class="relative text-center">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4"> <h1
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl"
>
Blog Blog
</h1> </h1>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto"> <p
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:mb-10 sm:text-base"
>
Thoughts, ideas, and explorations on technology and selfhosting. Thoughts, ideas, and explorations on technology and selfhosting.
</p> </p>
</div> </div>
</div> </div>
<!-- Grid layout for mobile experience --> <!-- Grid layout for mobile experience -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8"> <div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
<!-- Featured post (if exists) --> <!-- Featured post (if exists) -->
{sortedPosts.length > 0 && ( {
<div class="md:col-span-12 mb-8 sm:mb-12"> sortedPosts.length > 0 && (
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8"> <div class="mb-8 sm:mb-12 md:col-span-12">
<div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8"> <article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 dark:border-zinc-800 sm:pb-8">
{sortedPosts[0].image && ( <div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
<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"> {sortedPosts[0].image && (
<img <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">
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`} <img
alt={sortedPosts[0].title} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105" alt={sortedPosts[0].title}
loading="eager" class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
/> loading="eager"
</div> />
)} </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"> <div class="flex flex-1 flex-col justify-center">
<span class="font-medium uppercase tracking-wider">Featured</span> <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="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span> <span class="font-medium uppercase tracking-wider">Featured</span>
{sortedPosts[0].published_date && ( <span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
<time datetime={sortedPosts[0].published_date.toLocaleString()}> {sortedPosts[0].published_date && (
{sortedPosts[0].published_date.toLocaleString('en-US', { <time datetime={sortedPosts[0].published_date.toLocaleString()}>
year: 'numeric', {sortedPosts[0].published_date.toLocaleString('en-US', {
month: 'long', year: 'numeric',
day: 'numeric' month: 'long',
})} day: 'numeric',
</time> })}
)} </time>
</div> )}
</div>
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
<a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0"> <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">
{sortedPosts[0].title} <a
</a> href={`/blog/${sortedPosts[0].slug}/`}
</h2> class="before:absolute before:inset-0"
>
<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].title}
{sortedPosts[0].description} </a>
</p> </h2>
<!-- Improved mobile layout for featured post metadata --> <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">
<div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap"> {sortedPosts[0].description}
{sortedPosts[0].tags && ( </p>
<div class="flex flex-wrap gap-2 justify-center md:justify-start">
{sortedPosts[0].tags.slice(0, 2).map((tag) => ( <div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
<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[0].tags && (
{tag} <div class="flex flex-wrap justify-center gap-2 md:justify-start">
</span> {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">
</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">
{tag} {tag}
</span> </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>
)} )}
</div> </div>
</article> </div>
))} </div>
</div> </article>
</div> </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> </div>
</div> </div>
@@ -209,13 +237,14 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
.animate-blob { .animate-blob {
animation: blob-bounce 8s infinite ease; animation: blob-bounce 8s infinite ease;
} }
.animation-delay-2000 { .animation-delay-2000 {
animation-delay: 2s; animation-delay: 2s;
} }
@keyframes blob-bounce { @keyframes blob-bounce {
0%, 100% { 0%,
100% {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
25% { 25% {
@@ -228,36 +257,37 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
transform: translate(-5%, 5%) scale(0.95); transform: translate(-5%, 5%) scale(0.95);
} }
} }
/* Search container hover effect */ /* Search container hover effect */
.search-container:hover .search-pulse { .search-container:hover .search-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 0; opacity: 0;
} }
50% { 50% {
opacity: 1; opacity: 1;
} }
} }
/* Input focus animation */ /* Input focus animation */
input:focus + div .search-pulse { input:focus + div .search-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
/* Hide scrollbar but keep functionality */ /* Hide scrollbar but keep functionality */
.hide-scrollbar { .hide-scrollbar {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
} }
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
/* Line clamp for descriptions */ /* Line clamp for descriptions */
.line-clamp-2 { .line-clamp-2 {
display: -webkit-box; display: -webkit-box;
@@ -265,17 +295,18 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.line-clamp-3 { .line-clamp-3 {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
/* Improved touch targets for mobile */ /* Improved touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, button { a,
button {
min-height: 44px; min-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -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 // Script không thay đổi - giữ nguyên chức năng
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const backToTopButton = document.getElementById('back-to-top'); const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) { if (backToTopButton) {
// Show button when scrolled down // Show button when scrolled down
const toggleBackToTopButton = () => { const toggleBackToTopButton = () => {
@@ -299,50 +330,50 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
backToTopButton.classList.add('opacity-0', 'invisible'); backToTopButton.classList.add('opacity-0', 'invisible');
} }
}; };
// Scroll to top when clicked // Scroll to top when clicked
backToTopButton.addEventListener('click', () => { backToTopButton.addEventListener('click', () => {
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: 'smooth',
}); });
}); });
// Check scroll position // Check scroll position
window.addEventListener('scroll', toggleBackToTopButton); window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton(); // Initial check toggleBackToTopButton(); // Initial check
} }
// Add smooth scrolling to year links // Add smooth scrolling to year links
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => { document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) { anchor.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const targetId = this.getAttribute('href'); const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId); const targetElement = document.querySelector(targetId);
if (targetElement) { if (targetElement) {
window.scrollTo({ window.scrollTo({
top: targetElement.offsetTop - 100, top: targetElement.offsetTop - 100,
behavior: 'smooth' behavior: 'smooth',
}); });
// Update URL hash without jumping // Update URL hash without jumping
history.pushState(null, null, targetId); history.pushState(null, null, targetId);
} }
}); });
}); });
// Add touch support for hover effects // Add touch support for hover effects
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) { if (isTouchDevice) {
const articles = document.querySelectorAll('article'); const articles = document.querySelectorAll('article');
articles.forEach(article => { articles.forEach((article) => {
article.addEventListener('touchstart', () => { article.addEventListener('touchstart', () => {
article.classList.add('is-touched'); article.classList.add('is-touched');
}); });
article.addEventListener('touchend', () => { article.addEventListener('touchend', () => {
setTimeout(() => { setTimeout(() => {
article.classList.remove('is-touched'); article.classList.remove('is-touched');
@@ -351,34 +382,34 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
}); });
} }
}); });
// SPA transition handling // SPA transition handling
function setupSPATransitions() { function setupSPATransitions() {
// Handle all blog post links for SPA transitions // Handle all blog post links for SPA transitions
document.querySelectorAll('a[href^="/blog/"]').forEach(link => { document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed // Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) { if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.getAttribute('href'); const targetHref = link.getAttribute('href');
// Trigger page transition animation // Trigger page transition animation
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
if (pageTransition) { if (pageTransition) {
pageTransition.classList.remove('opacity-0'); pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
// Navigate after transition effect // Navigate after transition effect
setTimeout(() => { setTimeout(() => {
window.location.href = targetHref; window.location.href = targetHref;
@@ -389,19 +420,19 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
} }
}); });
}); });
// Handle year anchor links specially // Handle year anchor links specially
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => { document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.setAttribute('data-spa-internal', 'true'); anchor.setAttribute('data-spa-internal', 'true');
}); });
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions); document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions); document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions); document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>

View File

@@ -2,15 +2,15 @@
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import FormattedDate from '../components/FormattedDate.astro'; import FormattedDate from '../components/FormattedDate.astro';
import directus from "../../lib/directus" import directus from '../../lib/directus';
import { readItems,readSingleton } from "@directus/sdk"; import { readItems, readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton("global")); const global = await directus.request(readSingleton('global'));
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
@@ -18,42 +18,67 @@ const recentPosts = posts
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime()) .sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
.slice(0, 3); .slice(0, 3);
const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5); const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
--- ---
<Layout title=`Home | ${global.name}`> <Layout title=`Home | ${global.name}`>
<!-- Hero Section with improved mobile responsiveness --> <!-- Hero Section with improved mobile responsiveness -->
<section class="py-10 sm:py-16 md:py-20 px-4 sm:px-6 theme-transition-all"> <section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
<div class="max-w-2xl mx-auto relative"> <div class="relative mx-auto max-w-2xl">
<!-- Adjusted blob positions and sizes for better mobile appearance --> <!-- Adjusted blob positions and sizes for better mobile appearance -->
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div> <div
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div> class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:-left-20 sm:-top-20 sm:h-64 sm:w-64"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-64 sm:w-64"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color hero-text"> <h1
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl lg:text-6xl"
>
<span class="block">Writing on technology,</span> <span class="block">Writing on technology,</span>
<span class="block mt-1">development, and</span> <span class="mt-1 block">development, and</span>
<span class="block mt-1 relative"> <span class="relative mt-1 block">
<span class="relative inline-block"> <span class="relative inline-block">
selfhosting. selfhosting.
<span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-800 dark:bg-zinc-200 transform origin-left theme-transition-bg"></span> <span
class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200"
></span>
</span> </span>
</span> </span>
</h1> </h1>
<p class="mt-4 sm:mt-6 md:mt-8 text-base sm:text-lg text-zinc-600 dark:text-zinc-400 leading-relaxed theme-transition-color max-w-lg mx-auto sm:mx-0"> <p
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8"
>
{global.about} {global.about}
</p> </p>
<div class="mt-6 sm:mt-8 md:mt-10 flex flex-wrap gap-3 sm:gap-4 md:gap-6 justify-center sm:justify-start"> <div
<a 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"
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]" <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> <span>More about me</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </svg>
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span> <span
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
></span>
</a> </a>
</div> </div>
</div> </div>
@@ -61,139 +86,194 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
</section> </section>
<!-- Featured Post Section - Improved for mobile --> <!-- Featured Post Section - Improved for mobile -->
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> <section
<div class="max-w-3xl mx-auto"> class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16"
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8 md:mb-12"> >
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color text-center sm:text-left">Recent Posts</h2> <div class="mx-auto max-w-3xl">
<a <div
href="/blog" class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
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" >
<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"> <span class="flex items-center gap-1">
View all posts View all posts
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </svg>
</span> </span>
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span> <span
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
></span>
</a> </a>
</div> </div>
<!-- Improved grid for better mobile layout --> <!-- Improved grid for better mobile layout -->
<div class="grid gap-6 sm:gap-8 md:gap-12 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
{recentPosts.map((post, index) => ( {
<article class="group relative flex flex-col items-start hover-3d theme-transition-element max-w-sm mx-auto sm:mx-0 w-full"> recentPosts.map((post, index) => (
<div class="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 dark:bg-zinc-800/50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl theme-transition-bg"></div> <article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
{post.image && (
<div class="relative z-10 w-full aspect-video mb-4 overflow-hidden rounded-lg"> {post.image && (
<img <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`} <img
alt={post.title} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" alt={post.title}
loading={index === 0 ? "eager" : "lazy"} class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
width="400" loading={index === 0 ? 'eager' : 'lazy'}
height="225" 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>
)}
<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">
<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"> <a
<time datetime={post.published_date.toLocaleString()} class="font-medium"> href={`/blog/${post.slug}`}
<FormattedDate date={post.published_date} /> class="flex min-h-[44px] items-center justify-center sm:justify-start"
</time> >
</div> <span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
{post.title}
<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>
<a href={`/blog/${post.slug}`} class="min-h-[44px] flex items-center justify-center sm:justify-start"> </h3>
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4"></span>
{post.title} <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">
</a> {post.description}
</h3> </p>
<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.tags && post.tags.length > 0 && (
{post.description} <div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
</p> {post.tags.slice(0, 3).map((tag) => (
<a
{post.tags && post.tags.length > 0 && ( href={`/topics/${tag}`}
<div class="relative z-10 mt-3 sm:mt-4 flex flex-wrap gap-2 justify-center sm:justify-start w-full"> 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"
{post.tags.slice(0, 3).map(tag => ( >
<a #{tag}
href={`/topics/${tag}`} </a>
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]" ))}
> {post.tags.length > 3 && (
#{tag} <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">
</a> +{post.tags.length - 3} more
))} </span>
{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]"> </div>
+{post.tags.length - 3} more )}
<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> </span>
)} <span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
</div> Explore now
)} </span>
</span>
<a <svg
href={`/blog/${post.slug}`} viewBox="0 0 16 16"
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]" fill="none"
> aria-hidden="true"
<span class="relative overflow-hidden inline-block"> class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
<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> <path
</span> d="M6.75 5.75 9.25 8l-2.5 2.25"
<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"> stroke-width="1.5"
<path d="M6.75 5.75 9.25 8l-2.5 2.25" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> stroke-linecap="round"
</svg> stroke-linejoin="round"
</a> />
</article> </svg>
))} </a>
</article>
))
}
</div> </div>
</div> </div>
</section> </section>
<!-- Topics/Tags Section - Improved for mobile --> <!-- Topics/Tags Section - Improved for mobile -->
{allTags.length > 0 && ( {
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all"> allTags.length > 0 && (
<div class="max-w-3xl mx-auto"> <section class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16">
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 theme-transition-color text-center sm:text-left">Explore Topics</h2> <div class="mx-auto max-w-3xl">
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl">
<!-- Improved grid layout for mobile --> Explore Topics
<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"> </h2>
{allTags.map(tag => {
const tagCount = posts.filter(post => post.tags && post.tags.includes(tag)).length; <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">
return ( {allTags.map((tag) => {
<a const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
href={`/topics/${tag}`} return (
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]" <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"> <path
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100 theme-transition-color mr-2">#{tag}</span> stroke-linecap="round"
<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"> stroke-linejoin="round"
{tagCount} {tagCount === 1 ? 'post' : 'posts'} d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
</span> />
</div> </svg>
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1 theme-transition-color"> </a>
Explore articles about {tag} </div>
</p>
</a>
)
})}
</div> </div>
</section>
<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>
)}
</Layout> </Layout>
<script> <script>
@@ -201,69 +281,69 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Check if it's a touch device // Check if it's a touch device
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) { if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d'); const cards = document.querySelectorAll('.hover-3d');
cards.forEach(card => { cards.forEach((card) => {
card.addEventListener('touchstart', () => { card.addEventListener('touchstart', () => {
card.classList.add('is-touched'); card.classList.add('is-touched');
}); });
card.addEventListener('touchend', () => { card.addEventListener('touchend', () => {
setTimeout(() => { setTimeout(() => {
card.classList.remove('is-touched'); card.classList.remove('is-touched');
}, 300); }, 300);
}); });
}); });
// Disable hover animations on touch devices for better performance // Disable hover animations on touch devices for better performance
document.documentElement.classList.add('touch-device'); document.documentElement.classList.add('touch-device');
} }
// Improved viewport height fix for mobile browsers // Improved viewport height fix for mobile browsers
const setVh = () => { const setVh = () => {
const vh = window.innerHeight * 0.01; const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`); document.documentElement.style.setProperty('--vh', `${vh}px`);
}; };
// Set initial value // Set initial value
setVh(); setVh();
// Update on resize and scroll to prevent content shifting // Update on resize and scroll to prevent content shifting
window.addEventListener('resize', setVh); window.addEventListener('resize', setVh);
// Use a debounced scroll handler to prevent performance issues // Use a debounced scroll handler to prevent performance issues
let scrollTimeout; let scrollTimeout;
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (scrollTimeout) { if (scrollTimeout) {
window.cancelAnimationFrame(scrollTimeout); window.cancelAnimationFrame(scrollTimeout);
} }
scrollTimeout = window.requestAnimationFrame(() => { scrollTimeout = window.requestAnimationFrame(() => {
// Lock width during scroll // Lock width during scroll
document.body.style.width = '100%'; document.body.style.width = '100%';
document.body.style.overflowX = 'hidden'; document.body.style.overflowX = 'hidden';
}); });
}); });
// Fix for iOS Safari address bar height changes // Fix for iOS Safari address bar height changes
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
// Force the layout to use the initial viewport size // Force the layout to use the initial viewport size
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`); document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
// Apply fixed height to sections to prevent resizing // Apply fixed height to sections to prevent resizing
const sections = document.querySelectorAll('section'); const sections = document.querySelectorAll('section');
sections.forEach(section => { sections.forEach((section) => {
section.style.width = '100%'; section.style.width = '100%';
}); });
} }
// Improved theme change handler that preserves scroll position and provides smoother transitions // Improved theme change handler that preserves scroll position and provides smoother transitions
document.addEventListener('themeChanged', () => { document.addEventListener('themeChanged', () => {
// Store current scroll position // Store current scroll position
const scrollPosition = window.scrollY; const scrollPosition = window.scrollY;
// Create a temporary overlay for smoother transition // Create a temporary overlay for smoother transition
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.style.cssText = ` overlay.style.cssText = `
@@ -276,19 +356,22 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
`; `;
document.body.appendChild(overlay); document.body.appendChild(overlay);
// Fade in overlay // Fade in overlay
requestAnimationFrame(() => { requestAnimationFrame(() => {
overlay.style.opacity = '0.5'; overlay.style.opacity = '0.5';
// Update theme-transition elements without forcing reflow of entire page // Update theme-transition elements without forcing reflow of entire page
requestAnimationFrame(() => { requestAnimationFrame(() => {
document.querySelectorAll('.theme-transition-all, .theme-transition-bg, .theme-transition-color') document
.forEach(el => { .querySelectorAll(
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
)
.forEach((el) => {
// Apply a subtle animation instead of a hard reset // Apply a subtle animation instead of a hard reset
el.style.transition = 'all 0.5s ease'; el.style.transition = 'all 0.5s ease';
}); });
// Fade out overlay after transition completes // Fade out overlay after transition completes
setTimeout(() => { setTimeout(() => {
overlay.style.opacity = '0'; overlay.style.opacity = '0';
@@ -298,24 +381,24 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
}, 300); }, 300);
}); });
}); });
// Restore scroll position (prevents jumping to top) // Restore scroll position (prevents jumping to top)
if (scrollPosition > 0) { if (scrollPosition > 0) {
setTimeout(() => { setTimeout(() => {
window.scrollTo({ window.scrollTo({
top: scrollPosition, top: scrollPosition,
behavior: 'auto' // Use 'auto' to prevent animation behavior: 'auto', // Use 'auto' to prevent animation
}); });
}, 10); }, 10);
} }
}); });
// Fix theme inconsistency issues by checking theme on visibility change // Fix theme inconsistency issues by checking theme on visibility change
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
const storedTheme = localStorage.getItem('theme'); const storedTheme = localStorage.getItem('theme');
const currentThemeIsDark = document.documentElement.classList.contains('dark'); const currentThemeIsDark = document.documentElement.classList.contains('dark');
if (storedTheme === 'dark' && !currentThemeIsDark) { if (storedTheme === 'dark' && !currentThemeIsDark) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else if (storedTheme === 'light' && currentThemeIsDark) { } 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 // Add smooth reveal animations for content after loading
const animateContent = () => { const animateContent = () => {
// Animate hero section // Animate hero section
const heroElements = document.querySelectorAll('.hero-text span, .hero-text + p, .hero-text ~ div'); const heroElements = document.querySelectorAll(
'.hero-text span, .hero-text + p, .hero-text ~ div'
);
heroElements.forEach((el, index) => { heroElements.forEach((el, index) => {
setTimeout(() => { setTimeout(
el.classList.add('animate-reveal'); () => {
}, 100 + (index * 150)); el.classList.add('animate-reveal');
},
100 + index * 150
);
}); });
// Animate posts with staggered delay // Animate posts with staggered delay
const articles = document.querySelectorAll('article.group'); const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => { articles.forEach((article, index) => {
setTimeout(() => { setTimeout(
article.classList.add('animate-reveal'); () => {
}, 500 + (index * 150)); article.classList.add('animate-reveal');
},
500 + index * 150
);
}); });
// Animate topic cards with staggered delay // Animate topic cards with staggered delay
const topicCards = document.querySelectorAll('a.group.flex.flex-col'); const topicCards = document.querySelectorAll('a.group.flex.flex-col');
topicCards.forEach((card, index) => { topicCards.forEach((card, index) => {
setTimeout(() => { setTimeout(
card.classList.add('animate-reveal'); () => {
}, 800 + (index * 100)); card.classList.add('animate-reveal');
},
800 + index * 100
);
}); });
}; };
// Run animations after the loading screen is hidden // Run animations after the loading screen is hidden
const loadingScreen = document.getElementById('loading-screen'); const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) { if (loadingScreen) {
@@ -361,18 +455,20 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
// Wait for loading screen to hide // Wait for loading screen to hide
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.target === loadingScreen && if (
mutation.type === 'attributes' && mutation.target === loadingScreen &&
mutation.attributeName === 'style' && mutation.type === 'attributes' &&
loadingScreen.style.display === 'none') { mutation.attributeName === 'style' &&
loadingScreen.style.display === 'none'
) {
animateContent(); animateContent();
observer.disconnect(); observer.disconnect();
} }
}); });
}); });
observer.observe(loadingScreen, { attributes: true }); observer.observe(loadingScreen, { attributes: true });
// Fallback // Fallback
setTimeout(animateContent, 3500); setTimeout(animateContent, 3500);
} }
@@ -381,36 +477,38 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
animateContent(); animateContent();
} }
}); });
// SPA transition handling for homepage // SPA transition handling for homepage
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('target') === '_blank' || link.getAttribute('href').includes('#') ||
link.hasAttribute('data-spa-handled')) { link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.getAttribute('href'); const targetHref = link.getAttribute('href');
// Trigger page transition animation // Trigger page transition animation
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
if (pageTransition) { if (pageTransition) {
pageTransition.classList.remove('opacity-0'); pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
// Navigate after transition effect // Navigate after transition effect
setTimeout(() => { setTimeout(() => {
window.location.href = targetHref; window.location.href = targetHref;
@@ -422,13 +520,13 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
}); });
}); });
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions); document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions); document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions); document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>
@@ -439,51 +537,55 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
--theme-transition-duration: 0.5s; --theme-transition-duration: 0.5s;
--theme-transition-timing: ease; --theme-transition-timing: ease;
} }
:global(html), :global(body) { :global(html),
:global(body) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing); transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
} }
:global(.theme-transition-all) { :global(.theme-transition-all) {
transition: all var(--theme-transition-duration) var(--theme-transition-timing); transition: all var(--theme-transition-duration) var(--theme-transition-timing);
} }
:global(.theme-transition-bg) { :global(.theme-transition-bg) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing); transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
} }
:global(.theme-transition-color) { :global(.theme-transition-color) {
transition: color var(--theme-transition-duration) var(--theme-transition-timing); transition: color var(--theme-transition-duration) var(--theme-transition-timing);
} }
/* Ensure transitions apply to all theme-related properties */ /* Ensure transitions apply to all theme-related properties */
:global(*) { :global(*) {
transition-property: background-color, border-color, color, fill, stroke, opacity; transition-property: background-color, border-color, color, fill, stroke, opacity;
transition-duration: var(--theme-transition-duration); transition-duration: var(--theme-transition-duration);
transition-timing-function: var(--theme-transition-timing); transition-timing-function: var(--theme-transition-timing);
} }
/* Remove the forced transition disabling which causes flickering */ /* Remove the forced transition disabling which causes flickering */
:global(.theme-switching), :global(.theme-switching *) { :global(.theme-switching),
:global(.theme-switching *) {
/* Use a subtle transition instead of none */ /* Use a subtle transition instead of none */
transition-duration: 0.3s !important; transition-duration: 0.3s !important;
} }
/* Content reveal animations */ /* Content reveal animations */
.hero-text span, .hero-text span,
.hero-text + p, .hero-text + p,
.hero-text ~ div, .hero-text ~ div,
article.group, article.group,
a.group.flex.flex-col { a.group.flex.flex-col {
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(20px);
transition: opacity 0.8s ease, transform 0.8s ease; transition:
opacity 0.8s ease,
transform 0.8s ease;
} }
.animate-reveal { .animate-reveal {
opacity: 1 !important; opacity: 1 !important;
transform: translateY(0) !important; transform: translateY(0) !important;
} }
/* Rest of your existing styles... */ /* Rest of your existing styles... */
</style> </style>

View File

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

View File

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

View File

@@ -1,110 +1,147 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from "../../../lib/directus" import directus from '../../../lib/directus';
import { readItems } from "@directus/sdk"; import { readItems } from '@directus/sdk';
const posts = await directus.request( const posts = await directus.request(
readItems("posts", { readItems('posts', {
fields: ['*'], fields: ['*'],
sort: ["-published_date"], sort: ['-published_date'],
}) })
); );
const tags = [...new Set(posts.flatMap(post => post.tags || []))].sort(); const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
// Count posts for each tag and create tag objects with additional data // Count posts for each tag and create tag objects with additional data
const tagObjects = tags.map(tag => { const tagObjects = tags.map((tag) => {
const count = posts.filter(post => post.tags?.includes(tag)).length; const count = posts.filter((post) => post.tags?.includes(tag)).length;
// Generate a consistent but random-looking hue for each tag // Generate a consistent but random-looking hue for each tag
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360); const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
return { return {
name: tag, name: tag,
count, count,
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
hue hue,
}; };
}); });
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count); const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
--- ---
<BaseLayout title="Explore Tags"> <BaseLayout title="Explore Tags">
<div class="w-full mx-auto px-3 sm:px-6 py-6 sm:py-12 md:py-16 theme-transition-all"> <div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
<!-- Enhanced header section with animated elements - improved for mobile --> <!-- Enhanced header section with animated elements - improved for mobile -->
<div class="relative mb-8 sm:mb-12 md:mb-16 text-center theme-transition-element"> <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
<div class="absolute -top-16 -left-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div> <div
<div class="absolute -bottom-16 -right-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div> class="animate-blob theme-transition-bg absolute -left-16 -top-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:h-48 sm:w-48 md:h-72 md:w-72"
<div class="absolute top-8 right-8 w-24 sm:w-32 md:w-40 h-24 sm:h-32 md:h-40 bg-zinc-100/30 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-40 animate-blob animation-delay-4000 theme-transition-bg"></div> >
</div>
<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"> <div
<span class="inline-block relative"> 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="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 class="relative">Explore</span>
</span> </span>
{" "} {' '}
<span class="relative inline-block"> <span class="relative inline-block">
Topics Topics
<span class="absolute -bottom-1 sm:-bottom-2 left-0 w-full h-0.5 sm:h-1 bg-gradient-to-r from-zinc-400 to-zinc-600 dark:from-zinc-600 dark:to-zinc-400 transform origin-left animate-underline theme-transition-bg"></span> <span
class="animate-underline theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-full origin-left transform bg-gradient-to-r from-zinc-400 to-zinc-600 dark:from-zinc-600 dark:to-zinc-400 sm:-bottom-2 sm:h-1"
></span>
</span> </span>
</span> </span>
</h1> </h1>
<p class="relative text-sm sm:text-base md:text-lg lg:text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto theme-transition-color"> <p
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:text-base md:text-lg lg:text-xl"
>
Discover content organized by your interests Discover content organized by your interests
</p> </p>
</div> </div>
{tags.length === 0 ? ( {
<div class="text-center py-8 sm:py-12 md:py-16 theme-transition-element"> tags.length === 0 ? (
<div class="inline-flex items-center justify-center w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-3 sm:mb-4 md:mb-6 shadow-inner theme-transition-bg"> <div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 sm:w-10 md:w-12 h-8 sm:h-10 md:h-12 text-zinc-500 dark:text-zinc-400 theme-transition-color"> <div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner dark:bg-zinc-800 sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" /> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" /> xmlns="http://www.w3.org/2000/svg"
</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> </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 class="flex w-full justify-center">
</div> <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="flex justify-center w-full"> <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" />
<!-- Featured Tags Section - ultra-responsive design --> <div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl dark:from-zinc-700/20 dark:to-zinc-800/10 sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40" />
<div class="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> <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">
<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> Popular Topics
<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>
<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> <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) => (
<!-- Ultra-responsive grid layout with fallbacks --> <a
<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"> href={`/topics/${tag.name}`}
{sortedTags.map((tag) => ( 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"
<a style={`--tag-hue: ${tag.hue};`}
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" <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" />
style={`--tag-hue: ${tag.hue};`}
> <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="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="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">
<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>
<span class="text-xs xxxs:text-xs xxs:text-xs xs:text-sm sm:text-base md:text-lg font-semibold">#</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>
</a>
<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"> </div>
{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>
))}
</div> </div>
</div> </div>
</div> )
)} }
</div> </div>
</BaseLayout> </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'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta); document.getElementsByTagName('head')[0].appendChild(meta);
} else { } else {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
} }
// Fix for horizontal overflow // Fix for horizontal overflow
document.body.style.overflowX = 'hidden'; document.body.style.overflowX = 'hidden';
document.documentElement.style.overflowX = 'hidden'; document.documentElement.style.overflowX = 'hidden';
document.documentElement.style.width = '100%'; document.documentElement.style.width = '100%';
document.body.style.width = '100%'; document.body.style.width = '100%';
}; };
fixViewportWidth(); fixViewportWidth();
// Adjust tag items based on screen size with extreme precision // Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => { const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple'); const tagItems = document.querySelectorAll('.theme-ripple');
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360; const isVerySmall = width < 360;
const isExtremelySmall = width < 280; const isExtremelySmall = width < 280;
const isMicroScreen = width < 240; const isMicroScreen = width < 240;
// Fix container width to match viewport exactly // Fix container width to match viewport exactly
const container = document.querySelector('.tag-cloud'); const container = document.querySelector('.tag-cloud');
if (container) { if (container) {
container.style.maxWidth = '100vw'; container.style.maxWidth = '100vw';
container.style.width = '100%'; container.style.width = '100%';
container.style.boxSizing = 'border-box'; container.style.boxSizing = 'border-box';
// Remove any margins that might cause overflow // Remove any margins that might cause overflow
container.style.marginLeft = '0'; container.style.marginLeft = '0';
container.style.marginRight = '0'; container.style.marginRight = '0';
} }
// Fix grid width // Fix grid width
const grid = document.querySelector('.grid'); const grid = document.querySelector('.grid');
if (grid) { if (grid) {
grid.style.width = '100%'; grid.style.width = '100%';
grid.style.maxWidth = '100%'; grid.style.maxWidth = '100%';
} }
tagItems.forEach(item => { tagItems.forEach((item) => {
// Set appropriate classes based on screen size // Set appropriate classes based on screen size
if (isMicroScreen) { if (isMicroScreen) {
item.classList.add('micro-screen'); item.classList.add('micro-screen');
@@ -174,15 +215,15 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
} else { } else {
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen'); item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
} }
// Ensure text doesn't overflow on small screens // Ensure text doesn't overflow on small screens
const tagName = item.querySelector('h3'); const tagName = item.querySelector('h3');
const tagCount = item.querySelector('p'); const tagCount = item.querySelector('p');
if (tagName) { if (tagName) {
// Set title for accessibility // Set title for accessibility
tagName.title = tagName.textContent.trim(); tagName.title = tagName.textContent.trim();
// Adjust text length based on screen size // Adjust text length based on screen size
if (isMicroScreen && tagName.textContent.length > 6) { if (isMicroScreen && tagName.textContent.length > 6) {
tagName.dataset.fullText = tagName.textContent; tagName.dataset.fullText = tagName.textContent;
@@ -198,32 +239,32 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
delete tagName.dataset.fullText; delete tagName.dataset.fullText;
} }
} }
// Set the tag hue for hover effects // Set the tag hue for hover effects
const hue = item.style.getPropertyValue('--tag-hue'); const hue = item.style.getPropertyValue('--tag-hue');
item.style.setProperty('--hover-hue', hue); item.style.setProperty('--hover-hue', hue);
}); });
}; };
// Run on load // Run on load
adjustTagItems(); adjustTagItems();
// Run on resize with optimized debounce // Run on resize with optimized debounce
let resizeTimer; let resizeTimer;
const handleResize = () => { const handleResize = () => {
if (resizeTimer) { if (resizeTimer) {
window.cancelAnimationFrame(resizeTimer); window.cancelAnimationFrame(resizeTimer);
} }
resizeTimer = window.requestAnimationFrame(() => { resizeTimer = window.requestAnimationFrame(() => {
fixViewportWidth(); fixViewportWidth();
adjustTagItems(); adjustTagItems();
}); });
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize); window.addEventListener('orientationchange', handleResize);
// Ensure layout is recalculated after page is fully loaded // Ensure layout is recalculated after page is fully loaded
window.addEventListener('load', () => { window.addEventListener('load', () => {
fixViewportWidth(); fixViewportWidth();
@@ -234,17 +275,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
adjustTagItems(); adjustTagItems();
}, 500); }, 500);
}); });
// Fix for iOS Safari and other mobile browsers // Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) { if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty('--safe-area-inset-bottom', 'env(safe-area-inset-bottom)'); document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues // Fix for mobile viewport height issues
const setVh = () => { const setVh = () => {
const vh = window.innerHeight * 0.01; const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`); document.documentElement.style.setProperty('--vh', `${vh}px`);
}; };
setVh(); setVh();
window.addEventListener('resize', setVh); window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', () => { window.addEventListener('orientationchange', () => {
@@ -255,39 +299,51 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
}, 100); }, 100);
}); });
} }
// Add touch support for mobile devices // Add touch support for mobile devices
const addTouchSupport = () => { const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple'); const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach(item => { tagItems.forEach((item) => {
item.addEventListener('touchstart', () => { item.addEventListener(
item.classList.add('touch-active'); 'touchstart',
}, { passive: true }); () => {
item.classList.add('touch-active');
item.addEventListener('touchend', () => { },
setTimeout(() => { { passive: true }
item.classList.remove('touch-active'); );
}, 150);
}, { passive: true }); item.addEventListener(
'touchend',
() => {
setTimeout(() => {
item.classList.remove('touch-active');
}, 150);
},
{ passive: true }
);
// Cancel active state if touch moves away // Cancel active state if touch moves away
item.addEventListener('touchmove', (e) => { item.addEventListener(
const touch = e.touches[0]; 'touchmove',
const rect = item.getBoundingClientRect(); (e) => {
const touch = e.touches[0];
if ( const rect = item.getBoundingClientRect();
touch.clientX < rect.left ||
touch.clientX > rect.right || if (
touch.clientY < rect.top || touch.clientX < rect.left ||
touch.clientY > rect.bottom touch.clientX > rect.right ||
) { touch.clientY < rect.top ||
item.classList.remove('touch-active'); touch.clientY > rect.bottom
} ) {
}, { passive: true }); item.classList.remove('touch-active');
}
},
{ passive: true }
);
}); });
}; };
addTouchSupport(); addTouchSupport();
}); });
</script> </script>
@@ -295,9 +351,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
<style> <style>
/* Base styles */ /* Base styles */
.tag-cloud { .tag-cloud {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03), box-shadow:
0 2px 4px rgba(0, 0, 0, 0.03), 0 0 0 1px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05); 0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d; transform-style: preserve-3d;
perspective: 1000px; perspective: 1000px;
transition: all var(--theme-transition); transition: all var(--theme-transition);
@@ -307,19 +364,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
margin-left: 0 !important; margin-left: 0 !important;
margin-right: 0 !important; margin-right: 0 !important;
} }
/* Fix for horizontal overflow */ /* Fix for horizontal overflow */
:global(html), :global(body) { :global(html),
:global(body) {
overflow-x: hidden; overflow-x: hidden;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
:global(.max-w-6xl) { :global(.max-w-6xl) {
max-width: 100% !important; max-width: 100% !important;
width: 100% !important; width: 100% !important;
} }
/* Ultra-responsive breakpoints for extreme reliability */ /* Ultra-responsive breakpoints for extreme reliability */
/* Micro screens (below 240px) */ /* Micro screens (below 240px) */
@media (max-width: 239px) { @media (max-width: 239px) {
@@ -329,146 +387,146 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
border-radius: 0.25rem !important; border-radius: 0.25rem !important;
width: 100% !important; width: 100% !important;
} }
.tag-cloud h2 { .tag-cloud h2 {
font-size: 0.875rem !important; font-size: 0.875rem !important;
margin-bottom: 0.375rem !important; margin-bottom: 0.375rem !important;
} }
.grid { .grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important; grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
gap: 0.375rem !important; gap: 0.375rem !important;
width: 100% !important; width: 100% !important;
} }
.micro-screen .flex { .micro-screen .flex {
padding: 0.25rem !important; padding: 0.25rem !important;
} }
.micro-screen h3 { .micro-screen h3 {
font-size: 0.625rem !important; font-size: 0.625rem !important;
} }
.micro-screen p { .micro-screen p {
font-size: 0.5rem !important; font-size: 0.5rem !important;
} }
} }
/* Extra extra extra small screens (240px-279px) */ /* Extra extra extra small screens (240px-279px) */
@media (min-width: 240px) and (max-width: 279px) { @media (min-width: 240px) and (max-width: 279px) {
.xxxs\:grid-cols-2 { .xxxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.xxxs\:px-2 { .xxxs\:px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.xxxs\:py-2 { .xxxs\:py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.xxxs\:w-6 { .xxxs\:w-6 {
width: 1.5rem; width: 1.5rem;
} }
.xxxs\:h-6 { .xxxs\:h-6 {
height: 1.5rem; height: 1.5rem;
} }
.xxxs\:text-xs { .xxxs\:text-xs {
font-size: 0.75rem; font-size: 0.75rem;
} }
.xxxs\:gap-2 { .xxxs\:gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
.xxxs\:text-\[9px\] { .xxxs\:text-\[9px\] {
font-size: 9px; font-size: 9px;
} }
} }
/* Extra extra small screens (280px-359px) */ /* Extra extra small screens (280px-359px) */
@media (min-width: 280px) { @media (min-width: 280px) {
.xxs\:grid-cols-2 { .xxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.xxs\:px-2 { .xxs\:px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.xxs\:py-2 { .xxs\:py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.xxs\:w-6 { .xxs\:w-6 {
width: 1.5rem; width: 1.5rem;
} }
.xxs\:h-6 { .xxs\:h-6 {
height: 1.5rem; height: 1.5rem;
} }
.xxs\:text-xs { .xxs\:text-xs {
font-size: 0.75rem; font-size: 0.75rem;
} }
.xxs\:gap-2 { .xxs\:gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
.xxs\:text-\[9px\] { .xxs\:text-\[9px\] {
font-size: 9px; font-size: 9px;
} }
} }
/* Extra small screens (360px-639px) */ /* Extra small screens (360px-639px) */
@media (min-width: 360px) { @media (min-width: 360px) {
.xs\:grid-cols-3 { .xs\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.xs\:px-2 { .xs\:px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.xs\:py-2 { .xs\:py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.xs\:w-7 { .xs\:w-7 {
width: 1.75rem; width: 1.75rem;
} }
.xs\:h-7 { .xs\:h-7 {
height: 1.75rem; height: 1.75rem;
} }
.xs\:text-xs { .xs\:text-xs {
font-size: 0.75rem; font-size: 0.75rem;
} }
.xs\:text-sm { .xs\:text-sm {
font-size: 0.875rem; font-size: 0.875rem;
} }
.xs\:gap-2 { .xs\:gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
.xs\:text-\[10px\] { .xs\:text-\[10px\] {
font-size: 10px; font-size: 10px;
} }
} }
/* Ensure text doesn't overflow on small screens */ /* Ensure text doesn't overflow on small screens */
.truncate { .truncate {
overflow: hidden; overflow: hidden;
@@ -476,7 +534,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
white-space: nowrap; white-space: nowrap;
max-width: 100%; max-width: 100%;
} }
/* Ensure proper word breaking for long tag names */ /* Ensure proper word breaking for long tag names */
.break-words { .break-words {
word-break: break-word; word-break: break-word;
@@ -486,38 +544,42 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
.hyphens-auto { .hyphens-auto {
hyphens: auto; hyphens: auto;
} }
/* Improved shadow for dark mode */ /* Improved shadow for dark mode */
:global(.dark) .tag-cloud { :global(.dark) .tag-cloud {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05), box-shadow:
0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05),
0 4px 8px rgba(0, 0, 0, 0.15); 0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15);
} }
/* Prevent layout shifts */ /* Prevent layout shifts */
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
.min-w-0 { .min-w-0 {
min-width: 0; min-width: 0;
} }
/* Ensure container doesn't overflow */ /* Ensure container doesn't overflow */
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
} }
/* Touch support for mobile */ /* Touch support for mobile */
.touch-active { .touch-active {
transform: scale(0.97) !important; transform: scale(0.97) !important;
opacity: 0.9; opacity: 0.9;
transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out !important; transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
} }
/* Animation for blob */ /* Animation for blob */
@keyframes blob { @keyframes blob {
0%, 100% { 0%,
100% {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
25% { 25% {
@@ -530,19 +592,19 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
transform: translate(-10px, -10px) scale(1.05); transform: translate(-10px, -10px) scale(1.05);
} }
} }
.animate-blob { .animate-blob {
animation: blob 20s infinite ease-in-out; animation: blob 20s infinite ease-in-out;
} }
.animation-delay-2000 { .animation-delay-2000 {
animation-delay: 2s; animation-delay: 2s;
} }
.animation-delay-4000 { .animation-delay-4000 {
animation-delay: 4s; animation-delay: 4s;
} }
/* Animation for underline */ /* Animation for underline */
@keyframes underline { @keyframes underline {
0% { 0% {
@@ -552,11 +614,11 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
transform: scaleX(1); transform: scaleX(1);
} }
} }
.animate-underline { .animate-underline {
animation: underline 1.5s ease-out forwards; animation: underline 1.5s ease-out forwards;
} }
/* Fix for iOS Safari notch */ /* Fix for iOS Safari notch */
@supports (padding: max(0px)) { @supports (padding: max(0px)) {
.tag-cloud { .tag-cloud {
@@ -571,32 +633,34 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
// Handle SPA transitions for tags index page // Handle SPA transitions for tags index page
function setupSPATransitions() { function setupSPATransitions() {
// Handle all internal links for SPA transitions // Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach(link => { document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed // Skip links that are anchor links, external links, or already processed
if (link.getAttribute('href').includes('#') || if (
link.getAttribute('target') === '_blank' || link.getAttribute('href').includes('#') ||
link.hasAttribute('data-spa-handled')) { link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return; return;
} }
// Mark as handled to avoid duplicate listeners // Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true'); link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.) // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const targetHref = link.getAttribute('href'); const targetHref = link.getAttribute('href');
// Trigger page transition animation // Trigger page transition animation
const pageTransition = document.getElementById('page-transition'); const pageTransition = document.getElementById('page-transition');
if (pageTransition) { if (pageTransition) {
pageTransition.classList.remove('opacity-0'); pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100'); pageTransition.classList.add('opacity-100');
// Navigate after transition effect // Navigate after transition effect
setTimeout(() => { setTimeout(() => {
window.location.href = targetHref; 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 // Add hover effect for tag cards on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) { if (isTouchDevice) {
const tagCards = document.querySelectorAll('.tag-cloud a'); const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach(card => { tagCards.forEach((card) => {
card.addEventListener('touchstart', () => { card.addEventListener('touchstart', () => {
card.classList.add('is-touched'); card.classList.add('is-touched');
}); });
card.addEventListener('touchend', () => { card.addEventListener('touchend', () => {
setTimeout(() => { setTimeout(() => {
card.classList.remove('is-touched'); 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 // Animate tag cards with staggered delay
const tagCards = document.querySelectorAll('.tag-cloud a'); const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach((card, index) => { tagCards.forEach((card, index) => {
setTimeout(() => { setTimeout(
card.classList.add('animate-reveal'); () => {
}, 100 + (index * 50)); card.classList.add('animate-reveal');
},
100 + index * 50
);
}); });
} }
// Initialize on first load // Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions); document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions); document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system // For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions); document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>

View File

@@ -1,11 +1,9 @@
/* Remove all the complex mobile menu styles and keep only what's necessary */ /* Remove all the complex mobile menu styles and keep only what's necessary */
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities;
@layer base { @layer base {
:root { :root {
font-family: "Inter", sans-serif; font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
--theme-transition: 0.3s ease; --theme-transition: 0.3s ease;
@@ -17,17 +15,20 @@
} }
body { 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; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; overflow-x: hidden;
} }
/* Simple theme transition */ /* Simple theme transition */
body, a, button { body,
transition: background-color var(--theme-transition), a,
color var(--theme-transition), button {
border-color var(--theme-transition); transition:
background-color var(--theme-transition),
color var(--theme-transition),
border-color var(--theme-transition);
} }
} }
@@ -36,33 +37,55 @@
html { html {
scroll-padding-top: 4rem; scroll-padding-top: 4rem;
} }
/* Better touch targets on mobile */ /* Better touch targets on mobile */
button, a { button,
@apply min-h-[44px]; a {
@reference min-h-[44px];
} }
} }
/* Add smooth animations */ /* Add smooth animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideUp { @keyframes slideUp {
from { transform: translateY(20px); opacity: 0; } from {
to { transform: translateY(0); opacity: 1; } transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
@keyframes slideDown { @keyframes slideDown {
from { transform: translateY(-20px); opacity: 0; } from {
to { transform: translateY(0); opacity: 1; } transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
@keyframes scaleIn { @keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; } from {
to { transform: scale(1); opacity: 1; } transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
} }
/* Apply animations to elements */ /* Apply animations to elements */
@@ -100,17 +123,21 @@
} }
/* Smooth hover transitions */ /* Smooth hover transitions */
a, button { a,
button {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
a:hover, button:hover { a:hover,
button:hover {
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Smooth page transitions */ /* Smooth page transitions */
.page-transition { .page-transition {
transition: opacity 0.3s ease, transform 0.3s ease; transition:
opacity 0.3s ease,
transform 0.3s ease;
} }
.page-entering { .page-entering {

View File

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

View File

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