Compare commits

...

11 Commits

Author SHA1 Message Date
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
22 changed files with 471 additions and 1655 deletions

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.9.0" 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

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.9.0", "version": "0.10.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",

646
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,6 +1,6 @@
--- ---
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from '../layouts/BaseLayout.astro';
import DynamicIcon from '../components/DynamicIcon.tsx'; import DynamicIcon from '../utils/DynamicIcon.tsx';
import directus from '../../lib/directus'; import directus from '../../lib/directus';
import { readSingleton, readItems } from '@directus/sdk'; import { readSingleton, readItems } from '@directus/sdk';
@@ -16,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 -->
@@ -106,7 +109,7 @@ const skills = await directus.request(
</div> </div>
</div> </div>
<!-- Skills Section - Improved for mobile --> <!-- Skills Section -->
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24"> <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<h2 <h2
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 sm:mb-12 sm:text-3xl dark:text-zinc-100" class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 sm:mb-12 sm:text-3xl dark:text-zinc-100"
@@ -118,8 +121,11 @@ 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, ...skills, ...skills].map((skill) => ( [...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">
@@ -163,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
@@ -272,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:
@@ -332,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 {
@@ -364,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
@@ -373,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) {
@@ -460,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(() => {
@@ -473,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,4 +1,3 @@
/* Remove all the complex mobile menu styles and keep only what's necessary */
@import 'tailwindcss'; @import 'tailwindcss';
/* Dark mode support for Tailwind CSS v4 */ /* Dark mode support for Tailwind CSS v4 */
@@ -43,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];
@@ -136,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);
} }

View File

@@ -4,11 +4,13 @@ import * as MdIcons from 'react-icons/md';
import * as AiIcons from 'react-icons/ai'; import * as AiIcons from 'react-icons/ai';
import * as GiIcons from 'react-icons/gi'; import * as GiIcons from 'react-icons/gi';
import * as IoIcons from 'react-icons/io'; import * as IoIcons from 'react-icons/io';
import * as CiIcons from "react-icons/ci"; import * as CiIcons from 'react-icons/ci';
import * as FiIcons from "react-icons/fi"; import * as FiIcons from 'react-icons/fi';
import * as LuIcons from "react-icons/lu"; import * as LuIcons from 'react-icons/lu';
import * as SiIcons from 'react-icons/si'; import * as SiIcons from 'react-icons/si';
// Load React Icon library dynamically from attributes in Directus
const iconSets = { const iconSets = {
fa: FaIcons, fa: FaIcons,
md: MdIcons, md: MdIcons,
@@ -21,13 +23,25 @@ const iconSets = {
si: SiIcons, si: SiIcons,
}; };
const DynamicIcon = ({ name, set = 'fa', size = 20, color = 'currentColor', className = '' }: {name: string, set: string, size: number, color: string, className: string }) => { const DynamicIcon = ({
name,
set = 'fa',
size = 20,
color = 'currentColor',
className = '',
}: {
name: string;
set: string;
size: number;
color: string;
className: string;
}) => {
let IconComponent = FaIcons.FaAlignCenter; let IconComponent = FaIcons.FaAlignCenter;
if (name.startsWith("Fa")) { if (name.startsWith('Fa')) {
IconComponent = iconSets["fa"][name] IconComponent = iconSets['fa'][name];
} else if (name.startsWith("Si")) { } else if (name.startsWith('Si')) {
IconComponent = iconSets["si"][name] IconComponent = iconSets['si'][name];
} else { } else {
IconComponent = iconSets[set][name]; IconComponent = iconSets[set][name];
} }