- Default theme to system mode with live OS color-scheme detection - Fix custom dot-matrix sprite cursor for light/dark modes - Preserve natural color portraits across all views - Improve mobile hero layout with responsive portrait and Geist Pixel typography - Add subtle color tint to perimeter background dots while keeping reading content clear - Center certificate modal via createPortal and make verify button theme-responsive - Make settings controls (magnifier toggle, lens button, zoom levels) theme-responsive - Remove flagship badge and deep-dive technical sections from TIS-RMS case study
303 lines
11 KiB
Plaintext
303 lines
11 KiB
Plaintext
---
|
|
export const prerender = true;
|
|
import '../styles/global.css';
|
|
import { ClientRouter } from 'astro:transitions';
|
|
import SpotlightLens from '../components/SpotlightLens';
|
|
import PortfolioAssistantFAB from '../components/PortfolioAssistantFAB';
|
|
import DotMatrixCursor from '../components/DotMatrixCursor';
|
|
import SEO from '../components/SEO.astro';
|
|
|
|
|
|
interface Props {
|
|
title?: string;
|
|
description?: string;
|
|
image?: string;
|
|
preloadImage?: string;
|
|
type?: 'website' | 'article' | 'profile';
|
|
canonicalURL?: string;
|
|
}
|
|
|
|
const {
|
|
title = "Cris Charles Garcia | Full-Stack Developer Portfolio",
|
|
description = "Personal portfolio website of Cris Charles Garcia, showcasing projects, skills, experience, and custom AI chatbot assistant.",
|
|
image = "/logo.png",
|
|
preloadImage,
|
|
type = "website",
|
|
canonicalURL
|
|
} = Astro.props;
|
|
|
|
const isRootPage = Astro.url.pathname === '/';
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
<!-- Preload Critical LCP Image -->
|
|
{preloadImage && (
|
|
<link rel="preload" as="image" href={preloadImage} fetchpriority="high" />
|
|
)}
|
|
|
|
<!-- Resource Preconnect (Sanity CDN) -->
|
|
<link rel="preconnect" href="https://cdn.sanity.io" />
|
|
<link rel="dns-prefetch" href="https://cdn.sanity.io" />
|
|
<link rel="icon" type="image/png" href="/logo.png" />
|
|
<link rel="apple-touch-icon" href="/logo.png" />
|
|
<!-- PWA & Mobile Web App Manifest -->
|
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
<meta name="theme-color" content="#faf6f0" />
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<meta name="apple-mobile-web-app-title" content="Cris Charles" />
|
|
|
|
<!-- SEO & Structured Data -->
|
|
<SEO title={title} description={description} image={image} type={type} canonicalURL={canonicalURL} />
|
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
<!-- Astro ClientRouter for seamless SPA page transitions (no reload, no twitch) -->
|
|
<ClientRouter />
|
|
|
|
<!-- Theme Initialization Script (Prevents FOUC & preserves theme on Astro transitions) -->
|
|
<script is:inline>
|
|
(function() {
|
|
function getResolvedTheme() {
|
|
var storedTheme = localStorage.getItem('theme');
|
|
if (storedTheme === 'light') return 'light';
|
|
if (storedTheme === 'dark') return 'dark';
|
|
// Default: system mode
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
function applyTheme(doc) {
|
|
var targetDoc = doc || document;
|
|
var theme = getResolvedTheme();
|
|
var metaThemeColor = targetDoc.querySelector('meta[name="theme-color"]');
|
|
if (theme === 'dark') {
|
|
targetDoc.documentElement.classList.add('dark');
|
|
if (metaThemeColor) metaThemeColor.setAttribute('content', '#0c0c0f');
|
|
} else {
|
|
targetDoc.documentElement.classList.remove('dark');
|
|
if (metaThemeColor) metaThemeColor.setAttribute('content', '#ffffff');
|
|
}
|
|
}
|
|
|
|
applyTheme(document);
|
|
|
|
// Crucial for Astro ClientRouter: re-apply theme on before-swap, after-swap and page-load
|
|
document.addEventListener('astro:before-swap', function(e) {
|
|
if (e.newDocument) applyTheme(e.newDocument);
|
|
});
|
|
document.addEventListener('astro:after-swap', function() {
|
|
applyTheme(document);
|
|
});
|
|
document.addEventListener('astro:page-load', function() {
|
|
applyTheme(document);
|
|
});
|
|
|
|
// Automatically adapt if user is on system mode (no stored preference OR 'system')
|
|
try {
|
|
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
var listener = function(e) {
|
|
var stored = localStorage.getItem('theme');
|
|
if (!stored || stored === 'system') {
|
|
applyTheme(document);
|
|
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: e.matches ? 'dark' : 'light' } }));
|
|
}
|
|
};
|
|
if (darkQuery.addEventListener) {
|
|
darkQuery.addEventListener('change', listener);
|
|
} else if (darkQuery.addListener) {
|
|
darkQuery.addListener(listener);
|
|
}
|
|
} catch (err) {
|
|
// Ignore older browser issues
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!-- Performance Tier, Ultra-Fast Design & Lite Mode Detection (Persists across all page transitions) -->
|
|
<script is:inline>
|
|
(function() {
|
|
function applyTier(doc) {
|
|
var targetDoc = doc || document;
|
|
var manualLite = localStorage.getItem('liteMode');
|
|
var tier = 'mid';
|
|
if (manualLite === 'true') {
|
|
tier = 'low';
|
|
} else {
|
|
var cores = navigator.hardwareConcurrency || 4;
|
|
if (cores >= 8 && window.devicePixelRatio >= 2) {
|
|
tier = 'high';
|
|
} else {
|
|
tier = 'mid';
|
|
}
|
|
}
|
|
targetDoc.documentElement.dataset.tier = tier;
|
|
return tier;
|
|
}
|
|
|
|
function applyFontZoom(doc) {
|
|
var targetDoc = doc || document;
|
|
var savedZoom = localStorage.getItem('pageZoom');
|
|
if (savedZoom) {
|
|
var zoomVal = parseInt(savedZoom, 10);
|
|
if (!isNaN(zoomVal) && zoomVal >= 80 && zoomVal <= 130) {
|
|
targetDoc.documentElement.style.fontSize = ((zoomVal / 100) * 15) + 'px';
|
|
}
|
|
}
|
|
}
|
|
|
|
function initPreferences(doc) {
|
|
var targetDoc = doc || document;
|
|
var tier = applyTier(targetDoc);
|
|
applyFontZoom(targetDoc);
|
|
return tier;
|
|
}
|
|
|
|
initPreferences(document);
|
|
|
|
document.addEventListener('astro:before-swap', function(e) {
|
|
if (e.newDocument) {
|
|
initPreferences(e.newDocument);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('astro:after-swap', function() {
|
|
var tier = initPreferences(document);
|
|
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
|
|
});
|
|
document.addEventListener('astro:page-load', function() {
|
|
var tier = initPreferences(document);
|
|
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Service Worker Registration for PWA & Offline E-Business Card -->
|
|
<script is:inline>
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function() {
|
|
navigator.serviceWorker.register('/sw.js').catch(function(err) {
|
|
console.log('SW registration error:', err);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-background-custom text-foreground-custom min-h-screen flex flex-col selection:bg-foreground-custom selection:text-background-custom">
|
|
<!-- bryl-minimal Halftone Texture (Perimeter frame only; center masked away for reading clarity) -->
|
|
<div class="fixed inset-0 z-0 pointer-events-none select-none overflow-hidden" aria-hidden="true">
|
|
<div class="absolute inset-0 halftone-bg"></div>
|
|
</div>
|
|
|
|
<div id="magnify-content" data-vaul-drawer-wrapper class="w-full max-w-full min-h-screen flex flex-col pt-[var(--nav-height)] lg:pt-0 lg:pl-[var(--sidebar-width)] overflow-x-clip">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Back to Top Button (Zero-React Vanilla) -->
|
|
<div id="back-to-top-container" class="fixed bottom-6 left-5 sm:left-7 md:left-72 z-30 pointer-events-none opacity-0 transition-opacity duration-200">
|
|
<button
|
|
id="back-to-top-btn"
|
|
class="pointer-events-auto p-2.5 rounded-full bg-card-custom/95 hover:bg-card-custom text-foreground-custom border border-border-custom shadow-md hover:border-primary-custom/40 active:scale-95 transition-all duration-150 cursor-pointer group"
|
|
aria-label="Back to top"
|
|
>
|
|
<svg class="w-4 h-4 text-foreground-custom stroke-[2] group-hover:-translate-y-0.5 transition-transform duration-150" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Offline / Online Toast Banner (Zero-React Vanilla) -->
|
|
<div
|
|
id="network-status-toast"
|
|
role="status"
|
|
aria-live="polite"
|
|
class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-full border border-border-custom shadow-lg backdrop-blur-md items-center gap-2.5 text-xs font-mono font-medium select-none bg-card-custom/95 text-foreground-custom pointer-events-none"
|
|
>
|
|
<span id="network-status-dot" class="w-2 h-2 rounded-full shrink-0 bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
|
|
<span id="network-status-text">ONLINE • Live Updates Synced</span>
|
|
</div>
|
|
|
|
<PortfolioAssistantFAB client:idle transition:persist="portfolio-assistant" />
|
|
<SpotlightLens client:media="(hover: hover) and (pointer: fine)" transition:persist="spotlight-lens" />
|
|
<DotMatrixCursor client:media="(hover: hover) and (pointer: fine)" transition:persist="dm-cursor" />
|
|
|
|
<!-- Vanilla Scripts for BackToTop and NetworkStatus -->
|
|
<script is:inline>
|
|
(function() {
|
|
// 1. Back to Top
|
|
var btnContainer = document.getElementById('back-to-top-container');
|
|
var btn = document.getElementById('back-to-top-btn');
|
|
if (btnContainer && btn) {
|
|
var ticking = false;
|
|
function onScroll() {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(function() {
|
|
if (window.scrollY > 400) {
|
|
btnContainer.classList.remove('opacity-0', 'pointer-events-none');
|
|
} else {
|
|
btnContainer.classList.add('opacity-0', 'pointer-events-none');
|
|
}
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
}
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
btn.addEventListener('click', function() {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
}
|
|
|
|
// 2. Network Status
|
|
var toast = document.getElementById('network-status-toast');
|
|
var dot = document.getElementById('network-status-dot');
|
|
var text = document.getElementById('network-status-text');
|
|
if (toast && dot && text) {
|
|
var hideTimer;
|
|
function showStatus(isOnline) {
|
|
clearTimeout(hideTimer);
|
|
toast.classList.remove('hidden');
|
|
toast.classList.add('flex');
|
|
if (isOnline) {
|
|
dot.className = 'w-2 h-2 rounded-full shrink-0 bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]';
|
|
text.textContent = 'ONLINE • Live Updates Synced';
|
|
text.classList.remove('text-amber-500');
|
|
hideTimer = setTimeout(function() {
|
|
toast.classList.add('hidden');
|
|
toast.classList.remove('flex');
|
|
}, 3500);
|
|
} else {
|
|
dot.className = 'w-2 h-2 rounded-full shrink-0 bg-amber-500 animate-pulse';
|
|
text.textContent = 'OFFLINE • Cached Portfolio Mode';
|
|
text.classList.add('text-amber-500');
|
|
}
|
|
}
|
|
window.addEventListener('online', function() { showStatus(true); });
|
|
window.addEventListener('offline', function() { showStatus(false); });
|
|
if (!navigator.onLine) showStatus(false);
|
|
}
|
|
|
|
// 3. Global Visitor Geolocation Telemetry (Option A)
|
|
try {
|
|
if (!sessionStorage.getItem('geo_logged')) {
|
|
var pingGeo = function() {
|
|
fetch('/api/geo', { priority: 'low' })
|
|
.then(function() {
|
|
sessionStorage.setItem('geo_logged', '1');
|
|
})
|
|
.catch(function() {});
|
|
};
|
|
if ('requestIdleCallback' in window) {
|
|
requestIdleCallback(pingGeo, { timeout: 3000 });
|
|
} else {
|
|
setTimeout(pingGeo, 1500);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|