upgrade to different layout
This commit is contained in:
317
src/layouts/Layout.astro
Normal file
317
src/layouts/Layout.astro
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
import Navigation from '../components/Navigation.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Background from '../components/Background.astro';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 min-h-screen flex flex-col">
|
||||
<!-- Page transition overlay - for smooth transitions between pages -->
|
||||
<div id="page-transition" class="fixed inset-0 z-40 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none transition-opacity duration-300 flex items-center justify-center">
|
||||
<div class="transition-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Background component with dot pattern and ambient glow -->
|
||||
<Background />
|
||||
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 w-full flex-grow">
|
||||
<Navigation />
|
||||
<main class="py-12">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
<script>
|
||||
// SPA transition system with history API
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pageTransition = document.getElementById('page-transition');
|
||||
const mainContent = document.querySelector('main');
|
||||
|
||||
// Initialize content with entrance animation
|
||||
if (mainContent) {
|
||||
mainContent.classList.add('content-entering');
|
||||
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>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
/* Page transition effects */
|
||||
#page-transition {
|
||||
transition: opacity 0.3s ease;
|
||||
backdrop-filter: blur(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 */
|
||||
main {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
main.content-entering {
|
||||
animation: content-fade-in 0.6s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes content-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme transition effect */
|
||||
body.theme-transitioning * {
|
||||
transition-duration: 0.3s !important;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user