2025-05-03 14:20:57 -07:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2025-05-03 18:22:25 -07:00
< meta name = "description" content = "You are currently offline. Some pages may still be available if you've visited them before." >
2025-05-03 14:20:57 -07:00
< title > Offline | Justin Deal< / title >
2025-05-03 18:22:25 -07:00
< link rel = "icon" href = "/favicons/favicon.png" type = "image/png" >
< link rel = "apple-touch-icon" href = "/favicons/apple-touch-icon.png" >
2025-05-03 14:20:57 -07:00
< style >
:root {
--color-bg: #fbf1c7;
--color-text: #3c3836;
2025-05-03 18:22:25 -07:00
--color-accent: #fe8019;
--color-accent-secondary: #b8bb26;
2025-05-03 14:20:57 -07:00
--color-muted: #7c6f64;
2025-05-03 18:22:25 -07:00
--color-card: rgba(235, 219, 178, 0.8);
--color-card-hover: rgba(235, 219, 178, 1);
--font-mono: 'IBM Plex Mono', monospace, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-display: 'Press Start 2P', monospace, system-ui;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
2025-05-03 14:20:57 -07:00
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #282828;
--color-text: #ebdbb2;
--color-accent: #fe8019;
2025-05-03 18:22:25 -07:00
--color-accent-secondary: #b8bb26;
2025-05-03 14:20:57 -07:00
--color-muted: #a89984;
2025-05-03 18:22:25 -07:00
--color-card: rgba(40, 40, 40, 0.8);
--color-card-hover: rgba(40, 40, 40, 1);
2025-05-03 14:20:57 -07:00
}
}
2025-05-03 18:22:25 -07:00
/* Base styles */
2025-05-03 14:20:57 -07:00
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-mono);
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 2rem;
2025-05-03 18:22:25 -07:00
transition: background-color 0.3s ease, color 0.3s ease;
2025-05-03 14:20:57 -07:00
}
main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-family: var(--font-display);
2025-05-03 18:22:25 -07:00
font-size: clamp(1.5rem, 5vw, 2.5rem);
margin-bottom: 1rem;
color: var(--color-accent);
line-height: 1.3;
}
h2 {
font-size: clamp(1.1rem, 3vw, 1.5rem);
2025-05-03 14:20:57 -07:00
margin-bottom: 1rem;
color: var(--color-accent);
}
p {
margin-bottom: 1.5rem;
2025-05-03 18:22:25 -07:00
font-size: clamp(0.9rem, 2vw, 1.1rem);
max-width: 600px;
2025-05-03 14:20:57 -07:00
}
.offline-icon {
2025-05-03 18:22:25 -07:00
font-size: clamp(3rem, 10vw, 5rem);
2025-05-03 14:20:57 -07:00
margin-bottom: 2rem;
animation: pulse 2s infinite;
2025-05-03 18:22:25 -07:00
position: relative;
}
.offline-icon::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: radial-gradient(circle, rgba(254, 128, 25, 0.2) 0%, rgba(254, 128, 25, 0) 70%);
border-radius: 50%;
z-index: -1;
animation: pulse-shadow 2s infinite;
2025-05-03 14:20:57 -07:00
}
.button {
2025-05-03 18:22:25 -07:00
display: inline-flex;
align-items: center;
justify-content: center;
2025-05-03 14:20:57 -07:00
background-color: var(--color-accent);
color: var(--color-bg);
padding: 0.75rem 1.5rem;
2025-05-03 18:22:25 -07:00
border-radius: var(--radius-md);
2025-05-03 14:20:57 -07:00
text-decoration: none;
font-weight: bold;
2025-05-03 18:22:25 -07:00
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
2025-05-03 14:20:57 -07:00
cursor: pointer;
margin-top: 1rem;
2025-05-03 18:22:25 -07:00
border: none;
font-family: var(--font-mono);
font-size: 1rem;
min-width: 180px;
2025-05-03 14:20:57 -07:00
}
.button:hover {
transform: translateY(-2px);
2025-05-03 18:22:25 -07:00
box-shadow: var(--shadow-md);
background-color: var(--color-accent-secondary);
2025-05-03 14:20:57 -07:00
}
2025-05-03 18:22:25 -07:00
.button:active {
transform: translateY(0);
}
.button:focus {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.button-icon {
margin-right: 0.5rem;
}
.card {
background-color: var(--color-card);
border-radius: var(--radius-md);
padding: 1.5rem;
2025-05-03 14:20:57 -07:00
margin-top: 2rem;
width: 100%;
2025-05-03 18:22:25 -07:00
max-width: 500px;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
2025-05-03 14:20:57 -07:00
}
2025-05-03 18:22:25 -07:00
.card:hover {
background-color: var(--color-card-hover);
box-shadow: var(--shadow-md);
}
.cached-pages {
text-align: left;
width: 100%;
2025-05-03 14:20:57 -07:00
}
.cached-pages ul {
list-style: none;
2025-05-03 18:22:25 -07:00
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
margin-top: 1rem;
2025-05-03 14:20:57 -07:00
}
.cached-pages li {
margin-bottom: 0.5rem;
}
.cached-pages a {
color: var(--color-text);
text-decoration: none;
border-bottom: 1px solid var(--color-accent);
padding-bottom: 2px;
2025-05-03 18:22:25 -07:00
display: inline-block;
transition: color 0.2s, transform 0.2s;
2025-05-03 14:20:57 -07:00
}
.cached-pages a:hover {
color: var(--color-accent);
2025-05-03 18:22:25 -07:00
transform: translateX(3px);
}
.status-indicator {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
background-color: rgba(251, 73, 52, 0.2);
color: #fb4934;
border-radius: var(--radius-md);
font-size: 0.875rem;
margin-bottom: 2rem;
}
.status-indicator.online {
background-color: rgba(184, 187, 38, 0.2);
color: #b8bb26;
}
.status-indicator-icon {
margin-right: 0.5rem;
}
.footer {
margin-top: 3rem;
text-align: center;
font-size: 0.875rem;
color: var(--color-muted);
}
.footer a {
color: var(--color-accent);
text-decoration: none;
2025-05-03 14:20:57 -07:00
}
2025-05-03 18:22:25 -07:00
/* Animations */
2025-05-03 14:20:57 -07:00
@keyframes pulse {
0% {
opacity: 1;
2025-05-03 18:22:25 -07:00
transform: scale(1);
2025-05-03 14:20:57 -07:00
}
50% {
2025-05-03 18:22:25 -07:00
opacity: 0.7;
transform: scale(0.95);
2025-05-03 14:20:57 -07:00
}
100% {
opacity: 1;
2025-05-03 18:22:25 -07:00
transform: scale(1);
}
}
@keyframes pulse-shadow {
0% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.1);
}
100% {
opacity: 0.7;
transform: scale(1);
}
}
/* Responsive adjustments */
@media (max-width: 600px) {
body {
padding: 1.5rem;
}
.cached-pages ul {
grid-template-columns: 1fr;
}
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.offline-icon,
.offline-icon::after,
.button,
.cached-pages a {
animation: none;
transition: none;
2025-05-03 14:20:57 -07:00
}
}
< / style >
< / head >
< body >
< main >
2025-05-03 18:22:25 -07:00
< div class = "status-indicator" >
< span class = "status-indicator-icon" > ⚠️< / span >
< span > You are currently offline< / span >
< / div >
2025-05-03 14:20:57 -07:00
< div class = "offline-icon" > 📶< / div >
2025-05-03 18:22:25 -07:00
< h1 > No Internet Connection< / h1 >
2025-05-03 14:20:57 -07:00
< p > It looks like you've lost your internet connection. Some pages may still be available if you've visited them before.< / p >
2025-05-03 18:22:25 -07:00
< button class = "button" id = "retry-button" >
< span class = "button-icon" > 🔄< / span >
< span > Retry Connection< / span >
< / button >
< div class = "card" id = "cached-pages" >
2025-05-03 14:20:57 -07:00
< h2 > Available Pages< / h2 >
< p > Loading cached pages...< / p >
< ul id = "cached-pages-list" > < / ul >
< / div >
2025-05-03 18:22:25 -07:00
< div class = "footer" >
< p > © 2025 < a href = "/" > Justin Deal< / a > | < a href = "javascript:void(0)" id = "clear-cache-button" > Clear Cache< / a > < / p >
< / div >
2025-05-03 14:20:57 -07:00
< / main >
< script >
// Check if we're actually offline
function checkConnection() {
return navigator.onLine;
}
// Update UI based on connection status
function updateConnectionStatus() {
2025-05-03 18:22:25 -07:00
const statusIndicator = document.querySelector('.status-indicator');
const statusIcon = document.querySelector('.status-indicator-icon');
const statusText = statusIndicator.querySelector('span:last-child');
2025-05-03 14:20:57 -07:00
if (checkConnection()) {
2025-05-03 18:22:25 -07:00
// We're back online
statusIndicator.classList.add('online');
statusIcon.textContent = '✅';
statusText.textContent = 'You are back online';
// Update other UI elements
document.querySelector('.offline-icon').textContent = '🌐';
document.querySelector('h1').textContent = "Connection Restored";
document.querySelector('p').textContent = "Your internet connection has been restored. You can continue browsing or reload the page to get the latest content.";
// Change retry button to reload button
const button = document.getElementById('retry-button');
button.innerHTML = '< span class = "button-icon" > 🔄< / span > < span > Reload Page< / span > ';
button.addEventListener('click', () => {
window.location.reload();
}, { once: true });
2025-05-03 14:20:57 -07:00
} else {
// Still offline
2025-05-03 18:22:25 -07:00
statusIndicator.classList.remove('online');
statusIcon.textContent = '⚠️';
statusText.textContent = 'You are currently offline';
2025-05-03 14:20:57 -07:00
document.querySelector('.offline-icon').textContent = '📶';
2025-05-03 18:22:25 -07:00
document.querySelector('h1').textContent = "No Internet Connection";
2025-05-03 14:20:57 -07:00
}
}
// Listen for online/offline events
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
2025-05-03 18:22:25 -07:00
// Initial status check
updateConnectionStatus();
2025-05-03 14:20:57 -07:00
// Retry button
document.getElementById('retry-button').addEventListener('click', () => {
2025-05-03 18:22:25 -07:00
const button = document.getElementById('retry-button');
button.innerHTML = '< span class = "button-icon" > 🔄< / span > < span > Checking...< / span > ';
button.disabled = true;
2025-05-03 14:20:57 -07:00
document.querySelector('.offline-icon').textContent = '🔄';
document.querySelector('h1').textContent = "Checking Connection...";
// Try to fetch the homepage
2025-05-03 18:22:25 -07:00
fetch('/', { cache: 'no-store' })
2025-05-03 14:20:57 -07:00
.then(() => {
// If successful, we're online
2025-05-03 18:22:25 -07:00
updateConnectionStatus();
button.disabled = false;
2025-05-03 14:20:57 -07:00
})
.catch(() => {
// Still offline
updateConnectionStatus();
2025-05-03 18:22:25 -07:00
button.disabled = false;
button.innerHTML = '< span class = "button-icon" > 🔄< / span > < span > Retry Connection< / span > ';
2025-05-03 14:20:57 -07:00
});
});
2025-05-03 18:22:25 -07:00
// Clear cache button
document.getElementById('clear-cache-button').addEventListener('click', () => {
if ('caches' in window) {
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
return caches.delete(cacheName);
})
);
}).then(() => {
alert('Cache cleared successfully');
loadCachedPages(); // Refresh the cached pages list
}).catch(error => {
console.error('Error clearing cache:', error);
alert('Failed to clear cache: ' + error.message);
});
} else {
alert('Cache API not supported in your browser');
}
});
// Function to load and display cached pages
function loadCachedPages() {
const cachedPagesContainer = document.getElementById('cached-pages');
const cachedPagesList = document.getElementById('cached-pages-list');
// Check if caches API is available
if (!('caches' in window) || !('serviceWorker' in navigator)) {
cachedPagesContainer.innerHTML = '< h2 > Cache Not Available< / h2 > < p > Your browser does not support caching or service workers.< / p > ';
return;
}
// Get all cache stores
caches.keys().then(cacheNames => {
// Find caches that might contain HTML pages
const pageCaches = cacheNames.filter(name =>
name.includes('justin-deal') & &
!name.includes('metadata') & &
!name.includes('images')
);
if (pageCaches.length === 0) {
cachedPagesContainer.innerHTML = '< h2 > No Cached Pages Available< / h2 > < p > Try visiting some pages when you\'re back online.< / p > ';
return;
}
// Collect all cached requests from relevant caches
const allRequests = [];
Promise.all(
pageCaches.map(cacheName =>
caches.open(cacheName)
.then(cache => cache.keys())
.then(requests => {
allRequests.push(...requests);
})
)
).then(() => {
if (allRequests.length === 0) {
cachedPagesContainer.innerHTML = '< h2 > No Cached Pages Available< / h2 > < p > Try visiting some pages when you\'re back online.< / p > ';
return;
}
// Filter for HTML pages and remove duplicates
const uniqueUrls = new Set();
const htmlRequests = allRequests.filter(request => {
const url = new URL(request.url);
const isHtmlPage = url.pathname === '/' ||
url.pathname.endsWith('.html') ||
!url.pathname.includes('.');
// Only include if it's an HTML page and we haven't seen this URL before
if (isHtmlPage & & !uniqueUrls.has(url.pathname)) {
uniqueUrls.add(url.pathname);
return true;
}
return false;
});
// Sort by URL
htmlRequests.sort((a, b) => {
return new URL(a.url).pathname.localeCompare(new URL(b.url).pathname);
});
// Create list items
const listItems = htmlRequests.map(request => {
const url = new URL(request.url);
let pageName = url.pathname === '/' ? 'Home' : url.pathname
.replace(/\/$/, '')
.replace(/^\//, '')
.replace(/\.html$/, '')
.split('/')
.pop()
.replace(/-/g, ' ');
// Capitalize first letter of each word
pageName = pageName
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return `< li > < a href = "${url.pathname}" > ${pageName}< / a > < / li > `;
});
if (listItems.length === 0) {
cachedPagesContainer.innerHTML = '< h2 > No Cached Pages Available< / h2 > < p > Try visiting some pages when you\'re back online.< / p > ';
return;
}
cachedPagesList.innerHTML = listItems.join('');
document.querySelector('#cached-pages p').style.display = 'none';
2025-05-03 14:20:57 -07:00
});
2025-05-03 18:22:25 -07:00
}).catch(error => {
console.error('Error accessing cache:', error);
cachedPagesContainer.innerHTML = '< h2 > Could Not Access Cache< / h2 > < p > There was an error accessing the cached pages.< / p > ';
});
2025-05-03 14:20:57 -07:00
}
2025-05-03 18:22:25 -07:00
// Load cached pages on page load
loadCachedPages();
2025-05-03 14:20:57 -07:00
< / script >
< / body >
< / html >