Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
fcae7676c6 | |||
cc16b5435a | |||
27b5e6a36b | |||
bcb91972a1 | |||
b11666decb
|
|||
a947a05041 | |||
297c573281
|
|||
9093594973
|
|||
77ce0a1182
|
|||
799e6b6090 | |||
735e4b4877
|
|||
3e12a8647d | |||
e07210638e | |||
22d5b50f73 | |||
40acf8f34a | |||
543516baba | |||
e985f905f2 | |||
e1f09ca4ec | |||
0c09eb38e9 | |||
95eeb44e4f
|
|||
d47d67572e | |||
fa4841948a
|
|||
71e2b0185b | |||
7f9fb4d2b9 | |||
8420c8dd58 | |||
fa6ed18edb | |||
30860fce1e | |||
b479e0e22c | |||
cf01ebcd3c | |||
df8ccf81c2
|
|||
073911c1b9 | |||
3eeea3dd8f | |||
43fea76778
|
|||
d64df6473a | |||
63a6a00817
|
@@ -1,35 +0,0 @@
|
|||||||
name: process-pull-requests
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '@daily'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
process-pull-requests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Python Script
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: alexlebens/workflow-scripts
|
|
||||||
ref: main
|
|
||||||
token: ${{ secrets.BOT_TOKEN }}
|
|
||||||
path: scripts
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.13'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install requests
|
|
||||||
|
|
||||||
- name: Run Script
|
|
||||||
env:
|
|
||||||
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
|
|
||||||
REPOSITORY: ${{ gitea.repository }}
|
|
||||||
TOKEN: ${{ secrets.BOT_TOKEN }}
|
|
||||||
STALE_DAYS: 3
|
|
||||||
STALE_TAG: 'stale'
|
|
||||||
REQUIRED_TAG: 'automerge'
|
|
||||||
run: python ./scripts/scripts/process-pull-requests.py
|
|
@@ -1,11 +1,11 @@
|
|||||||
name: process-issues
|
name: process-repository
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '@daily'
|
- cron: "@daily"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
process-issues:
|
process-repository:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Python Script
|
- name: Checkout Python Script
|
||||||
@@ -14,22 +14,27 @@ jobs:
|
|||||||
repository: alexlebens/workflow-scripts
|
repository: alexlebens/workflow-scripts
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ secrets.BOT_TOKEN }}
|
token: ${{ secrets.BOT_TOKEN }}
|
||||||
path: scripts
|
path: workflow-scripts
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.13'
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install requests
|
run: pip install requests immutabledict
|
||||||
|
|
||||||
- name: Run Script
|
- name: Run Script
|
||||||
env:
|
env:
|
||||||
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
|
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
|
||||||
|
OWNER: ${{ gitea.owner }}
|
||||||
REPOSITORY: ${{ gitea.repository }}
|
REPOSITORY: ${{ gitea.repository }}
|
||||||
TOKEN: ${{ secrets.BOT_TOKEN }}
|
TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
STALE_DAYS: 3
|
LOG_LEVEL: DEBUG
|
||||||
STALE_TAG: 'stale'
|
ISSUE_STALE_DAYS: 3
|
||||||
EXCLUDE_TAG: 'renovate'
|
ISSUE_STALE_TAG: 23
|
||||||
run: python ./scripts/scripts/process-issues.py
|
ISSUE_EXCLUDE_TAG: 17
|
||||||
|
PULL_REQUEST_STALE_DAYS: 3
|
||||||
|
PULL_REQUEST_STALE_TAG: 23
|
||||||
|
PULL_REQUEST_REQUIRED_TAG: 22
|
||||||
|
run: python ./workflow-scripts/process-repository.py
|
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.17.x
|
node-version: 22.17.1
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
ARG REGISTRY=docker.io
|
ARG REGISTRY=docker.io
|
||||||
FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base
|
FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base
|
||||||
|
|
||||||
LABEL version="0.8.13"
|
LABEL version="0.11.0"
|
||||||
LABEL description="Astro based personal website"
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Copyright (c) 2025 Lê Vĩnh Khang
|
Copyright (c) 2025 Lê Vĩnh Khang
|
||||||
|
|
||||||
|
Copyright (c) 2025 Alex Lebens
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.8.13",
|
"version": "0.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
@@ -31,13 +31,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@typescript-eslint/parser": "8.36.0",
|
"@typescript-eslint/parser": "8.37.0",
|
||||||
"eslint": "9.30.1",
|
"eslint": "9.31.0",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-astro": "1.3.1",
|
"eslint-plugin-astro": "1.3.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||||
"typescript-eslint": "8.36.0"
|
"typescript-eslint": "8.37.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
895
pnpm-lock.yaml
generated
895
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
||||||
@@ -29,24 +29,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Theme transition script
|
// Theme transition script
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
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
|
|
||||||
document.documentElement.classList.add('theme-transitioning');
|
document.documentElement.classList.add('theme-transitioning');
|
||||||
|
|
||||||
// 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
|
|
||||||
overlay.style.opacity = '0';
|
overlay.style.opacity = '0';
|
||||||
|
|
||||||
// Remove transitioning class after animation completes
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.documentElement.classList.remove('theme-transitioning');
|
document.documentElement.classList.remove('theme-transitioning');
|
||||||
}, 700);
|
}, 700);
|
||||||
@@ -60,13 +55,13 @@
|
|||||||
/* Grid pattern for dots */
|
/* Grid pattern for dots */
|
||||||
.bg-grid-pattern {
|
.bg-grid-pattern {
|
||||||
background-size: 24px 24px;
|
background-size: 24px 24px;
|
||||||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
|
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 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.15) 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ambient glow animations */
|
/* Ambient glow animations */
|
||||||
|
@@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links'));
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ text: 'About', href: '/about' },
|
{ text: 'Home', href: '/' },
|
||||||
{ text: 'Blog', href: '/blog' },
|
{ text: 'Blog', href: '/blog' },
|
||||||
{ text: 'Topics', href: '/topics' },
|
{ text: 'About', href: '/about' },
|
||||||
{ text: 'RSS', href: '/rss.xml' },
|
{ text: 'RSS', href: '/rss' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
@@ -35,6 +35,7 @@ const socialLinks = [
|
|||||||
|
|
||||||
<footer
|
<footer
|
||||||
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
||||||
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
@@ -53,28 +54,26 @@ const socialLinks = [
|
|||||||
|
|
||||||
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||||
<div class="mx-auto max-w-4xl">
|
<div class="mx-auto max-w-4xl">
|
||||||
<!-- Main footer content -->
|
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
<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="group inline-block">
|
<a href="/" class="group inline-block">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div
|
||||||
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
|
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 dark:from-zinc-200 dark:to-zinc-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
class="theme-transition-all text-xl font-bold text-zinc-100 duration-300 dark:text-zinc-900"
|
||||||
>{global.initals}</span
|
|
||||||
>
|
>
|
||||||
<div
|
{global.initals}
|
||||||
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"
|
</span>
|
||||||
>
|
<div class="absolute inset-0"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||||
>Blog</span
|
|
||||||
>
|
>
|
||||||
|
Blog
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -127,7 +126,6 @@ const socialLinks = [
|
|||||||
>
|
>
|
||||||
<span class="relative inline-block overflow-hidden">
|
<span class="relative inline-block overflow-hidden">
|
||||||
<span class="relative z-10">{link.text}</span>
|
<span class="relative z-10">{link.text}</span>
|
||||||
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -146,8 +144,8 @@ const socialLinks = [
|
|||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
||||||
>Built with</span
|
>Built with
|
||||||
>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="https://astro.build"
|
href="https://astro.build"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -174,7 +172,8 @@ const socialLinks = [
|
|||||||
Astro
|
Astro
|
||||||
<span
|
<span
|
||||||
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
|
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
|
||||||
></span>
|
>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,7 +10,22 @@ const parsedDate = typeof date === 'string' ? new Date(date) : date;
|
|||||||
|
|
||||||
{
|
{
|
||||||
parsedDate && (
|
parsedDate && (
|
||||||
<time datetime={parsedDate.toISOString()}>
|
<time datetime={parsedDate.toISOString()} class="z-10 flex items-center gap-1.5">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
||||||
|
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
{parsedDate.toLocaleDateString('en-us', {
|
{parsedDate.toLocaleDateString('en-us', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
|
@@ -5,12 +5,13 @@ 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 navItems = [
|
const navItems = [
|
||||||
{ text: 'Home', href: '/' },
|
{ text: 'Home', href: '/' },
|
||||||
{ text: 'Blog', href: '/blog' },
|
{ text: 'Blog', href: '/blog' },
|
||||||
{ text: 'Topics', href: '/topics' },
|
|
||||||
{ text: 'About', href: '/about' },
|
{ text: 'About', href: '/about' },
|
||||||
|
{ text: 'Gitea', href: links.gitea },
|
||||||
{ text: 'RSS', href: 'rss.xml' },
|
{ text: 'RSS', href: 'rss.xml' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const currentPath = pathname.slice(1);
|
|||||||
|
|
||||||
<header
|
<header
|
||||||
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
||||||
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
@@ -35,8 +37,8 @@ const currentPath = pathname.slice(1);
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
class={`text-sm font-medium ${
|
class={`text-sm font-medium ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-zinc-900 dark:text-white'
|
? 'text-zinc-900 dark:text-zinc-100'
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
@@ -100,8 +102,8 @@ const currentPath = pathname.slice(1);
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-zinc-900 dark:text-white'
|
? 'text-zinc-900 dark:text-zinc-100'
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
: 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
|
||||||
}`}
|
}`}
|
||||||
style={`transition-delay: ${index * 0.05}s;`}
|
style={`transition-delay: ${index * 0.05}s;`}
|
||||||
>
|
>
|
||||||
@@ -121,7 +123,7 @@ const currentPath = pathname.slice(1);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mobile menu toggle with animations
|
// Mobile menu toggle with animations
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
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');
|
||||||
|
@@ -29,10 +29,12 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
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
|
|
||||||
>
|
>
|
||||||
|
<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}`}
|
||||||
@@ -50,8 +52,9 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
|
|
||||||
>
|
>
|
||||||
|
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<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}`}
|
||||||
@@ -69,10 +72,12 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
|
|
||||||
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
|
|
||||||
></circle></svg
|
|
||||||
>
|
>
|
||||||
|
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z">
|
||||||
|
</path>
|
||||||
|
<rect x="2" y="9" width="4" height="12"></rect>
|
||||||
|
<circle cx="4" cy="4" r="2"></circle>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
id="copy-link-button"
|
id="copy-link-button"
|
||||||
@@ -89,9 +94,10 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
|
|
||||||
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
|
|
||||||
>
|
>
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"> </path>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"> </path>
|
||||||
|
</svg>
|
||||||
<span
|
<span
|
||||||
id="copy-tooltip"
|
id="copy-tooltip"
|
||||||
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
||||||
@@ -101,75 +107,3 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Function to handle copy link button
|
|
||||||
function setupCopyLinkButton() {
|
|
||||||
const copyButtons = document.querySelectorAll('#copy-link-button');
|
|
||||||
|
|
||||||
copyButtons.forEach((button) => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// Get the current URL
|
|
||||||
const url = window.location.href;
|
|
||||||
|
|
||||||
// Copy to clipboard
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => {
|
|
||||||
// Show tooltip
|
|
||||||
const tooltip = button.querySelector('#copy-tooltip');
|
|
||||||
if (tooltip) {
|
|
||||||
tooltip.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Hide tooltip after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
tooltip.classList.remove('opacity-100');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Failed to copy: ', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the copy link button when the DOM is loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Also set up when the page content is updated via SPA navigation
|
|
||||||
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// For compatibility with the custom page transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Handle SPA transitions for share links
|
|
||||||
function setupSpaTransitions() {
|
|
||||||
// Get all share links
|
|
||||||
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
|
||||||
|
|
||||||
// Make sure external share links don't trigger page transitions
|
|
||||||
shareLinks.forEach((link) => {
|
|
||||||
link.setAttribute('data-spa-external', 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SPA transitions
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
|
||||||
document.addEventListener('astro:page-load', setupSpaTransitions);
|
|
||||||
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
|
||||||
|
|
||||||
// Dispatch custom event when share action is completed
|
|
||||||
function notifyShareComplete() {
|
|
||||||
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add analytics tracking for share actions if needed
|
|
||||||
function trackShareAction(platform) {
|
|
||||||
// You can implement analytics tracking here
|
|
||||||
console.log(`Shared on ${platform}`);
|
|
||||||
|
|
||||||
// Notify other components that share action is complete
|
|
||||||
notifyShareComplete();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
@@ -8,16 +8,21 @@ const { tags = [], class: className = '' } = Astro.props;
|
|||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
tags.length > 0 && (
|
tags && (
|
||||||
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
|
<div class={`mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start ${className}`}>
|
||||||
{tags.map((tag) => (
|
{tags.slice(0, 2).map((postTag) => (
|
||||||
<a
|
<a
|
||||||
href={`/tag/${tag}`}
|
href={`/tags/${postTag}`}
|
||||||
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
class={`inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700`}
|
||||||
>
|
>
|
||||||
{tag}
|
#{postTag}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
{tags.length > 2 && (
|
||||||
|
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||||
|
+{tags.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -47,24 +47,25 @@
|
|||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
// Use a function to persist theme when using SPA transitions
|
||||||
|
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution
|
||||||
|
function applyTheme() {
|
||||||
|
localStorage.theme === 'dark'
|
||||||
|
? document.documentElement.classList.add('dark')
|
||||||
|
: document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('astro:after-swap', applyTheme);
|
||||||
|
|
||||||
|
applyTheme();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<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
|
|
||||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
// Check for saved theme preference or use the system preference
|
|
||||||
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
|
||||||
|
|
||||||
// Apply the theme on initial load
|
|
||||||
if (currentTheme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create theme switch overlay element if it doesn't exist
|
// 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');
|
||||||
@@ -184,7 +185,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run setup on load
|
// Run setup on load
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeToggle);
|
document.addEventListener('astro:page-load', 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', () => {
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
import Layout from './Layout.astro';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={global.title} description={global.title}>
|
|
||||||
<slot />
|
|
||||||
</Layout>
|
|
@@ -15,45 +15,3 @@ export interface Props {
|
|||||||
<Layout title={global.title} description={global.title}>
|
<Layout title={global.title} description={global.title}>
|
||||||
<slot />
|
<slot />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.addEventListener('click', () => {
|
|
||||||
document.documentElement.classList.add('theme-switching');
|
|
||||||
|
|
||||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
|
||||||
rippleElements.forEach((el) => {
|
|
||||||
el.classList.add('ripple-active');
|
|
||||||
setTimeout(() => {
|
|
||||||
el.classList.remove('ripple-active');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = new CustomEvent('themeChange', {
|
|
||||||
detail: {
|
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('theme-switching');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const socialLinks = document.querySelectorAll('.social-link');
|
|
||||||
socialLinks.forEach((link) => {
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('hover-active');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('hover-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
@@ -18,7 +18,6 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const post = Astro.props;
|
const post = Astro.props;
|
||||||
const published_date: string = post.published_date.toLocaleString();
|
|
||||||
|
|
||||||
let canonicalURL;
|
let canonicalURL;
|
||||||
try {
|
try {
|
||||||
@@ -30,19 +29,34 @@ try {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={post.title} description={post.description}>
|
<Layout title={post.title} description={post.description}>
|
||||||
<article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl">
|
<article
|
||||||
<div class="mb-12">
|
class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
|
<div class="hero-text mb-12">
|
||||||
<h1
|
<h1
|
||||||
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
|
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
|
<p
|
||||||
<FormattedDate date={published_date} />
|
class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
<FormattedDate date={post.published_date} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TagList tags={post.tags} class="mt-2" />
|
<div
|
||||||
|
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
<TagList tags={post.tags} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero image -->
|
<!-- Hero image -->
|
||||||
@@ -70,7 +84,6 @@ try {
|
|||||||
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
||||||
<!-- Convert URL to string -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,285 +100,60 @@ try {
|
|||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Blog post SPA transitions
|
document.addEventListener('astro:page-load', () => {
|
||||||
function setupBlogPostTransitions() {
|
// Add smooth reveal animations for content after loading
|
||||||
// Animate article entrance
|
const animateContent = () => {
|
||||||
const article = document.querySelector('article');
|
// Animate hero section
|
||||||
if (article) {
|
const heroElements = document.querySelectorAll(
|
||||||
article.classList.add('article-entering');
|
'.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a'
|
||||||
|
|
||||||
// Remove class after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
article.classList.remove('article-entering');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure consistent code block styling
|
|
||||||
function updateCodeBlockStyles() {
|
|
||||||
document.querySelectorAll('pre').forEach((pre) => {
|
|
||||||
// Force the background color with !important for both light and dark mode
|
|
||||||
pre.setAttribute('style', 'background-color: #1e293b !important');
|
|
||||||
|
|
||||||
// Also apply to any nested code elements
|
|
||||||
const codeElements = pre.querySelectorAll('code');
|
|
||||||
codeElements.forEach((code) => {
|
|
||||||
code.setAttribute(
|
|
||||||
'style',
|
|
||||||
'background-color: transparent !important; color: #e5e7eb !important;'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial application
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
|
|
||||||
// Watch for theme changes
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
||||||
|
|
||||||
// Also run on any content changes that might add new code blocks
|
|
||||||
const contentObserver = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.addedNodes.length) {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentObserver.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
observer.disconnect();
|
|
||||||
contentObserver.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the parallax effect for hero image
|
|
||||||
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
const navLinks = document.querySelectorAll('.blog-nav-link');
|
|
||||||
navLinks.forEach((link) => {
|
|
||||||
if (!link.hasAttribute('data-spa-handled')) {
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('nav-link-hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('nav-link-hover');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate headings when they enter the viewport
|
|
||||||
const animateHeadings = () => {
|
|
||||||
const headings = document.querySelectorAll('article h2, article h3');
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('heading-visible');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.2,
|
|
||||||
rootMargin: '0px 0px -100px 0px',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
heroElements.forEach((el, index) => {
|
||||||
headings.forEach((heading) => {
|
setTimeout(
|
||||||
heading.classList.add('heading-animated');
|
() => {
|
||||||
observer.observe(heading);
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return observer;
|
// Animate posts with staggered delay
|
||||||
|
const articles = document.querySelectorAll('article.group');
|
||||||
|
articles.forEach((article, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
article.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
500 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize heading animations
|
animateContent();
|
||||||
const headingObserver = animateHeadings();
|
});
|
||||||
|
|
||||||
// Enhance code blocks with syntax highlighting and copy button
|
|
||||||
function enhanceCodeBlocks() {
|
|
||||||
const codeBlocks = document.querySelectorAll('pre code');
|
|
||||||
|
|
||||||
codeBlocks.forEach((codeBlock) => {
|
|
||||||
// Skip if already processed
|
|
||||||
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
|
||||||
|
|
||||||
// Mark as enhanced
|
|
||||||
codeBlock.parentElement.classList.add('enhanced');
|
|
||||||
|
|
||||||
// Create copy button
|
|
||||||
const copyButton = document.createElement('button');
|
|
||||||
copyButton.className = 'copy-code-button';
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add copy functionality
|
|
||||||
copyButton.addEventListener('click', () => {
|
|
||||||
const code = codeBlock.textContent;
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
|
|
||||||
// Show copied feedback
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add copy button to pre element
|
|
||||||
codeBlock.parentElement.appendChild(copyButton);
|
|
||||||
|
|
||||||
// Fix line numbers implementation
|
|
||||||
const codeText = codeBlock.textContent;
|
|
||||||
const lines = codeText.split('\n');
|
|
||||||
|
|
||||||
const lineNumbers = document.createElement('div');
|
|
||||||
lineNumbers.className = 'line-numbers';
|
|
||||||
|
|
||||||
// Always include all lines, including empty ones
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineNumber = document.createElement('span');
|
|
||||||
lineNumber.textContent = i + 1;
|
|
||||||
lineNumbers.appendChild(lineNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
codeBlock.parentElement.classList.add('with-line-numbers');
|
|
||||||
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
|
||||||
|
|
||||||
// Fix language label detection and display
|
|
||||||
const className = codeBlock.className;
|
|
||||||
const languageMatch = className.match(/language-(\w+)/);
|
|
||||||
|
|
||||||
if (languageMatch && languageMatch[1]) {
|
|
||||||
const language = languageMatch[1];
|
|
||||||
|
|
||||||
// Add language label at top right
|
|
||||||
const languageLabel = document.createElement('div');
|
|
||||||
languageLabel.className = 'language-label';
|
|
||||||
languageLabel.textContent = language;
|
|
||||||
codeBlock.parentElement.appendChild(languageLabel);
|
|
||||||
|
|
||||||
// Add language badge at bottom right with markdown syntax
|
|
||||||
const languageBadge = document.createElement('div');
|
|
||||||
languageBadge.className = 'language-badge';
|
|
||||||
languageBadge.textContent = `\`\`\`${language}`;
|
|
||||||
languageBadge.style.position = 'absolute';
|
|
||||||
languageBadge.style.bottom = '0.5rem';
|
|
||||||
languageBadge.style.right = '0.5rem';
|
|
||||||
languageBadge.style.fontSize = '0.7rem';
|
|
||||||
languageBadge.style.padding = '0.1rem 0.3rem';
|
|
||||||
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
|
||||||
languageBadge.style.color = '#e5e7eb';
|
|
||||||
languageBadge.style.borderRadius = '0.25rem';
|
|
||||||
languageBadge.style.fontFamily =
|
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
||||||
languageBadge.style.zIndex = '10';
|
|
||||||
codeBlock.parentElement.appendChild(languageBadge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance tables with better styling
|
|
||||||
function enhanceTables() {
|
|
||||||
const tables = document.querySelectorAll('.markdown-content table');
|
|
||||||
|
|
||||||
tables.forEach((table) => {
|
|
||||||
if (table.classList.contains('enhanced-table')) return;
|
|
||||||
|
|
||||||
table.classList.add('enhanced-table');
|
|
||||||
|
|
||||||
// Wrap table in responsive container
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'table-container';
|
|
||||||
table.parentNode.insertBefore(wrapper, table);
|
|
||||||
wrapper.appendChild(table);
|
|
||||||
|
|
||||||
// Add zebra striping to rows
|
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
|
||||||
rows.forEach((row, index) => {
|
|
||||||
if (index % 2 === 0) {
|
|
||||||
row.classList.add('even-row');
|
|
||||||
} else {
|
|
||||||
row.classList.add('odd-row');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance blockquotes with icons
|
|
||||||
function enhanceBlockquotes() {
|
|
||||||
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
|
||||||
|
|
||||||
blockquotes.forEach((blockquote) => {
|
|
||||||
if (blockquote.classList.contains('enhanced-quote')) return;
|
|
||||||
|
|
||||||
blockquote.classList.add('enhanced-quote');
|
|
||||||
|
|
||||||
// Add quote icon
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'quote-icon';
|
|
||||||
icon.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
blockquote.insertBefore(icon, blockquote.firstChild);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all enhancements
|
|
||||||
enhanceCodeBlocks();
|
|
||||||
enhanceTables();
|
|
||||||
enhanceBlockquotes();
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
if (headingObserver) {
|
|
||||||
headingObserver.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Also initialize when SPA navigation completes
|
|
||||||
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Enhanced hero image styling */
|
/* Content reveal animations */
|
||||||
|
.hero-text h1,
|
||||||
|
.hero-text div,
|
||||||
|
.hero-text ~ div,
|
||||||
|
.hero-text span,
|
||||||
|
.hero-text p,
|
||||||
|
.hero-text + a,
|
||||||
|
article.group {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition:
|
||||||
|
opacity 0.8s ease,
|
||||||
|
transform 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-reveal {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero image styling */
|
||||||
article img:first-of-type {
|
article img:first-of-type {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -377,22 +165,4 @@ try {
|
|||||||
article img:first-of-type:hover {
|
article img:first-of-type:hover {
|
||||||
transform: scale(1.01);
|
transform: scale(1.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Article entrance animation */
|
|
||||||
.article-entering {
|
|
||||||
animation: article-fade-in 0.8s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rest of the styles remain unchanged... */
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
|
||||||
import Navigation from '../components/Navigation.astro';
|
import Navigation from '../components/Navigation.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import Background from '../components/Background.astro';
|
import Background from '../components/Background.astro';
|
||||||
|
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,19 +30,30 @@ const { title, description } = Astro.props;
|
|||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<!-- Load theme early to prevent flashes between light and dark modes -->
|
||||||
|
<script is:inline>
|
||||||
|
const theme = (() => {
|
||||||
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||||
|
return localStorage.getItem('theme');
|
||||||
|
}
|
||||||
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (theme === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
window.localStorage.setItem('theme', theme);
|
||||||
|
</script>
|
||||||
|
<ClientRouter />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
<!-- Page transition overlay - for smooth transitions between pages -->
|
|
||||||
<div
|
|
||||||
id="page-transition"
|
|
||||||
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="transition-spinner"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Background component with dot pattern and ambient glow -->
|
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
|
<div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
|
||||||
@@ -49,262 +63,10 @@ const { title, description } = Astro.props;
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
<script>
|
|
||||||
// SPA transition system with history API
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
const mainContent = document.querySelector('main');
|
|
||||||
|
|
||||||
// Initialize content with entrance animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load content via fetch
|
|
||||||
async function loadContent(url) {
|
|
||||||
try {
|
|
||||||
// Show transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade out current content
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the new page content
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
// Create a temporary element to parse the HTML
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
// Extract the main content
|
|
||||||
const newContent = doc.querySelector('main');
|
|
||||||
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
|
||||||
|
|
||||||
// Extract the title
|
|
||||||
const newTitle = doc.querySelector('title');
|
|
||||||
if (newTitle) {
|
|
||||||
document.title = newTitle.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract meta description
|
|
||||||
const newDescription = doc.querySelector('meta[name="description"]');
|
|
||||||
if (newDescription) {
|
|
||||||
const currentDescription = document.querySelector('meta[name="description"]');
|
|
||||||
if (currentDescription) {
|
|
||||||
currentDescription.setAttribute(
|
|
||||||
'content',
|
|
||||||
newDescription.getAttribute('content') || ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit for transition effect
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
// Replace the content
|
|
||||||
if (mainContent && newContent) {
|
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
|
||||||
|
|
||||||
// Run scripts in the new content
|
|
||||||
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
|
|
||||||
const newScript = document.createElement('script');
|
|
||||||
Array.from(oldScript.attributes).forEach((attr) => {
|
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
|
||||||
});
|
|
||||||
newScript.textContent = oldScript.textContent;
|
|
||||||
if (oldScript.parentNode) {
|
|
||||||
mainContent.appendChild(newScript);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade in new content with animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
|
||||||
mainContent.style.opacity = '1';
|
|
||||||
mainContent.style.transform = 'translateY(0)';
|
|
||||||
|
|
||||||
// Add entrance animation class
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
setTimeout(() => {
|
|
||||||
pageTransition.classList.add('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.remove('opacity-100');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch custom event for content loaded
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent('spa-content-loaded', {
|
|
||||||
detail: { url },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Scroll to top or to saved position
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
// Re-attach event listeners to new content
|
|
||||||
attachLinkListeners();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading content:', error);
|
|
||||||
|
|
||||||
// Fallback to traditional navigation on error
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to attach event listeners to all links
|
|
||||||
function attachLinkListeners() {
|
|
||||||
document.querySelectorAll('a').forEach((link) => {
|
|
||||||
// Skip links that are already handled, anchor links, external links, or have special attributes
|
|
||||||
if (
|
|
||||||
link.hasAttribute('data-spa-handled') ||
|
|
||||||
!link.href.startsWith(window.location.origin) ||
|
|
||||||
link.href.includes('#') ||
|
|
||||||
link.hasAttribute('target') ||
|
|
||||||
link.hasAttribute('download') ||
|
|
||||||
link.getAttribute('rel') === 'external' ||
|
|
||||||
link.getAttribute('rel') === 'nofollow'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.href;
|
|
||||||
|
|
||||||
// Don't transition if clicking the current page
|
|
||||||
if (targetHref === window.location.href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update browser history
|
|
||||||
window.history.pushState({ path: targetHref }, '', targetHref);
|
|
||||||
|
|
||||||
// Load the new content
|
|
||||||
loadContent(targetHref);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial attachment of link listeners
|
|
||||||
attachLinkListeners();
|
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
|
||||||
window.addEventListener('popstate', (e) => {
|
|
||||||
if (e.state && e.state.path) {
|
|
||||||
loadContent(e.state.path);
|
|
||||||
} else {
|
|
||||||
loadContent(window.location.href);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
const checkAndGenerateRSS = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/rss.xml');
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not check RSS feed status.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
checkAndGenerateRSS();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Theme handling with transition effects
|
|
||||||
function setupThemeHandling() {
|
|
||||||
// Apply theme from localStorage or system preference
|
|
||||||
const theme = localStorage.getItem('theme');
|
|
||||||
if (
|
|
||||||
theme === 'dark' ||
|
|
||||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
||||||
) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for theme changes
|
|
||||||
document.addEventListener('themeChanged', () => {
|
|
||||||
// Add transition class to body
|
|
||||||
document.body.classList.add('theme-transitioning');
|
|
||||||
|
|
||||||
// Remove class after transition completes
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.classList.remove('theme-transitioning');
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme handling
|
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Page transition effects */
|
|
||||||
#page-transition {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
backdrop-filter: blur-sm(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition spinner animation */
|
|
||||||
.transition-spinner {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
animation: spin 0.7s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .transition-spinner {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-top-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content entrance animation */
|
/* Content entrance animation */
|
||||||
main {
|
main {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
import { ClientRouter } from 'astro:transitions';
|
|
||||||
import BaseLayout from './BaseLayout.astro';
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={title} description={description}>
|
|
||||||
<ClientRouter fallback="swap" />
|
|
||||||
|
|
||||||
<div transition:animate="slide">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Custom transition styles */
|
|
||||||
::view-transition-old(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -5,23 +5,8 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
<Layout title="404 - Page Not Found">
|
<Layout title="404 - Page Not Found">
|
||||||
<div
|
<div
|
||||||
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
||||||
|
transition:animate="slide"
|
||||||
>
|
>
|
||||||
<!-- Animated background elements -->
|
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="animate-blob absolute -top-20 -left-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-2000 absolute top-1/2 right-1/4 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content with animation -->
|
<!-- Main content with animation -->
|
||||||
<div class="relative z-10 mx-auto max-w-xl">
|
<div class="relative z-10 mx-auto max-w-xl">
|
||||||
<div class="glitch-wrapper">
|
<div class="glitch-wrapper">
|
||||||
@@ -48,7 +33,8 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
||||||
></span>
|
>
|
||||||
|
</span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -61,7 +47,8 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="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"
|
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
|
||||||
></path>
|
>
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="relative z-10 font-medium">Return Home</span>
|
<span class="relative z-10 font-medium">Return Home</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -81,7 +68,9 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-medium">Go Back</span>
|
<span class="font-medium">Go Back</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -127,97 +116,9 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
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
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-initialize back button after SPA navigation
|
|
||||||
const backButton = document.getElementById('back-button');
|
|
||||||
if (backButton) {
|
|
||||||
backButton.addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Animation for floating blobs */
|
|
||||||
@keyframes blob {
|
|
||||||
0% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
transform: translate(30px, -50px) scale(1.1);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
transform: translate(-20px, 20px) scale(0.9);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob 7s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-4000 {
|
|
||||||
animation-delay: 4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glitch effect for 404 text */
|
/* Glitch effect for 404 text */
|
||||||
.glitch-wrapper {
|
.glitch-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
import DynamicIcon from '../utils/DynamicIcon.tsx';
|
||||||
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';
|
||||||
@@ -17,23 +16,16 @@ const skills = await directus.request(
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="About Me" description={global.description}>
|
<BaseLayout title="About Me" description={global.description}>
|
||||||
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
<div
|
||||||
|
class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
<!-- 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 -->
|
|
||||||
<div
|
|
||||||
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
||||||
<div class="order-2 text-center md:order-1 md:text-left">
|
<div class="hero-text order-2 text-center md:order-1 md:text-left">
|
||||||
<h1
|
<h1
|
||||||
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
|
class="theme-transition-color hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
Hello, I'm <span
|
Hello, I'm <span
|
||||||
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
|
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"
|
||||||
@@ -42,7 +34,7 @@ const skills = await directus.request(
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
|
class="theme-transition-color hero-text mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
|
||||||
>
|
>
|
||||||
{about.background}
|
{about.background}
|
||||||
</p>
|
</p>
|
||||||
@@ -65,13 +57,6 @@ const skills = await directus.request(
|
|||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decorative elements -->
|
|
||||||
<div
|
|
||||||
class="theme-transition-all absolute -right-4 -bottom-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg sm:-right-6 sm:-bottom-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24 dark:border-zinc-900 dark:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<span class="text-2xl sm:text-3xl">👋</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,16 +76,22 @@ const skills = await directus.request(
|
|||||||
></span>
|
></span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="theme-transition-all prose prose-zinc dark:prose-invert max-w-none">
|
<div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none">
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
<p
|
||||||
|
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
|
||||||
|
>
|
||||||
{about.experience}
|
{about.experience}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
<p
|
||||||
|
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
|
||||||
|
>
|
||||||
{about.education}
|
{about.education}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
<p
|
||||||
|
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
|
||||||
|
>
|
||||||
{about.certifications}
|
{about.certifications}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,13 +110,16 @@ const skills = await directus.request(
|
|||||||
<!-- Main slider container -->
|
<!-- Main slider container -->
|
||||||
<div class="slider-track animate-slide flex">
|
<div class="slider-track animate-slide flex">
|
||||||
{
|
{
|
||||||
skills.map((skill, index) => (
|
[...skills, ...skills, ...skills].map((skill, index) => (
|
||||||
<div class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600">
|
<div
|
||||||
|
key={`${skill.title}-${index}`}
|
||||||
|
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"
|
||||||
|
>
|
||||||
<div class="p-4 sm:p-6">
|
<div class="p-4 sm:p-6">
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-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="flex items-center gap-2 sm:gap-4">
|
||||||
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
|
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
|
||||||
<skill.icon />
|
<DynamicIcon name={skill.icon} />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
||||||
{skill.title}
|
{skill.title}
|
||||||
@@ -164,7 +158,6 @@ const skills = await directus.request(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Section -->
|
<!-- Contact Section -->
|
||||||
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
||||||
<h2
|
<h2
|
||||||
@@ -178,205 +171,62 @@ const skills = await directus.request(
|
|||||||
I'm always open to new opportunities and collaborations. If you'd like to work together or
|
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.
|
just say hello, feel free to reach out.
|
||||||
</p>
|
</p>
|
||||||
|
<div class="group">
|
||||||
<a
|
<a
|
||||||
href=`mailto:${global.email}`
|
href=`mailto:${global.email}`
|
||||||
class="hover theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
|
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 group-hover:bg-blue-600 group-hover:text-zinc-100 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:group-hover:bg-blue-600 dark:group-hover:text-zinc-100"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
||||||
stroke-width="2"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
></path>
|
stroke="currentColor"
|
||||||
</svg>
|
>
|
||||||
Say Hello
|
<path
|
||||||
</a>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span class="relative inline-block overflow-hidden">
|
||||||
|
<span class="relative z-10">Say Hello</span>
|
||||||
|
<span
|
||||||
|
class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-100 transition-all duration-300 group-hover:w-full"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Blob animation */
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob-bounce 8s infinite ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(5%, 5%) scale(1.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, 10%) scale(1);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-5%, 5%) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tech Stack Slider */
|
|
||||||
.slider-track {
|
|
||||||
width: fit-content;
|
|
||||||
animation: scroll 40s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.slider-track {
|
|
||||||
animation: scroll 60s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-stack-slider:hover .slider-track {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card {
|
|
||||||
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce animation complexity on mobile for better performance */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.skill-card {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
transform: translateY(-5px) !important;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -10%;
|
|
||||||
left: -10%;
|
|
||||||
width: 120%;
|
|
||||||
height: 120%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at center,
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
rgba(255, 255, 255, 0) 70%
|
|
||||||
);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover:before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
animation: progress-shine 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-shine {
|
|
||||||
0% {
|
|
||||||
left: -100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
min-height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link {
|
|
||||||
min-width: 44px;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition effect */
|
|
||||||
:global(.theme-switching) .theme-transition-element {
|
|
||||||
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth card transition during theme switch */
|
|
||||||
.skill-card.theme-transition-element {
|
|
||||||
transition:
|
|
||||||
background-color var(--theme-transition),
|
|
||||||
border-color var(--theme-transition),
|
|
||||||
color var(--theme-transition),
|
|
||||||
box-shadow var(--theme-transition),
|
|
||||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Wait for the DOM to be fully loaded
|
document.addEventListener('astro:page-load', () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// Add smooth reveal animations for content after loading
|
||||||
const sliderTrack = document.querySelector('.slider-track');
|
const animateContent = () => {
|
||||||
|
const heroElements = document.querySelectorAll(
|
||||||
|
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
|
||||||
|
);
|
||||||
|
heroElements.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
animateContent();
|
||||||
|
|
||||||
// Create seamless infinite scrolling effect
|
// Create seamless infinite scrolling effect
|
||||||
|
const sliderTrack = document.querySelector('.slider-track');
|
||||||
function setupInfiniteScroll() {
|
function setupInfiniteScroll() {
|
||||||
const cards = document.querySelectorAll('.skill-card');
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
if (!cards.length) return;
|
if (!cards.length) return;
|
||||||
|
|
||||||
// Clone the first set of cards and append to create seamless loop
|
|
||||||
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
|
|
||||||
|
|
||||||
// Set proper animation based on screen size
|
// Set proper animation based on screen size
|
||||||
function updateScrollAnimation() {
|
function updateScrollAnimation() {
|
||||||
if (window.innerWidth >= 640) {
|
if (window.innerWidth >= 640) {
|
||||||
@@ -461,9 +311,7 @@ const skills = await directus.request(
|
|||||||
|
|
||||||
// Handle theme transition
|
// Handle theme transition
|
||||||
document.addEventListener('themeChange', () => {
|
document.addEventListener('themeChange', () => {
|
||||||
// Add special effects during theme transition
|
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
// Add staggered animation delay
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
card.classList.add('theme-changing');
|
card.classList.add('theme-changing');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -475,103 +323,155 @@ const skills = await directus.request(
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<style>
|
||||||
// Handle SPA transitions for about page
|
/* Tech Stack Slider */
|
||||||
function setupSPATransitions() {
|
.slider-track {
|
||||||
// Handle all internal links for SPA transitions
|
width: fit-content;
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
animation: scroll 40s linear infinite;
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize animations for about page
|
|
||||||
function animateAboutContent() {
|
|
||||||
// Animate hero section elements
|
|
||||||
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
|
||||||
heroElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
el.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 150
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate profile image
|
|
||||||
const profileImage = document.querySelector('.aspect-square');
|
|
||||||
if (profileImage) {
|
|
||||||
setTimeout(() => {
|
|
||||||
profileImage.classList.add('animate-reveal');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate skill bars with staggered delay
|
|
||||||
const skillBars = document.querySelectorAll('.skill-bar');
|
|
||||||
skillBars.forEach((bar, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
bar.classList.add('animate-skill');
|
|
||||||
},
|
|
||||||
500 + index * 100
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate sections with staggered delay
|
|
||||||
const sections = document.querySelectorAll('section');
|
|
||||||
sections.forEach((section, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
section.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
300 + index * 200
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
|
||||||
animateAboutContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
@keyframes scroll {
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
@media (min-width: 640px) {
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
.slider-track {
|
||||||
|
animation: scroll 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
@keyframes scroll {
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
0% {
|
||||||
</script>
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-stack-slider:hover .slider-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card {
|
||||||
|
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce animation complexity on mobile */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.skill-card {
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
transform: translateY(-5px) !important;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 70%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
animation: progress-shine 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-shine {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch targets for mobile */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content reveal animations */
|
||||||
|
.hero-text h1,
|
||||||
|
.hero-text span,
|
||||||
|
.hero-text p,
|
||||||
|
.hero-text ~ div {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition:
|
||||||
|
opacity 0.8s ease,
|
||||||
|
transform 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-reveal {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme transition effect */
|
||||||
|
:global(.theme-switching) .theme-transition-element {
|
||||||
|
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth card transition during theme switch */
|
||||||
|
.skill-card.theme-transition-element {
|
||||||
|
transition:
|
||||||
|
background-color var(--theme-transition),
|
||||||
|
border-color var(--theme-transition),
|
||||||
|
color var(--theme-transition),
|
||||||
|
box-shadow var(--theme-transition),
|
||||||
|
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -41,14 +41,14 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
updated_date={post.updated_date}
|
updated_date={post.updated_date}
|
||||||
tags={post.tags}
|
tags={post.tags}
|
||||||
>
|
>
|
||||||
<!-- Main Content - Enhanced with better typography and spacing -->
|
<!-- Main Content -->
|
||||||
<div
|
<div
|
||||||
class="prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none"
|
class="hero-text prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none"
|
||||||
>
|
>
|
||||||
<div set:html={post.content} />
|
<div set:html={post.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next/Previous Navigation - Improved responsive design -->
|
<!-- Next/Previous Navigation -->
|
||||||
<div
|
<div
|
||||||
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800"
|
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800"
|
||||||
>
|
>
|
||||||
@@ -116,7 +116,62 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
</BlogPost>
|
</BlogPost>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Removing TOC-related functions
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
// Show button when scrolled down
|
||||||
|
const backToTopButton = document.getElementById('back-to-top');
|
||||||
|
if (backToTopButton) {
|
||||||
|
const toggleBackToTopButton = () => {
|
||||||
|
if (window.scrollY > 300) {
|
||||||
|
backToTopButton.classList.remove('opacity-0', 'invisible');
|
||||||
|
backToTopButton.classList.add('opacity-100', 'visible');
|
||||||
|
} else {
|
||||||
|
backToTopButton.classList.remove('opacity-100', 'visible');
|
||||||
|
backToTopButton.classList.add('opacity-0', 'invisible');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scroll to top when clicked
|
||||||
|
backToTopButton.addEventListener('click', () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check scroll position
|
||||||
|
window.addEventListener('scroll', toggleBackToTopButton);
|
||||||
|
toggleBackToTopButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add smooth reveal animations for content after loading
|
||||||
|
const animateContent = () => {
|
||||||
|
// Animate hero section
|
||||||
|
const heroElements = document.querySelectorAll(
|
||||||
|
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
|
||||||
|
);
|
||||||
|
heroElements.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate posts with staggered delay
|
||||||
|
const articles = document.querySelectorAll('article.group');
|
||||||
|
articles.forEach((article, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
article.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
500 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
animateContent();
|
||||||
|
});
|
||||||
|
|
||||||
// Add copy buttons to code blocks
|
// Add copy buttons to code blocks
|
||||||
function initializeCodeCopyButtons() {
|
function initializeCodeCopyButtons() {
|
||||||
@@ -183,50 +238,9 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SPA transitions for blog post navigation
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links or already processed
|
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main initialization function
|
// Main initialization function
|
||||||
function initializeBlogPost() {
|
function initializeBlogPost() {
|
||||||
// Initialize remaining components
|
|
||||||
initializeCodeCopyButtons();
|
initializeCodeCopyButtons();
|
||||||
setupSPATransitions();
|
|
||||||
|
|
||||||
// Scroll to hash if present in URL
|
// Scroll to hash if present in URL
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
@@ -239,18 +253,28 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
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
|
|
||||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Removing TOC-related styles */
|
/* Content reveal animations */
|
||||||
|
.hero-text h1,
|
||||||
|
.hero-text span,
|
||||||
|
.hero-text p,
|
||||||
|
.hero-text ~ div,
|
||||||
|
article.group {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition:
|
||||||
|
opacity 0.8s ease,
|
||||||
|
transform 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-reveal {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Language badge styling */
|
/* Language badge styling */
|
||||||
.language-badge {
|
.language-badge {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
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 TagList from '../components/TagList.astro';
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
import directus from '../../lib/directus';
|
||||||
import { readItems, readSingleton } from '@directus/sdk';
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
@@ -22,19 +23,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title=`Home | ${global.name}`>
|
<Layout title=`Home | ${global.name}`>
|
||||||
<!-- Hero Section with improved mobile responsiveness -->
|
<section
|
||||||
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
|
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
<div class="relative mx-auto max-w-2xl">
|
<div class="relative mx-auto max-w-2xl">
|
||||||
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
|
||||||
<div
|
|
||||||
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-64 sm:w-64 dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative text-center sm:text-left">
|
<div class="relative text-center sm:text-left">
|
||||||
<h1
|
<h1
|
||||||
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
|
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
|
||||||
@@ -60,7 +53,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/about"
|
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"
|
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-400 dark:hover:text-zinc-100"
|
||||||
>
|
>
|
||||||
<span>More about me</span>
|
<span>More about me</span>
|
||||||
<svg
|
<svg
|
||||||
@@ -76,16 +69,13 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span
|
|
||||||
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
|
||||||
></span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Featured Post Section - Improved for mobile -->
|
<!-- Featured post section -->
|
||||||
<section
|
<section
|
||||||
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
|
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
|
||||||
>
|
>
|
||||||
@@ -100,7 +90,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</h2>
|
</h2>
|
||||||
<a
|
<a
|
||||||
href="/blog"
|
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 sm:self-auto dark:text-zinc-100 dark:hover:text-zinc-300"
|
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
View all posts
|
View all posts
|
||||||
@@ -118,25 +108,22 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</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 -->
|
<!-- Grid for mobile layout -->
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
<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) => (
|
recentPosts.map((post, index) => (
|
||||||
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
||||||
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl dark:bg-zinc-800/50" />
|
<div class="theme-transition-all absolute -inset-x-4 -inset-y-6 z-0 border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 sm:-inset-x-6 sm:rounded-2xl dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70" />
|
||||||
|
|
||||||
{post.image && (
|
{post.image && (
|
||||||
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
||||||
<img
|
<img
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
class="h-full w-full object-cover"
|
||||||
loading={index === 0 ? 'eager' : 'lazy'}
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
width="400"
|
width="400"
|
||||||
height="225"
|
height="225"
|
||||||
@@ -144,12 +131,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 sm:justify-start sm:gap-x-4 dark:text-zinc-400">
|
|
||||||
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
|
||||||
<FormattedDate date={post.published_date} />
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
<a
|
<a
|
||||||
href={`/blog/${post.slug}`}
|
href={`/blog/${post.slug}`}
|
||||||
@@ -160,45 +141,29 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 sm:mt-3 sm:text-left dark:text-zinc-400">
|
<p class="z-10 mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
|
||||||
{post.description}
|
{post.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{post.tags && post.tags.length > 0 && (
|
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
|
||||||
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
|
<FormattedDate date={post.published_date} />
|
||||||
{post.tags.slice(0, 3).map((tag) => (
|
</div>
|
||||||
<a
|
|
||||||
href={`/topics/${tag}`}
|
<TagList tags={post.tags} class="z-10" />
|
||||||
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 sm:px-3 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
{post.tags.length > 3 && (
|
|
||||||
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
|
||||||
+{post.tags.length - 3} more
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`/blog/${post.slug}`}
|
href={`/blog/${post.slug}`}
|
||||||
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
||||||
>
|
>
|
||||||
<span class="relative inline-block overflow-hidden">
|
<span class="relative inline-block overflow-hidden">
|
||||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
<span class="relative z-10">Read article</span>
|
||||||
Read article
|
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
||||||
</span>
|
|
||||||
<span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
|
||||||
Explore now
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
|
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||||
@@ -215,22 +180,22 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Topics/Tags Section - Improved for mobile -->
|
<!-- Topics section -->
|
||||||
{
|
{
|
||||||
allTags.length > 0 && (
|
allTags.length > 0 && (
|
||||||
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
|
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
|
||||||
<div class="mx-auto max-w-3xl">
|
<div class="mx-auto max-w-3xl">
|
||||||
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
|
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
|
||||||
Explore Topics
|
Popular Tags
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
<div class="hover-3d mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
||||||
{allTags.map((tag) => {
|
{allTags.map((tag) => {
|
||||||
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`/topics/${tag}`}
|
href={`/tags/${tag}`}
|
||||||
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/70"
|
class="theme-transition-all flex min-h-[80px] flex-col rounded-xl border border-zinc-300 bg-white/50 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:bg-zinc-800/70"
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-start justify-between">
|
<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">
|
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
@@ -247,29 +212,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 text-center sm:mt-8">
|
|
||||||
<a
|
|
||||||
href="/tags"
|
|
||||||
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
||||||
>
|
|
||||||
<span>View all topics</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@@ -278,8 +220,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Add hover effect for cards on touch devices
|
// Add hover effect for cards on touch devices
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
// 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) {
|
||||||
@@ -297,11 +238,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable hover animations on touch devices for better performance
|
// Disable hover animations on touch devices
|
||||||
document.documentElement.classList.add('touch-device');
|
document.documentElement.classList.add('touch-device');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved viewport height fix for mobile browsers
|
// 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`);
|
||||||
@@ -339,7 +280,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved theme change handler that preserves scroll position and provides smoother transitions
|
// 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;
|
||||||
@@ -445,90 +386,8 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run animations after the loading screen is hidden
|
animateContent();
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
|
||||||
if (loadingScreen) {
|
|
||||||
// Check if loading screen is already hidden (page refresh)
|
|
||||||
if (loadingScreen.style.display === 'none') {
|
|
||||||
animateContent();
|
|
||||||
} else {
|
|
||||||
// Wait for loading screen to hide
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (
|
|
||||||
mutation.target === loadingScreen &&
|
|
||||||
mutation.type === 'attributes' &&
|
|
||||||
mutation.attributeName === 'style' &&
|
|
||||||
loadingScreen.style.display === 'none'
|
|
||||||
) {
|
|
||||||
animateContent();
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(loadingScreen, { attributes: true });
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
setTimeout(animateContent, 3500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If loading screen doesn't exist for some reason
|
|
||||||
animateContent();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA transition handling for homepage
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -555,13 +414,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
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 */
|
|
||||||
:global(*) {
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
|
||||||
transition-duration: var(--theme-transition-duration);
|
|
||||||
transition-timing-function: var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove the forced transition disabling which causes flickering */
|
/* Remove the forced transition disabling which causes flickering */
|
||||||
:global(.theme-switching),
|
:global(.theme-switching),
|
||||||
:global(.theme-switching *) {
|
:global(.theme-switching *) {
|
||||||
@@ -586,6 +438,4 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
transform: translateY(0) !important;
|
transform: translateY(0) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rest of your existing styles... */
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -14,7 +14,6 @@ export async function getStaticPaths() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -41,7 +40,6 @@ const sortedPosts =
|
|||||||
: [];
|
: [];
|
||||||
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 relatedTags = [
|
const relatedTags = [
|
||||||
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
||||||
].slice(0, 5);
|
].slice(0, 5);
|
||||||
@@ -49,20 +47,10 @@ const relatedTags = [
|
|||||||
|
|
||||||
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
||||||
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
|
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
|
||||||
<!-- Header section -->
|
|
||||||
<div class="relative mb-10 sm:mb-16">
|
<div class="relative mb-10 sm:mb-16">
|
||||||
<div
|
|
||||||
class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl sm:h-48 sm:w-48 dark:bg-zinc-900/20"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative text-center sm:text-left">
|
<div class="relative text-center sm:text-left">
|
||||||
<a
|
<a
|
||||||
href="/tags"
|
href="/blog#topics"
|
||||||
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"
|
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
|
<svg
|
||||||
@@ -76,9 +64,11 @@ const relatedTags = [
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Back to all topics</span>
|
<span>Back to blog</span>
|
||||||
<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"
|
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>
|
></span>
|
||||||
@@ -102,8 +92,9 @@ const relatedTags = [
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="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"
|
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
||||||
></path>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"></path>
|
</path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"> </path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,7 +106,7 @@ const relatedTags = [
|
|||||||
<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 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
|
||||||
></span>
|
></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"
|
class="animate-expand absolute -bottom-1 left-0 h-1 w-full bg-zinc-900 opacity-70 dark:bg-zinc-100"
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -136,14 +127,14 @@ const relatedTags = [
|
|||||||
<!-- Related tags section -->
|
<!-- Related tags section -->
|
||||||
{
|
{
|
||||||
relatedTags.length > 0 && (
|
relatedTags.length > 0 && (
|
||||||
<div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
|
<div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
|
||||||
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
|
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
|
||||||
Related topics
|
Related topics
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
|
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
|
||||||
{relatedTags.map((relatedTag) => (
|
{relatedTags.map((relatedTag) => (
|
||||||
<a
|
<a
|
||||||
href={`/topics/${relatedTag}`}
|
href={`/tags/${relatedTag}`}
|
||||||
class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
#{relatedTag}
|
#{relatedTag}
|
||||||
@@ -156,55 +147,31 @@ const relatedTags = [
|
|||||||
|
|
||||||
<!-- Posts list -->
|
<!-- Posts list -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10">
|
<div
|
||||||
|
class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative space-y-6 sm:space-y-8">
|
<div class="relative space-y-6 sm:space-y-8">
|
||||||
{
|
{
|
||||||
sortedPosts.map((post) => (
|
sortedPosts.map((post) => (
|
||||||
<article class="hover-card group relative mx-auto flex max-w-2xl flex-col rounded-2xl border border-zinc-200 p-5 transition-all duration-300 hover:bg-zinc-50/80 hover:shadow-md sm:mx-0 sm:p-8 dark:border-zinc-800 dark:hover:bg-zinc-900/50">
|
<article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
|
||||||
<div class="absolute inset-0 rounded-2xl 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="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
||||||
{post.image && (
|
{post.image && (
|
||||||
<div class="mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl shadow-xs transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56">
|
<div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl 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="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
class="h-full w-full object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="z-10 flex-1">
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
|
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
|
||||||
{post.published_date && (
|
|
||||||
<time
|
|
||||||
datetime={post.published_date.toLocaleString()}
|
|
||||||
class="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
|
||||||
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<FormattedDate date={post.published_date} />
|
|
||||||
</time>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
|
||||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
||||||
{post.title}
|
{post.title}
|
||||||
</a>
|
</a>
|
||||||
@@ -213,15 +180,19 @@ const relatedTags = [
|
|||||||
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
|
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
|
||||||
{post.description}
|
{post.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
|
||||||
|
<FormattedDate date={post.published_date} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
|
<div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
|
||||||
{post.tags && post.tags.length > 0 && (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
|
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
|
||||||
{post.tags.slice(0, 3).map((postTag) => (
|
{post.tags.slice(0, 3).map((postTag) => (
|
||||||
<a
|
<a
|
||||||
href={`/topics/${postTag}`}
|
href={`/blog/${postTag}`}
|
||||||
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
||||||
postTag === tag
|
postTag === tag
|
||||||
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
||||||
@@ -241,31 +212,24 @@ const relatedTags = [
|
|||||||
|
|
||||||
<div class="mx-auto sm:mr-0 sm:ml-auto">
|
<div class="mx-auto sm:mr-0 sm:ml-auto">
|
||||||
<a
|
<a
|
||||||
href={`/blog/${post.slug}/`}
|
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"
|
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
||||||
aria-hidden="true"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
>
|
||||||
<span class="relative inline-block overflow-hidden">
|
<span class="relative inline-block overflow-hidden">
|
||||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
<span class="relative z-10">Read article</span>
|
||||||
Read article
|
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
||||||
</span>
|
|
||||||
<span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
|
||||||
Explore now
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
aria-hidden="true"
|
||||||
stroke-width="1.5"
|
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
|
||||||
stroke="currentColor"
|
|
||||||
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||||
|
stroke-width="1.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
@@ -277,7 +241,7 @@ const relatedTags = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state với màu zinc -->
|
<!-- Empty state -->
|
||||||
{
|
{
|
||||||
sortedPosts.length === 0 && (
|
sortedPosts.length === 0 && (
|
||||||
<div class="py-12 text-center sm:py-20">
|
<div class="py-12 text-center sm:py-20">
|
||||||
@@ -327,6 +291,61 @@ const relatedTags = [
|
|||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
// Add smooth reveal animations for content after loading
|
||||||
|
const animateContent = () => {
|
||||||
|
// Animate hero section
|
||||||
|
const heroElements = document.querySelectorAll(
|
||||||
|
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
|
||||||
|
);
|
||||||
|
heroElements.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate posts with staggered delay
|
||||||
|
const articles = document.querySelectorAll('article.group');
|
||||||
|
articles.forEach((article, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
article.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
500 + index * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
animateContent();
|
||||||
|
|
||||||
|
// Add hover effect for cards on touch devices
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
|
||||||
|
if (isTouchDevice) {
|
||||||
|
const cards = document.querySelectorAll('.hover-3d');
|
||||||
|
|
||||||
|
cards.forEach((card) => {
|
||||||
|
card.addEventListener('touchstart', () => {
|
||||||
|
card.classList.add('is-touched');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('touchend', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.remove('is-touched');
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable hover animations on touch devices
|
||||||
|
document.documentElement.classList.add('touch-device');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Grid pattern background */
|
/* Grid pattern background */
|
||||||
.bg-grid-pattern {
|
.bg-grid-pattern {
|
||||||
@@ -354,7 +373,7 @@ const relatedTags = [
|
|||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
width: 50%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,43 +381,22 @@ const relatedTags = [
|
|||||||
animation: expand 1s ease-out forwards;
|
animation: expand 1s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blob animation */
|
/* Content reveal animations */
|
||||||
.animate-blob {
|
.hero-text h1,
|
||||||
animation: blob 7s infinite;
|
.hero-text span,
|
||||||
}
|
.hero-text p,
|
||||||
|
.hero-text ~ div,
|
||||||
.animation-delay-2000 {
|
article.group {
|
||||||
animation-delay: 2s;
|
opacity: 0;
|
||||||
}
|
transform: translateY(20px);
|
||||||
|
|
||||||
@keyframes blob {
|
|
||||||
0% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
transform: translate(20px, -20px) scale(1.1);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
transform: translate(-20px, 20px) scale(0.9);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover card effect */
|
|
||||||
.hover-card {
|
|
||||||
transform: translateY(0);
|
|
||||||
transition:
|
transition:
|
||||||
transform 0.3s ease,
|
opacity 0.8s ease,
|
||||||
box-shadow 0.3s ease,
|
transform 0.8s ease;
|
||||||
background-color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
.animate-reveal {
|
||||||
.hover-card:hover {
|
opacity: 1 !important;
|
||||||
transform: translateY(-2px);
|
transform: translateY(0) !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Line clamp for descriptions */
|
/* Line clamp for descriptions */
|
||||||
@@ -423,98 +421,3 @@ const relatedTags = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for tag pages
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize animations for tag page
|
|
||||||
function animateTagContent() {
|
|
||||||
// Animate header elements
|
|
||||||
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
|
|
||||||
headerElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
el.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 150
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate posts with staggered delay
|
|
||||||
const articles = document.querySelectorAll('article');
|
|
||||||
articles.forEach((article, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
article.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
400 + index * 100
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate related tags
|
|
||||||
const relatedTags = document.querySelectorAll('.related-tags a');
|
|
||||||
relatedTags.forEach((tag, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
tag.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
600 + index * 50
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
|
||||||
animateTagContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add this at the end of your page -->
|
|
@@ -1,714 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
||||||
|
|
||||||
import directus from '../../../lib/directus';
|
|
||||||
import { readItems } from '@directus/sdk';
|
|
||||||
|
|
||||||
const posts = await directus.request(
|
|
||||||
readItems('posts', {
|
|
||||||
fields: ['*'],
|
|
||||||
sort: ['-published_date'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
|
|
||||||
|
|
||||||
// Count posts for each tag and create tag objects with additional data
|
|
||||||
const tagObjects = tags.map((tag) => {
|
|
||||||
const count = posts.filter((post) => post.tags?.includes(tag)).length;
|
|
||||||
// Generate a consistent but random-looking hue for each tag
|
|
||||||
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
|
||||||
return {
|
|
||||||
name: tag,
|
|
||||||
count,
|
|
||||||
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
|
|
||||||
hue,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title="Explore Tags">
|
|
||||||
<div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
|
|
||||||
<!-- Enhanced header section with animated elements - improved for mobile -->
|
|
||||||
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
|
|
||||||
<div
|
|
||||||
class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-16 -bottom-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-blob animation-delay-4000 theme-transition-bg absolute top-8 right-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl sm:h-32 sm:w-32 md:h-40 md:w-40 dark:bg-zinc-700/20"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1
|
|
||||||
class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<span class="relative inline-block">
|
|
||||||
<span class="relative inline-block">
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-xs dark:from-zinc-800/50 dark:to-zinc-700/50"
|
|
||||||
></span>
|
|
||||||
<span class="relative">Explore</span>
|
|
||||||
</span>
|
|
||||||
{' '}
|
|
||||||
<span class="relative inline-block">
|
|
||||||
Topics
|
|
||||||
<span
|
|
||||||
class="animate-underline theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-full origin-left transform bg-gradient-to-r from-zinc-400 to-zinc-600 sm:-bottom-2 sm:h-1 dark:from-zinc-600 dark:to-zinc-400"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 sm:text-base md:text-lg lg:text-xl dark:text-zinc-400"
|
|
||||||
>
|
|
||||||
Discover content organized by your interests
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
tags.length === 0 ? (
|
|
||||||
<div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
|
|
||||||
<div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24 dark:bg-zinc-800">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="theme-transition-color h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 md:h-12 md:w-12 dark:text-zinc-400"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
|
||||||
/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="theme-transition-color text-lg font-medium text-zinc-800 sm:text-xl md:text-2xl dark:text-zinc-200">
|
|
||||||
No tags found yet.
|
|
||||||
</p>
|
|
||||||
<p class="theme-transition-color mt-2 text-xs text-zinc-500 sm:text-sm md:text-base dark:text-zinc-500">
|
|
||||||
Check back later for categorized content.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="flex w-full justify-center">
|
|
||||||
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-xs sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8 dark:border-zinc-800 dark:bg-zinc-900/50">
|
|
||||||
<div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
|
|
||||||
<div class="theme-transition-bg absolute -top-8 -right-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
|
||||||
<div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
|
||||||
|
|
||||||
<h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl dark:text-zinc-100">
|
|
||||||
Popular Topics
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 xxxs:gap-2 xxs:gap-2 xs:gap-2 grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 sm:gap-3 md:grid-cols-4 md:gap-4 lg:grid-cols-5">
|
|
||||||
{sortedTags.map((tag) => (
|
|
||||||
<a
|
|
||||||
href={`/topics/${tag.name}`}
|
|
||||||
class="theme-transition-element theme-ripple group relative min-w-0 grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl dark:border-zinc-800 dark:hover:border-zinc-700"
|
|
||||||
style={`--tag-hue: ${tag.hue};`}
|
|
||||||
>
|
|
||||||
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
|
|
||||||
|
|
||||||
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
|
|
||||||
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-xs transition-all duration-300 sm:h-8 sm:w-8 md:h-10 md:w-10 dark:bg-zinc-800 dark:text-zinc-300">
|
|
||||||
<span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
|
|
||||||
#
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1 overflow-hidden">
|
|
||||||
<h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate text-[10px] font-bold break-words hyphens-auto text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-sm md:text-base dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
|
||||||
{tag.name}
|
|
||||||
</h3>
|
|
||||||
<p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 sm:text-xs md:text-xs dark:text-zinc-400">
|
|
||||||
{tag.count} article{tag.count !== 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Ultra-reliable responsiveness handling
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Fix viewport width issues on mobile
|
|
||||||
const fixViewportWidth = () => {
|
|
||||||
// Force the viewport to be exactly the width of the device
|
|
||||||
const viewport = document.querySelector('meta[name="viewport"]');
|
|
||||||
if (!viewport) {
|
|
||||||
const meta = document.createElement('meta');
|
|
||||||
meta.name = 'viewport';
|
|
||||||
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
|
||||||
} else {
|
|
||||||
viewport.setAttribute(
|
|
||||||
'content',
|
|
||||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix for horizontal overflow
|
|
||||||
document.body.style.overflowX = 'hidden';
|
|
||||||
document.documentElement.style.overflowX = 'hidden';
|
|
||||||
document.documentElement.style.width = '100%';
|
|
||||||
document.body.style.width = '100%';
|
|
||||||
};
|
|
||||||
|
|
||||||
fixViewportWidth();
|
|
||||||
|
|
||||||
// Adjust tag items based on screen size with extreme precision
|
|
||||||
const adjustTagItems = () => {
|
|
||||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
|
||||||
const width =
|
|
||||||
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
|
||||||
const isVerySmall = width < 360;
|
|
||||||
const isExtremelySmall = width < 280;
|
|
||||||
const isMicroScreen = width < 240;
|
|
||||||
|
|
||||||
// Fix container width to match viewport exactly
|
|
||||||
const container = document.querySelector('.tag-cloud');
|
|
||||||
if (container) {
|
|
||||||
container.style.maxWidth = '100vw';
|
|
||||||
container.style.width = '100%';
|
|
||||||
container.style.boxSizing = 'border-box';
|
|
||||||
|
|
||||||
// Remove any margins that might cause overflow
|
|
||||||
container.style.marginLeft = '0';
|
|
||||||
container.style.marginRight = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix grid width
|
|
||||||
const grid = document.querySelector('.grid');
|
|
||||||
if (grid) {
|
|
||||||
grid.style.width = '100%';
|
|
||||||
grid.style.maxWidth = '100%';
|
|
||||||
}
|
|
||||||
|
|
||||||
tagItems.forEach((item) => {
|
|
||||||
// Set appropriate classes based on screen size
|
|
||||||
if (isMicroScreen) {
|
|
||||||
item.classList.add('micro-screen');
|
|
||||||
item.classList.remove('extremely-small-screen', 'very-small-screen');
|
|
||||||
} else if (isExtremelySmall) {
|
|
||||||
item.classList.add('extremely-small-screen');
|
|
||||||
item.classList.remove('very-small-screen', 'micro-screen');
|
|
||||||
} else if (isVerySmall) {
|
|
||||||
item.classList.add('very-small-screen');
|
|
||||||
item.classList.remove('extremely-small-screen', 'micro-screen');
|
|
||||||
} else {
|
|
||||||
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure text doesn't overflow on small screens
|
|
||||||
const tagName = item.querySelector('h3');
|
|
||||||
const tagCount = item.querySelector('p');
|
|
||||||
|
|
||||||
if (tagName) {
|
|
||||||
// Set title for accessibility
|
|
||||||
tagName.title = tagName.textContent.trim();
|
|
||||||
|
|
||||||
// Adjust text length based on screen size
|
|
||||||
if (isMicroScreen && tagName.textContent.length > 6) {
|
|
||||||
tagName.dataset.fullText = tagName.textContent;
|
|
||||||
tagName.textContent = tagName.textContent.substring(0, 6) + '...';
|
|
||||||
} else if (isExtremelySmall && tagName.textContent.length > 8) {
|
|
||||||
tagName.dataset.fullText = tagName.textContent;
|
|
||||||
tagName.textContent = tagName.textContent.substring(0, 8) + '...';
|
|
||||||
} else if (isVerySmall && tagName.textContent.length > 12) {
|
|
||||||
tagName.dataset.fullText = tagName.textContent;
|
|
||||||
tagName.textContent = tagName.textContent.substring(0, 12) + '...';
|
|
||||||
} else if (tagName.dataset.fullText) {
|
|
||||||
tagName.textContent = tagName.dataset.fullText;
|
|
||||||
delete tagName.dataset.fullText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the tag hue for hover effects
|
|
||||||
const hue = item.style.getPropertyValue('--tag-hue');
|
|
||||||
item.style.setProperty('--hover-hue', hue);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run on load
|
|
||||||
adjustTagItems();
|
|
||||||
|
|
||||||
// Run on resize with optimized debounce
|
|
||||||
let resizeTimer;
|
|
||||||
const handleResize = () => {
|
|
||||||
if (resizeTimer) {
|
|
||||||
window.cancelAnimationFrame(resizeTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
resizeTimer = window.requestAnimationFrame(() => {
|
|
||||||
fixViewportWidth();
|
|
||||||
adjustTagItems();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
window.addEventListener('orientationchange', handleResize);
|
|
||||||
|
|
||||||
// Ensure layout is recalculated after page is fully loaded
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
fixViewportWidth();
|
|
||||||
adjustTagItems();
|
|
||||||
// Force recalculation after images and fonts are loaded
|
|
||||||
setTimeout(() => {
|
|
||||||
fixViewportWidth();
|
|
||||||
adjustTagItems();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fix for iOS Safari and other mobile browsers
|
|
||||||
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--safe-area-inset-bottom',
|
|
||||||
'env(safe-area-inset-bottom)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fix for mobile viewport height issues
|
|
||||||
const setVh = () => {
|
|
||||||
const vh = window.innerHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
setVh();
|
|
||||||
window.addEventListener('resize', setVh);
|
|
||||||
window.addEventListener('orientationchange', () => {
|
|
||||||
// Wait for orientation change to complete
|
|
||||||
setTimeout(() => {
|
|
||||||
setVh();
|
|
||||||
fixViewportWidth();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add touch support for mobile devices
|
|
||||||
const addTouchSupport = () => {
|
|
||||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
|
||||||
|
|
||||||
tagItems.forEach((item) => {
|
|
||||||
item.addEventListener(
|
|
||||||
'touchstart',
|
|
||||||
() => {
|
|
||||||
item.classList.add('touch-active');
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
item.addEventListener(
|
|
||||||
'touchend',
|
|
||||||
() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
item.classList.remove('touch-active');
|
|
||||||
}, 150);
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cancel active state if touch moves away
|
|
||||||
item.addEventListener(
|
|
||||||
'touchmove',
|
|
||||||
(e) => {
|
|
||||||
const touch = e.touches[0];
|
|
||||||
const rect = item.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (
|
|
||||||
touch.clientX < rect.left ||
|
|
||||||
touch.clientX > rect.right ||
|
|
||||||
touch.clientY < rect.top ||
|
|
||||||
touch.clientY > rect.bottom
|
|
||||||
) {
|
|
||||||
item.classList.remove('touch-active');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
addTouchSupport();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Base styles */
|
|
||||||
.tag-cloud {
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(0, 0, 0, 0.03),
|
|
||||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
|
||||||
0 4px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
perspective: 1000px;
|
|
||||||
transition: all var(--theme-transition);
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix for horizontal overflow */
|
|
||||||
:global(html),
|
|
||||||
:global(body) {
|
|
||||||
overflow-x: hidden;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.max-w-6xl) {
|
|
||||||
max-width: 100% !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ultra-responsive breakpoints for extreme reliability */
|
|
||||||
/* Micro screens (below 240px) */
|
|
||||||
@media (max-width: 239px) {
|
|
||||||
.tag-cloud {
|
|
||||||
padding: 0.5rem !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border-radius: 0.25rem !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-cloud h2 {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
margin-bottom: 0.375rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
|
||||||
gap: 0.375rem !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-screen .flex {
|
|
||||||
padding: 0.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-screen h3 {
|
|
||||||
font-size: 0.625rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-screen p {
|
|
||||||
font-size: 0.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra extra extra small screens (240px-279px) */
|
|
||||||
@media (min-width: 240px) and (max-width: 279px) {
|
|
||||||
.xxxs\:grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:px-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:py-2 {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:w-6 {
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:h-6 {
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:gap-2 {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxxs\:text-\[9px\] {
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra extra small screens (280px-359px) */
|
|
||||||
@media (min-width: 280px) {
|
|
||||||
.xxs\:grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:px-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:py-2 {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:w-6 {
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:h-6 {
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:gap-2 {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xxs\:text-\[9px\] {
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra small screens (360px-639px) */
|
|
||||||
@media (min-width: 360px) {
|
|
||||||
.xs\:grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:px-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:py-2 {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:w-7 {
|
|
||||||
width: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:h-7 {
|
|
||||||
height: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:text-sm {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:gap-2 {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:text-\[10px\] {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure text doesn't overflow on small screens */
|
|
||||||
.truncate {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure proper word breaking for long tag names */
|
|
||||||
.break-words {
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hyphens-auto {
|
|
||||||
hyphens: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved shadow for dark mode */
|
|
||||||
:global(.dark) .tag-cloud {
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
|
||||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent layout shifts */
|
|
||||||
.grow {
|
|
||||||
grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-w-0 {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure container doesn't overflow */
|
|
||||||
.overflow-hidden {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Touch support for mobile */
|
|
||||||
.touch-active {
|
|
||||||
transform: scale(0.97) !important;
|
|
||||||
opacity: 0.9;
|
|
||||||
transition:
|
|
||||||
transform 0.15s ease-in-out,
|
|
||||||
opacity 0.15s ease-in-out !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation for blob */
|
|
||||||
@keyframes blob {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(10px, -10px) scale(1.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, 20px) scale(0.95);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-10px, -10px) scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob 20s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-4000 {
|
|
||||||
animation-delay: 4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation for underline */
|
|
||||||
@keyframes underline {
|
|
||||||
0% {
|
|
||||||
transform: scaleX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-underline {
|
|
||||||
animation: underline 1.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix for iOS Safari notch */
|
|
||||||
@supports (padding: max(0px)) {
|
|
||||||
.tag-cloud {
|
|
||||||
padding-left: max(0.75rem, env(safe-area-inset-left));
|
|
||||||
padding-right: max(0.75rem, env(safe-area-inset-right));
|
|
||||||
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for tags index page
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add hover effect for tag cards on touch devices
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
||||||
|
|
||||||
if (isTouchDevice) {
|
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
|
||||||
|
|
||||||
tagCards.forEach((card) => {
|
|
||||||
card.addEventListener('touchstart', () => {
|
|
||||||
card.classList.add('is-touched');
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate tag cards with staggered delay
|
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
|
||||||
tagCards.forEach((card, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
card.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 50
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
|
@@ -1,6 +1,9 @@
|
|||||||
/* Remove all the complex mobile menu styles and keep only what's necessary */
|
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* Dark mode support for Tailwind CSS v4 */
|
||||||
|
/* https://tailwindcss.com/docs/dark-mode */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
@@ -12,6 +15,7 @@
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-padding-top: 5rem;
|
scroll-padding-top: 5rem;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -38,7 +42,7 @@
|
|||||||
scroll-padding-top: 4rem;
|
scroll-padding-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better touch targets on mobile */
|
/* Touch targets on mobile */
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
@apply min-h-[44px];
|
@apply min-h-[44px];
|
||||||
@@ -131,21 +135,4 @@ button {
|
|||||||
a.hover:hover,
|
a.hover:hover,
|
||||||
button:hover {
|
button:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth page transitions */
|
|
||||||
.page-transition {
|
|
||||||
transition:
|
|
||||||
opacity 0.3s ease,
|
|
||||||
transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-entering {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-entered {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
40
src/utils/DynamicIcon.tsx
Normal file
40
src/utils/DynamicIcon.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as FaIcons from 'react-icons/fa';
|
||||||
|
import * as MdIcons from 'react-icons/md';
|
||||||
|
import * as AiIcons from 'react-icons/ai';
|
||||||
|
import * as GiIcons from 'react-icons/gi';
|
||||||
|
import * as IoIcons from 'react-icons/io';
|
||||||
|
import * as CiIcons from 'react-icons/ci';
|
||||||
|
import * as FiIcons from 'react-icons/fi';
|
||||||
|
import * as LuIcons from 'react-icons/lu';
|
||||||
|
import * as SiIcons from 'react-icons/si';
|
||||||
|
|
||||||
|
// Load React Icon library dynamically from attributes in Directus
|
||||||
|
|
||||||
|
const iconSets = {
|
||||||
|
fa: FaIcons,
|
||||||
|
md: MdIcons,
|
||||||
|
ai: AiIcons,
|
||||||
|
gi: GiIcons,
|
||||||
|
io: IoIcons,
|
||||||
|
ci: CiIcons,
|
||||||
|
fi: FiIcons,
|
||||||
|
lu: LuIcons,
|
||||||
|
si: SiIcons,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DynamicIcon = ({ name, set = 'fa' }: { name: string; set: string }) => {
|
||||||
|
let IconComponent = FaIcons.FaAlignCenter;
|
||||||
|
|
||||||
|
if (name.startsWith('Fa')) {
|
||||||
|
IconComponent = iconSets['fa'][name];
|
||||||
|
} else if (name.startsWith('Si')) {
|
||||||
|
IconComponent = iconSets['si'][name];
|
||||||
|
} else {
|
||||||
|
IconComponent = iconSets[set][name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IconComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicIcon;
|
@@ -3,6 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
Reference in New Issue
Block a user