2025-05-03 15:31:48 -07:00
<!DOCTYPE html> < html lang = "en" > < head > <!-- Theme flash prevention script - must be first in head --> < script >
// Immediately apply the saved theme to prevent flash
(function() {
function getThemePreference() {
if (typeof localStorage !== 'undefined' & & localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const theme = getThemePreference();
// Apply theme immediately to prevent flash
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Store the theme in localStorage for future visits
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', theme);
}
// Add a class to indicate JS is loaded and theme is applied
document.documentElement.classList.add('theme-loaded');
})();
< / script > <!-- Service worker registration --> < script >
// Register service worker for offline support and caching
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('SW registered: ', registration.scope);
})
.catch(error => {
console.log('SW registration failed: ', error);
});
});
}
< / script > < meta charset = "UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1" > < meta name = "generator" content = "Astro v5.7.5" > <!-- Favicons --> < link rel = "icon" type = "image/svg+xml" href = "/favicons/favicon.png" > < link rel = "apple-touch-icon" sizes = "180x180" href = "/favicons/apple-touch-icon.png" > < link rel = "icon" type = "image/png" sizes = "32x32" href = "/favicons/favicon-32x32.png" > < link rel = "icon" type = "image/png" sizes = "16x16" href = "/favicons/favicon-16x16.png" > < link rel = "manifest" href = "/site.webmanifest" > <!-- Theme colors --> < meta name = "theme-color" content = "#282828" media = "(prefers-color-scheme: dark)" > < meta name = "theme-color" content = "#ebdbb2" media = "(prefers-color-scheme: light)" > <!-- Preconnect to external resources --> < link rel = "preconnect" href = "https://fonts.bunny.net" crossorigin > < link rel = "dns-prefetch" href = "https://fonts.bunny.net" > < link rel = "preconnect" href = "https://cdn.jsdelivr.net" crossorigin > < link rel = "dns-prefetch" href = "https://cdn.jsdelivr.net" > < link rel = "preconnect" href = "https://unpkg.com" crossorigin > < link rel = "dns-prefetch" href = "https://unpkg.com" > <!-- Preload critical fonts with font - display: swap --> < link rel = "preload" href = "https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i&display=swap" as = "style" > < link href = "https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i&display=swap" rel = "stylesheet" > <!-- Preload critical font files --> < link rel = "preload" href = "https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff2" as = "font" type = "font/woff2" crossorigin > <!-- Preload Alpine.js --> < link rel = "preload" href = "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" as = "script" > < script src = "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer > < / script > <!-- Font loading observer --> < script >
if ("fonts" in document) {
Promise.all([
document.fonts.load("1em IBM Plex Mono"),
document.fonts.load("1em press-start-2p")
]).then(() => {
document.documentElement.classList.add("fonts-loaded");
});
} else {
document.documentElement.classList.add("fonts-loaded");
}
< / script > <!-- Time - based theme scheduler --> < script >
// Time-based theme switching
(function() {
// Only run if user has selected auto mode
const themeMode = localStorage.getItem('theme-mode');
if (themeMode === 'auto') {
const checkTime = () => {
const hour = new Date().getHours();
const isDayTime = hour >= 7 & & hour < 19 ; / / 7 AM to 7 PM
const theme = isDayTime ? 'light' : 'dark';
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
if (theme !== currentTheme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme', theme);
// Dispatch theme changed event
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: { theme, automatic: true, mode: 'auto' }
}));
}
};
// Check immediately
checkTime();
// Check every hour
setInterval(checkTime, 60 * 60 * 1000);
// Also check at specific times (7 AM and 7 PM)
const scheduleCheck = () => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
// Calculate time until next check (either 7 AM or 7 PM)
let nextCheckHour;
if (hours < 7 ) {
nextCheckHour = 7; // Next check at 7 AM
} else if (hours < 19 ) {
nextCheckHour = 19; // Next check at 7 PM
} else {
nextCheckHour = 7; // Next check at 7 AM tomorrow
}
// Calculate milliseconds until next check
let msUntilNextCheck;
if (hours >= 19) {
// After 7 PM, next check is 7 AM tomorrow
msUntilNextCheck = ((24 - hours + 7) * 60 - minutes) * 60 * 1000;
} else {
// Before 7 PM, next check is either 7 AM or 7 PM today
msUntilNextCheck = ((nextCheckHour - hours) * 60 - minutes) * 60 * 1000;
}
// Schedule the next check
setTimeout(() => {
checkTime();
scheduleCheck(); // Schedule the next check after this one
}, msUntilNextCheck);
};
// Start the scheduling
scheduleCheck();
2025-04-27 22:15:19 -07:00
}
})();
2025-05-03 15:31:48 -07:00
< / script > <!-- Theme transition script --> < script type = "module" > d o c u m e n t . a d d E v e n t L i s t e n e r ( " D O M C o n t e n t L o a d e d " , ( ) = > { w i n d o w . a d d E v e n t L i s t e n e r ( " t h e m e - c h a n g e d " , t = > { c o n s t a = ( t i n s t a n c e o f C u s t o m E v e n t ? t : n u l l ) ? . d e t a i l ? . t h e m e | | ( d o c u m e n t . d o c u m e n t E l e m e n t . c l a s s L i s t . c o n t a i n s ( " d a r k " ) ? " d a r k " : " l i g h t " ) ; w i n d o w . m a t c h M e d i a ( " ( p r e f e r s - r e d u c e d - m o t i o n : r e d u c e ) " ) . m a t c h e s | | ( d o c u m e n t . q u e r y S e l e c t o r A l l ( " h 1 , h 2 , h 3 " ) . f o r E a c h ( ( e , o ) = > { e . c l a s s L i s t . r e m o v e ( " t h e m e - a n i m a t e - s l i d e " ) , e . o f f s e t W i d t h , s e t T i m e o u t ( ( ) = > { e . c l a s s L i s t . a d d ( " t h e m e - a n i m a t e - s l i d e " ) } , 5 0 + o * 3 0 ) } ) , d o c u m e n t . q u e r y S e l e c t o r A l l ( " . c a r d , a r t i c l e , s e c t i o n : n o t ( s e c t i o n s e c t i o n ) " ) . f o r E a c h ( ( e , o ) = > { e . c l a s s L i s t . r e m o v e ( " t h e m e - a n i m a t e - s c a l e " ) , e . o f f s e t W i d t h , s e t T i m e o u t ( ( ) = > { e . c l a s s L i s t . a d d ( " t h e m e - a n i m a t e - s c a l e " ) } , 1 0 0 + o * 4 0 ) } ) , d o c u m e n t . q u e r y S e l e c t o r A l l ( " i m g , s v g " ) . f o r E a c h ( ( e , o ) = > { e . c l a s s L i s t . r e m o v e ( " t h e m e - a n i m a t e - f a d e " ) , e . o f f s e t W i d t h , s e t T i m e o u t ( ( ) = > { e . c l a s s L i s t . a d d ( " t h e m e - a n i m a t e - f a d e " ) } , 1 5 0 + o * 2 0 ) } ) , a = = = " d a r k " ? ( d o c u m e n t . q u e r y S e l e c t o r A l l ( " c o d e , p r e " ) . f o r E a c h ( e = > { e . c l a s s L i s t . a d d ( " d a r k - t h e m e - c o d e " ) } ) , d o c u m e n t . q u e r y S e l e c t o r A l l ( " . b u t t o n - p r i m a r y , . c t a - b u t t o n " ) . f o r E a c h ( e = > { e . c l a s s L i s t . a d d ( " d a r k - t h e m e - g l o w " ) } ) ) : ( d o c u m e n t . q u e r y S e l e c t o r A l l ( " c o d e , p r e " ) . f o r E a c h ( e = > { e . c l a s s L i s t . r e m o v e ( " d a r k - t h e m e - c o d e " ) } ) , d o c u m e n t . q u e r y S e l e c t o r A l l ( " . b u t t o n - p r i m a r y , . c t a - b u t t o n " ) . f o r E a c h ( e = > { e . c l a s s L i s t . r e m o v e ( " d a r k - t h e m e - g l o w " ) } ) ) ) } ) } ) ; c o n s t d = ( ) = > { c o n s t t = d o c u m e n t . c r e a t e E l e m e n t ( " s t y l e " ) ; t . t e x t C o n t e n t = `
.dark-theme-code {
box-shadow: 0 0 8px rgba(254, 128, 25, 0.3);
}
.dark-theme-glow {
box-shadow: 0 0 12px rgba(254, 128, 25, 0.4);
}
@media (prefers-reduced-motion: reduce) {
.theme-animate-fade,
.theme-animate-slide,
.theme-animate-scale {
animation: none !important;
transition: none !important;
}
}
`,document.head.appendChild(t)};document.addEventListener("DOMContentLoaded",d);< / script > < script >
/**
* Initialize search functionality for any content type
* @returns {Object} Alpine.js data object with search functionality
*/
function initializeSearch(contentSelector = '.searchable-item', options = {}) {
const defaults = {
nameAttribute: 'data-name',
tagsAttribute: 'data-tags',
categoryAttribute: 'data-category',
additionalAttributes: [],
noResultsMessage: 'No results found',
allItemsMessage: 'Showing all items',
resultCountMessage: (count) => `Found ${count} items`,
itemLabel: 'items'
};
2025-04-27 22:15:19 -07:00
2025-05-03 15:31:48 -07:00
const config = { ...defaults, ...options };
2025-04-27 22:15:19 -07:00
2025-05-03 15:31:48 -07:00
return {
searchQuery: '',
hasResults: true,
visibleCount: 0,
loading: false, // Start with loading state false - the LoadingManager will control this
init() {
// Initialize the visible count
this.visibleCount = document.querySelectorAll(contentSelector).length;
this.setupWatchers();
this.setupKeyboardShortcuts();
// Handle theme changes
window.addEventListener('theme-changed', () => {
this.filterContent(this.searchQuery);
});
},
setupWatchers() {
this.$watch('searchQuery', (query) => {
// Filter content immediately - no artificial delay
this.filterContent(query);
});
},
setupKeyboardShortcuts() {
// Track the currently focused item index
this.focusedItemIndex = -1;
document.addEventListener('keydown', (e) => {
// '/' key focuses the search input
if (e.key === '/' & & document.activeElement.id !== 'app-search') {
e.preventDefault();
document.getElementById('app-search').focus();
}
// Escape key clears the search
if (e.key === 'Escape' & & this.searchQuery !== '') {
this.searchQuery = '';
document.getElementById('app-search').focus();
this.focusedItemIndex = -1;
this.clearItemFocus();
}
// Arrow key navigation through results
if (this.searchQuery & & (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
e.preventDefault();
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
// Update focused item index
if (e.key === 'ArrowDown') {
this.focusedItemIndex = Math.min(this.focusedItemIndex + 1, visibleItems.length - 1);
} else {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, -1);
}
// Clear previous focus
this.clearItemFocus();
// If we're back at -1, focus the search input
if (this.focusedItemIndex === -1) {
document.getElementById('app-search').focus();
return;
}
// Focus the new item
const itemToFocus = visibleItems[this.focusedItemIndex];
this.focusItem(itemToFocus);
}
// Enter key selects the focused item
if (e.key === 'Enter' & & this.focusedItemIndex >= 0) {
const visibleItems = this.getVisibleItems();
if (visibleItems.length === 0) return;
const selectedItem = visibleItems[this.focusedItemIndex];
const link = selectedItem.querySelector('a');
if (link) {
link.click();
}
}
});
},
getVisibleItems() {
return Array.from(document.querySelectorAll(contentSelector))
.filter(item => item.style.display !== 'none');
},
clearItemFocus() {
// Remove focus styling from all items
document.querySelectorAll(`${contentSelector}.keyboard-focus`).forEach(item => {
item.classList.remove('keyboard-focus');
});
},
focusItem(item) {
// Add focus styling
item.classList.add('keyboard-focus');
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
},
filterContent(query) {
query = query.toLowerCase();
let anyResults = false;
let visibleCount = 0;
// Process all content items
document.querySelectorAll(contentSelector).forEach((item) => {
// Get searchable attributes
const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
// Check additional attributes if specified
const additionalMatches = config.additionalAttributes.some(attr => {
const value = (item.getAttribute(attr) || '').toLowerCase();
return value.includes(query);
});
const isMatch = query === '' ||
name.includes(query) ||
tags.includes(query) ||
category.includes(query) ||
additionalMatches;
if (isMatch) {
item.style.display = '';
anyResults = true;
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update category visibility for homelab page
this.updateCategoryVisibility(query);
// Update parent containers if needed
this.updateContainerVisibility(query);
this.updateResultsStatus(query, anyResults, visibleCount);
},
updateCategoryVisibility(query) {
// Only proceed if we have category sections (homelab page)
const categorySections = document.querySelectorAll('.category-section');
if (categorySections.length === 0) return;
// For each category section, check if it has any visible app cards
categorySections.forEach((categorySection) => {
const categoryId = categorySection.getAttribute('data-category');
const appCards = categorySection.querySelectorAll('.app-card');
// Count visible app cards in this category
const visibleApps = Array.from(appCards).filter(card =>
card.style.display !== 'none'
).length;
// If no visible apps and we're searching, hide the category
if (query !== '' & & visibleApps === 0) {
categorySection.style.display = 'none';
} else {
categorySection.style.display = '';
}
});
},
updateContainerVisibility(query) {
// If there are container elements that should be hidden when empty
const containers = document.querySelectorAll('.content-container');
if (containers.length > 0) {
containers.forEach((container) => {
const hasVisibleItems = Array.from(
container.querySelectorAll(contentSelector)
).some((item) => item.style.display !== 'none');
if (query === '' || hasVisibleItems) {
container.style.display = '';
} else {
container.style.display = 'none';
}
});
}
},
updateResultsStatus(query, anyResults, count) {
// Update results status
this.hasResults = query === '' || anyResults;
this.visibleCount = count;
// Update screen reader status
const statusEl = document.getElementById('search-status');
if (statusEl) {
if (query === '') {
statusEl.textContent = config.allItemsMessage;
this.visibleCount = document.querySelectorAll(contentSelector).length;
} else if (this.hasResults) {
statusEl.textContent = config.resultCountMessage(count);
} else {
statusEl.textContent = config.noResultsMessage;
}
}
}
2025-04-27 22:15:19 -07:00
};
2025-05-03 15:31:48 -07:00
}
2025-04-27 22:15:19 -07:00
2025-05-03 15:31:48 -07:00
// Register Alpine.js data components when Alpine is loaded
document.addEventListener('alpine:init', () => {
// Homelab search
window.Alpine.data('searchServices', () => {
const baseSearch = initializeSearch('.app-card', {
nameAttribute: 'data-app-name',
tagsAttribute: 'data-app-tags',
categoryAttribute: 'data-app-category',
noResultsMessage: 'No services found',
allItemsMessage: 'Showing all services',
resultCountMessage: (count) => `Found ${count} services`,
itemLabel: 'services'
});
// Add icon size slider functionality
return {
...baseSearch,
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
iconSize: 'medium', // small, medium, large
viewMode: 'grid', // grid or list
displayMode: 'both', // both, image, or name
debounceTimeout: null, // For debouncing slider changes
init() {
baseSearch.init.call(this);
// Apply initial icon size, view mode, and display mode
this.applyIconSize();
this.applyViewMode();
this.applyDisplayMode();
},
// Icon size methods
setIconSize(size) {
if (typeof size === 'string') {
// Handle legacy string values (small, medium, large)
this.iconSize = size;
this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3;
} else {
// Handle slider numeric values
this.iconSizeValue = parseFloat(size);
// Map slider value to size name
if (this.iconSizeValue < = 1.33) {
this.iconSize = 'small';
} else if (this.iconSizeValue < = 2.33) {
this.iconSize = 'medium';
} else {
this.iconSize = 'large';
}
}
this.applyIconSize();
},
// Handle slider input with debounce
handleSliderChange(event) {
const value = event.target.value;
// Clear any existing timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
// Set a new timeout
this.debounceTimeout = setTimeout(() => {
this.setIconSize(value);
}, 50); // 50ms debounce
},
applyIconSize() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing size classes
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
// Add the new size class
appList.classList.add(`icon-size-${this.iconSize}`);
// Apply custom CSS variable for fine-grained control
appList.style.setProperty('--icon-scale', this.iconSizeValue);
},
// View mode methods
toggleViewMode() {
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
this.applyViewMode();
},
setViewMode(mode) {
this.viewMode = mode;
this.applyViewMode();
},
applyViewMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing view mode classes
appList.classList.remove('view-mode-grid', 'view-mode-list');
// Add the new view mode class
appList.classList.add(`view-mode-${this.viewMode}`);
// Update all category sections
document.querySelectorAll('.category-section').forEach(section => {
const gridContainer = section.querySelector('.grid');
if (gridContainer) {
// Update grid classes based on view mode
if (this.viewMode === 'grid') {
gridContainer.classList.remove('grid-cols-1');
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
} else {
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
gridContainer.classList.add('grid-cols-1');
}
}
});
},
// Display mode methods
setDisplayMode(mode) {
this.displayMode = mode;
this.applyDisplayMode();
},
applyDisplayMode() {
const appList = document.getElementById('app-list');
if (!appList) return;
// Remove existing display mode classes
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
// Add the new display mode class
if (this.displayMode === 'image') {
appList.classList.add('display-image-only');
} else if (this.displayMode === 'name') {
appList.classList.add('display-name-only');
} else {
appList.classList.add('display-both');
}
// Update all category sections
document.querySelectorAll('.category-section').forEach(section => {
const gridContainer = section.querySelector('.grid');
if (gridContainer) {
// Update grid classes based on view mode
if (this.viewMode === 'grid') {
gridContainer.classList.remove('grid-cols-1');
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
} else {
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
gridContainer.classList.add('grid-cols-1');
}
}
});
}
};
});
// Blog search
window.Alpine.data('searchArticles', () => {
return initializeSearch('.article-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description'],
noResultsMessage: 'No articles found',
allItemsMessage: 'Showing all articles',
resultCountMessage: (count) => `Found ${count} articles`,
itemLabel: 'articles'
});
});
// Projects search
window.Alpine.data('searchProjects', () => {
return initializeSearch('.project-item', {
nameAttribute: 'data-title',
tagsAttribute: 'data-tags',
additionalAttributes: ['data-description', 'data-github', 'data-live'],
noResultsMessage: 'No projects found',
allItemsMessage: 'Showing all projects',
resultCountMessage: (count) => `Found ${count} projects`,
itemLabel: 'projects'
});
});
});
< / script > < link rel = "stylesheet" href = "/_astro/jellyfin-at-home.z5IOlzDh.css" > < / head > < body class = "zag-bg zag-text zag-transition font-mono" > <!-- Loading overlay for long loading times --> < div class = "loading-overlay fixed inset-0 flex flex-col items-center justify-center z-50 bg-opacity-90 zag-bg " role = "alert" aria-live = "polite" data-astro-cid-veun55td > < div class = "loading-content text-center p-8 rounded-lg" data-astro-cid-veun55td > <!-- Enhanced loading indicator --> < div class = "mb-6 flex justify-center" data-astro-cid-veun55td > < div class = "loading-dots w-12 h-12" style = "--dots-color:var(--color-zag-accent-dark)" data-astro-cid-ypbmo55r > < div class = "dot" data-astro-cid-ypbmo55r > < / div > < div class = "dot" data-astro-cid-ypbmo55r > < / div > < div class = "dot" data-astro-cid-ypbmo55r > < / div > < / div > < / div > <!-- Messages with animation --> < div class = "messages-container" data-astro-cid-veun55td > < p class = "text-xl font-semibold mb-2 zag-text message-animate" data-astro-cid-veun55td > This is taking longer than expected...< / p > < p class = "text-base zag-text-muted message-animate-delay" data-astro-cid-veun55td > Still working on loading your content< / p > < / div > < / div > < / div > < script type = "module" > class e { overlay = null ; timeoutId = null ; longLoadingTimeoutId = null ; constructor ( ) { this . overlay = document . querySelector ( ".loading-overlay" ) , this . setupNavigationListeners ( ) } setupNavigationListeners ( ) { "navigation" in window ? ( window . navigation . addEventListener ( "navigate" , i => { new URL ( i . destination . url ) . origin === window . location . origin && this . showLoading ( ) } ) , window . navigation . addEventListener ( "navigatesuccess" , ( ) => { this . hideLoading ( ) } ) , window . navigation . addEventListener ( "navigateerror" , ( ) => { this . hideLoading ( ) } ) ) : ( window . addEventListener ( "beforeunload" , ( ) => { this . showLoading ( ) } ) , document . addEventListener ( "astro:page-load" , ( ) => { this . hideLoading ( ) } ) ) , document . addEventListener ( "astro:before-swap" , ( ) => { this . showLoading ( ) } ) , document . addEventListener ( "astro:after-swap" , ( ) => { this . hideLoading ( ) } ) } showLoading ( ) { this . timeoutId && ( clearTimeout ( this . timeoutId ) , this . timeoutId = null ) , this . longLoadingTimeoutId && ( clearTimeout ( this . longLoadingTimeoutId ) , this . longLoadingTimeoutId = null ) ; const i = this . getConnectionBasedDelay ( ) ; this . timeoutId = setTimeout ( ( ) => { document . querySelectorAll ( "[x-data]" ) . forEach ( t => { t . _ _x && typeof t . _ _x . $data . loading < "u" && ( t . _ _x . $data . loading = ! 0 ) } ) } , i ) , this . longLoadingTimeoutId = setTimeout ( ( ) => { this . overlay && this . overlay . classList . add ( "visible" ) } , 5e3 ) } hideLoading ( ) { this . timeoutId && ( clearTimeout ( this . timeoutId ) , this . timeoutId = null ) , this . longLoadingTimeoutId && ( clearTimeout ( this . longLoadingTimeoutId ) , this . longLoadingTimeoutId = null ) , this . overlay && this . overlay . classList . remove ( "visible" ) , setTimeout ( ( ) => { document . querySelectorAll ( "[x-data]" ) . forEach ( i => { i . _ _x && typeof i . _ _x . $data . loading < "u" && ( i . _ _x . $data . loading = ! 1 ) } ) } , 100 ) } getConnectionBasedDelay ( ) { const i = navigator . connection || navigator . mozConnection || navigator . webkitConnection ; if ( i ) switch ( i . effectiveType ) { case "4g" : return 100 ; case "3g" : return 200 ; case "2g" : case "slow-2g" : return 0 ; default : return 100 } return 100 } } document . addEventListener ( "DOMContentLoaded" , ( ) => { new e } ) ; < / script > <!-- Theme - specific background patterns and transition effect --> < div class = "theme-background" aria-hidden = "true" data-astro-cid-nzjwcpgp > < div class = "light-pattern" data-astro-cid-nzjwcpgp > < / div > < div class = "dark-pattern" data-astro-cid-nzjwcpgp > < / div > < / div > < div id = "themeTransitionOverlay" class = "theme-transition-overlay" aria-hidden = "true" data-astro-cid-dewpavae > < / div > < script type = "module" > d o c u m e n t . a d d E v e n t L i s t e n e r ( " D O M C o n t e n t L o a d e d " , ( ) = > { c o n s t t = d o c u m e n t . g e t E l e m e n t B y I d ( " t h e m e T r a n s i t i o n O v e r l a y " ) ; i f ( ! t ) r e t u r n ; l e t o = d o c u m e n t . d o c u m e n t E l e m e n t . c l a s s L i s t . c o n t a i n s ( " d a r k " ) ? " d a r k " : " l i g h t " ; w i n d o w . a d d E v e n t L i s t e n e r ( " t h e m e - c h a n g e d " , a = > { c o n s t s = a . d e t a i l . t h e m e ; i f ( s = = = o ) r e t u r n ; c o n s t n = d o c u m e n t . g e t E l e m e n t B y I d ( " t h e m e T o g g l e " ) ; l e t i = " 5 0 % " , d = " 5 0 % " ; i f ( n ) { c o n s t e = n . g e t B o u n d i n g C l i e n t R e c t ( ) ; i = ` $ { e . l e f t + e . w i d t h / 2 } p x ` , d = ` $ { e . t o p + e . h e i g h t / 2 } p x ` } t . s t y l e . s e t P r o p e r t y ( " - - x " , i ) , t . s t y l e . s e t P r o p e r t y ( " - - y " , d ) , s = = = " d a r k " ? ( t . c l a s s L i s t . a d d ( " l i g h t - t o - d a r k " ) , t . c l a s s L i s t . r e m o v e ( " d a r k - t o -
2025-04-27 22:15:19 -07:00
Well, this is awkward
< / h1 > < h2 class = "text-xl sm:text-2xl pb-8" > 404 - Page not found< / h2 > < p >
It seems that this page does not exist. If you want to return to safety, < a class = "underline underline-offset-4 font-medium" href = "/" > click here to go home< / a > .
2025-05-03 15:31:48 -07:00
< / p > < / section > < / main > < footer class = "mt-16 mb-8" > < section class = "p-4 max-w-2xl mx-auto mb-4" > < div class = "flex flex-col gap-6" > <!-- Social icons row - now above the links --> < div class = "flex justify-center gap-4" > < a href = "https://github.com/justindeal" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" aria-label = "GitHub Profile" > < svg xmlns = "http://www.w3.org/2000/svg" width = "32" height = "32" viewBox = "0 0 24 24" > < path class = "zag-fill zag-transition" d = "M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" > < / path > < / svg > < / a > < a href = "https://code.justin.deal/dealjus" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" aria-label = "Gitea Profile" > < svg xmlns = "http://www.w3.org/2000/svg" width = "32" height = "32" viewBox = "0 0 24 24" > < path class = "zag-fill zag-transition" d = "M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" > < / path > < / svg > < / a > < a href = "https://www.linkedin.com/in/justin-deal/" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" aria-label = "LinkedIn Profile" > < svg xmlns = "http://www.w3.org/2000/svg" width = "32" height = "32" viewBox = "0 0 24 24" > < path class = "zag-fill zag-transition" fill = "currentColor" d = "M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11.12 19h-3.08v-9h3.08v9zm-1.54-10.29c-.99 0-1.79-.8-1.79-1.79s.8-1.79 1.79-1.79 1.79.8 1.79 1.79-.8 1.79-1.79 1.79zm13.16 10.29h-3.08v-4.89c0-1.16-.02-2.64-1.61-2.64s-1.86 1.26-1.86 2.57v4.96h-3.08v-9h2.96v1.23h.04c.41-.78 1.4-1.6 2.88-1.6 3.08 0 3.65 2.03 3.65 4.66v4.71z" > < / path > < / svg > < / a > < / div > <!-- Navigation links row - now below the icons --> < div class = "zag-border-b zag-transition pb-4" > < ul class = "flex flex-wrap justify-center gap-4 sm:gap-6" > < li > < a href = "/" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > home < / a > < / li > < li > < a href = "/about" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > about < / a > < / li > < li > < a href = "/blog" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > blog < / a > < / li > < li > < a href = "/projects" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > projects < / a > < / li > < li > < a href = "/homelab" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > homelab < / a > < / li > < li > < a href = "https://code.justin.deal" class = "zag-link zag-interactive font-medium flex items-center" target = "_self" > code < / a > < / li > < / ul > < / div > < / div > < / section > < p class = "zag-text zag-transition text-center text-sm font-medium" >
2025-04-27 22:15:19 -07:00
© 2025 Justin Deal. All rights reserved.
< / p > < / footer > < / body > < / html >