Compare commits

...

32 Commits

Author SHA1 Message Date
bcb91972a1 Merge pull request 'Update astro monorepo' (#42) from renovate/astro-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 23s
test-build / build (push) Successful in 28s
Reviewed-on: #42
2025-07-20 03:34:37 +00:00
b11666decb Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-20 00:02:37 +00:00
a947a05041 Merge pull request 'Update dependency eslint-config-prettier to v10.1.8' (#43) from renovate/eslint-config-prettier-10.x into main
All checks were successful
renovate / renovate (push) Successful in 47s
test-build / build (push) Successful in 34s
2025-07-20 00:01:54 +00:00
297c573281 Update dependency eslint-config-prettier to v10.1.8
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 25s
2025-07-20 00:01:31 +00:00
9093594973 Update dependency astro to v5.11.2
All checks were successful
test-build / build (push) Successful in 37s
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Successful in 21s
test-build / build (pull_request) Successful in 36s
2025-07-18 00:01:29 +00:00
77ce0a1182 Update dependency framer-motion to v12.23.6
All checks were successful
renovate / renovate (push) Successful in 28s
test-build / build (pull_request) Successful in 29s
test-build / build (push) Successful in 40s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 16:14:52 +00:00
799e6b6090 Merge pull request 'Update typescript-eslint monorepo to v8.37.0' (#38) from renovate/typescript-eslint-monorepo into main
Some checks failed
test-build / build (push) Successful in 26s
process-repository / process-repository (push) Failing after 11s
renovate / renovate (push) Successful in 1m21s
Reviewed-on: #38
2025-07-16 16:14:17 +00:00
735e4b4877 Update typescript-eslint monorepo to v8.37.0
All checks were successful
test-build / build (pull_request) Successful in 27s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 02:59:27 +00:00
3e12a8647d update tag
All checks were successful
test-build / build (push) Successful in 51s
renovate / renovate (push) Successful in 1m9s
release-image / release (push) Successful in 3m32s
2025-07-15 21:58:20 -05:00
e07210638e add astro native SPA transition 2025-07-15 21:58:20 -05:00
22d5b50f73 formatting and layout 2025-07-15 21:58:20 -05:00
40acf8f34a strip theme transition on load to use early script 2025-07-15 21:58:20 -05:00
543516baba remove SPA scripts 2025-07-15 21:58:20 -05:00
e985f905f2 formatting changes 2025-07-15 21:58:20 -05:00
e1f09ca4ec fix slider 2025-07-15 21:58:20 -05:00
0c09eb38e9 Merge pull request 'Update dependency framer-motion to v12.23.5' (#37) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 41s
test-build / build (push) Successful in 24s
2025-07-16 00:03:06 +00:00
95eeb44e4f Update dependency framer-motion to v12.23.5
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 34s
2025-07-16 00:02:26 +00:00
d47d67572e Merge pull request 'Update dependency astro to v5.11.1' (#36) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Successful in 59s
test-build / build (push) Has been cancelled
2025-07-16 00:01:41 +00:00
fa4841948a Update dependency astro to v5.11.1
All checks were successful
test-build / build (pull_request) Successful in 30s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 00:00:52 +00:00
71e2b0185b update tag
Some checks failed
test-build / build (push) Successful in 28s
release-image / release (push) Successful in 3m31s
process-repository / process-repository (push) Failing after 1m7s
renovate / renovate (push) Successful in 1m38s
2025-07-15 01:43:31 -05:00
7f9fb4d2b9 fix scrollbar affecting layout
Some checks failed
renovate / renovate (push) Successful in 29s
test-build / build (push) Has been cancelled
2025-07-15 01:39:40 -05:00
8420c8dd58 fix tech stack slider 2025-07-15 01:37:23 -05:00
fa6ed18edb fix dark mode 2025-07-14 23:18:38 -05:00
30860fce1e change paths
All checks were successful
test-build / build (push) Successful in 30s
renovate / renovate (push) Successful in 21s
2025-07-14 22:31:12 -05:00
b479e0e22c use single workflow script
Some checks failed
process-repository / process-repository (push) Failing after 17s
renovate / renovate (push) Successful in 39s
test-build / build (push) Successful in 41s
2025-07-13 23:43:58 -05:00
cf01ebcd3c Merge pull request 'Update dependency eslint to v9.31.0' (#35) from renovate/eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 35s
renovate / renovate (push) Successful in 34s
process-pull-requests / process-pull-requests (push) Successful in 10s
process-issues / process-issues (push) Successful in 13s
Reviewed-on: #35
2025-07-13 03:33:05 +00:00
df8ccf81c2 Update dependency eslint to v9.31.0
All checks were successful
test-build / build (pull_request) Successful in 38s
renovate/stability-days Updates have met minimum release age requirement
2025-07-13 00:01:03 +00:00
073911c1b9 use tag ids
Some checks failed
process-pull-requests / process-pull-requests (push) Failing after 11s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 58s
test-build / build (push) Successful in 31s
2025-07-11 21:47:20 -05:00
3eeea3dd8f use tag ids
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 23s
2025-07-11 21:36:40 -05:00
43fea76778 Update dependency framer-motion to v12.23.3
All checks were successful
test-build / build (pull_request) Successful in 36s
renovate / renovate (push) Successful in 32s
test-build / build (push) Successful in 41s
renovate/stability-days Updates have met minimum release age requirement
2025-07-12 00:01:43 +00:00
d64df6473a Update dependency prettier-plugin-tailwindcss to v0.6.14
Some checks failed
test-build / build (push) Successful in 32s
process-pull-requests / process-pull-requests (push) Successful in 15s
process-issues / process-issues (push) Failing after 9s
renovate / renovate (push) Successful in 1m7s
2025-07-10 20:59:07 +00:00
63a6a00817 Update dependency framer-motion to v12.23.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 48s
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-07-10 20:57:58 +00:00
26 changed files with 661 additions and 1813 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.0-alpine3.22 AS base
LABEL version="0.8.13" LABEL version="0.10.0"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"

View File

@@ -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

View File

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.8.13", "version": "0.10.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

File diff suppressed because it is too large Load Diff

View File

@@ -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);

View File

@@ -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: 'Topics', href: '/topics' },
{ text: 'RSS', href: '/rss.xml' }, { text: 'About', href: '/about' },
]; ];
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,7 +54,6 @@ 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">
@@ -64,8 +64,9 @@ const socialLinks = [
> >
<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-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
>{global.initals}</span
> >
{global.initals}
</span>
<div <div
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100" 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"
> >
@@ -73,8 +74,9 @@ const socialLinks = [
</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>
@@ -146,8 +148,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 +176,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>

View File

@@ -20,6 +20,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 -->
@@ -121,7 +122,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');

View File

@@ -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>

View File

@@ -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', () => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -70,7 +70,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>
@@ -86,286 +85,8 @@ try {
<slot name="after-article" /> <slot name="after-article" />
</Layout> </Layout>
<script>
// Blog post SPA transitions
function setupBlogPostTransitions() {
// Animate article entrance
const article = document.querySelector('article');
if (article) {
article.classList.add('article-entering');
// Remove class after animation completes
setTimeout(() => {
article.classList.remove('article-entering');
}, 1000);
}
// Ensure consistent code block styling
function updateCodeBlockStyles() {
document.querySelectorAll('pre').forEach((pre) => {
// 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',
}
);
headings.forEach((heading) => {
heading.classList.add('heading-animated');
observer.observe(heading);
});
return observer;
};
// Initialize heading animations
const headingObserver = animateHeadings();
// Enhance code blocks with syntax highlighting and copy button
function enhanceCodeBlocks() {
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>
<style> <style>
/* Enhanced hero image styling */ /* 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 +98,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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -5,6 +5,7 @@ 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 --> <!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
@@ -48,7 +49,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 +63,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 +84,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,66 +132,6 @@ 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>

View File

@@ -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,7 +16,10 @@ 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 --> <!-- Decorative elements -->
@@ -119,13 +121,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 +169,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
@@ -273,7 +277,7 @@ const skills = await directus.request(
z-index: 10; z-index: 10;
} }
/* Reduce animation complexity on mobile for better performance */ /* Reduce animation complexity on mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
.skill-card { .skill-card {
transition: transition:
@@ -333,7 +337,7 @@ const skills = await directus.request(
} }
} }
/* Improved touch targets for mobile */ /* Touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, a,
button { button {
@@ -365,8 +369,7 @@ const skills = await directus.request(
</style> </style>
<script> <script>
// Wait for the DOM to be fully loaded document.addEventListener('astro:page-load', () => {
document.addEventListener('DOMContentLoaded', () => {
const sliderTrack = document.querySelector('.slider-track'); const sliderTrack = document.querySelector('.slider-track');
// Create seamless infinite scrolling effect // Create seamless infinite scrolling effect
@@ -374,9 +377,6 @@ const skills = await directus.request(
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 +461,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(() => {
@@ -474,104 +472,3 @@ const skills = await directus.request(
}); });
}); });
</script> </script>
<script>
// Handle SPA transitions for about 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;
}
});
});
// 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
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>

View File

@@ -116,8 +116,6 @@ const { post, nextPost, prevPost } = Astro.props;
</BlogPost> </BlogPost>
<script> <script>
// Removing TOC-related functions
// Add copy buttons to code blocks // Add copy buttons to code blocks
function initializeCodeCopyButtons() { function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre'); const codeBlocks = document.querySelectorAll('pre');
@@ -183,50 +181,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,19 +196,11 @@ 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 */
/* Language badge styling */ /* Language badge styling */
.language-badge { .language-badge {
font-family: font-family:

View File

@@ -22,19 +22,11 @@ const postsByYear = sortedPosts.reduce((acc, post) => {
}, {}); }, {});
const years = Object.keys(postsByYear).sort((a, b) => b - a); const years = Object.keys(postsByYear).sort((a, b) => b - a);
// Get total post count
const totalPosts = sortedPosts.length;
// Get unique tags for search suggestions
const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
--- ---
<BaseLayout title="Blog"> <BaseLayout title="Blog">
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16"> <div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide">
<!-- Header with search -->
<div class="relative mb-12 sm:mb-20"> <div class="relative mb-12 sm:mb-20">
<!-- Decorative elements -->
<div <div
class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30" class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
> >
@@ -124,7 +116,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
) )
} }
<!-- Improved sidebar for mobile --> <!-- Sidebar for mobile -->
<div class="relative md:col-span-3"> <div class="relative md:col-span-3">
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0"> <div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
<h3 <h3
@@ -156,7 +148,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
</div> </div>
</div> </div>
<!-- Improved post grid for mobile --> <!-- Post grid for mobile -->
<div class="md:col-span-9"> <div class="md:col-span-9">
{ {
years.map((year) => ( years.map((year) => (
@@ -168,7 +160,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
<div <div
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`} class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
> >
{postsByYear[year].map((post, index) => ( {postsByYear[year].map((post) => (
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0"> <article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
{post.image && ( {post.image && (
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56"> <div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
@@ -303,7 +295,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
overflow: hidden; overflow: hidden;
} }
/* Improved touch targets for mobile */ /* Touch targets for mobile */
@media (max-width: 640px) { @media (max-width: 640px) {
a, a,
button { button {
@@ -315,8 +307,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
</style> </style>
<script> <script>
// Script không thay đổi - giữ nguyên chức năng document.addEventListener('astro:page-load', () => {
document.addEventListener('DOMContentLoaded', () => {
const backToTopButton = document.getElementById('back-to-top'); const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) { if (backToTopButton) {
@@ -341,7 +332,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
// Check scroll position // Check scroll position
window.addEventListener('scroll', toggleBackToTopButton); window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton(); // Initial check toggleBackToTopButton();
} }
// Add smooth scrolling to year links // Add smooth scrolling to year links
@@ -382,57 +373,4 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
}); });
} }
}); });
// SPA transition handling
function setupSPATransitions() {
// Handle all blog post links for SPA transitions
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// 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;
}
});
});
// Handle year anchor links specially
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.setAttribute('data-spa-internal', 'true');
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>

View File

@@ -22,10 +22,13 @@ 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 --> <!-- Hero Section with mobile responsiveness -->
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"> <section
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
transition:animate="slide"
>
<div class="relative mx-auto max-w-2xl"> <div class="relative mx-auto max-w-2xl">
<!-- Adjusted blob positions and sizes for better mobile appearance --> <!-- Adjusted blob positions and sizes for mobile appearance -->
<div <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" 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"
> >
@@ -85,7 +88,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</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"
> >
@@ -124,7 +127,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</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) => (
@@ -215,7 +218,7 @@ 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">
@@ -278,8 +281,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 +299,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 +341,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;
@@ -477,58 +479,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
animateContent(); 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>
@@ -586,6 +536,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>

View File

@@ -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,7 +47,6 @@ 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 <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" 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"
@@ -277,7 +274,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">
@@ -423,98 +420,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 -->

View File

@@ -30,8 +30,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
--- ---
<BaseLayout title="Explore Tags"> <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"> <div
<!-- Enhanced header section with animated elements - improved for mobile --> class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16"
transition:animate="slide"
>
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16"> <div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
<div <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" 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"
@@ -146,9 +148,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
</BaseLayout> </BaseLayout>
<script> <script>
// Ultra-reliable responsiveness handling document.addEventListener('astro:page-load', () => {
document.addEventListener('DOMContentLoaded', () => {
// Fix viewport width issues on mobile
const fixViewportWidth = () => { const fixViewportWidth = () => {
// Force the viewport to be exactly the width of the device // Force the viewport to be exactly the width of the device
const viewport = document.querySelector('meta[name="viewport"]'); const viewport = document.querySelector('meta[name="viewport"]');
@@ -378,7 +378,6 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
width: 100% !important; width: 100% !important;
} }
/* Ultra-responsive breakpoints for extreme reliability */
/* Micro screens (below 240px) */ /* Micro screens (below 240px) */
@media (max-width: 239px) { @media (max-width: 239px) {
.tag-cloud { .tag-cloud {
@@ -545,7 +544,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
hyphens: auto; hyphens: auto;
} }
/* Improved shadow for dark mode */ /* Shadow for dark mode */
:global(.dark) .tag-cloud { :global(.dark) .tag-cloud {
box-shadow: box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.05),
@@ -628,87 +627,3 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
} }
} }
</style> </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>

View File

@@ -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);
} }

52
src/utils/DynamicIcon.tsx Normal file
View File

@@ -0,0 +1,52 @@
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',
size = 20,
color = 'currentColor',
className = '',
}: {
name: string;
set: string;
size: number;
color: string;
className: 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 size={size} color={color} className={className} />;
};
export default DynamicIcon;

View File

@@ -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,