503 lines
15 KiB
HTML
503 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content="You are currently offline. Some pages may still be available if you've visited them before.">
|
|
<title>Offline | Justin Deal</title>
|
|
<link rel="icon" href="/favicons/favicon.png" type="image/png">
|
|
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png">
|
|
<style>
|
|
:root {
|
|
--color-bg: #fbf1c7;
|
|
--color-text: #3c3836;
|
|
--color-accent: #fe8019;
|
|
--color-accent-secondary: #b8bb26;
|
|
--color-muted: #7c6f64;
|
|
--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;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--color-bg: #282828;
|
|
--color-text: #ebdbb2;
|
|
--color-accent: #fe8019;
|
|
--color-accent-secondary: #b8bb26;
|
|
--color-muted: #a89984;
|
|
--color-card: rgba(40, 40, 40, 0.8);
|
|
--color-card-hover: rgba(40, 40, 40, 1);
|
|
}
|
|
}
|
|
|
|
/* Base styles */
|
|
* {
|
|
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;
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
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);
|
|
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);
|
|
margin-bottom: 1rem;
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 1.5rem;
|
|
font-size: clamp(0.9rem, 2vw, 1.1rem);
|
|
max-width: 600px;
|
|
}
|
|
|
|
.offline-icon {
|
|
font-size: clamp(3rem, 10vw, 5rem);
|
|
margin-bottom: 2rem;
|
|
animation: pulse 2s infinite;
|
|
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;
|
|
}
|
|
|
|
.button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: var(--color-accent);
|
|
color: var(--color-bg);
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: var(--radius-md);
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
|
|
cursor: pointer;
|
|
margin-top: 1rem;
|
|
border: none;
|
|
font-family: var(--font-mono);
|
|
font-size: 1rem;
|
|
min-width: 180px;
|
|
}
|
|
|
|
.button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-md);
|
|
background-color: var(--color-accent-secondary);
|
|
}
|
|
|
|
.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;
|
|
margin-top: 2rem;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
|
|
}
|
|
|
|
.card:hover {
|
|
background-color: var(--color-card-hover);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.cached-pages {
|
|
text-align: left;
|
|
width: 100%;
|
|
}
|
|
|
|
.cached-pages ul {
|
|
list-style: none;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 0.75rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.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;
|
|
display: inline-block;
|
|
transition: color 0.2s, transform 0.2s;
|
|
}
|
|
|
|
.cached-pages a:hover {
|
|
color: var(--color-accent);
|
|
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;
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes pulse {
|
|
0% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
transform: scale(0.95);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
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;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<div class="status-indicator">
|
|
<span class="status-indicator-icon">⚠️</span>
|
|
<span>You are currently offline</span>
|
|
</div>
|
|
|
|
<div class="offline-icon">📶</div>
|
|
<h1>No Internet Connection</h1>
|
|
<p>It looks like you've lost your internet connection. Some pages may still be available if you've visited them before.</p>
|
|
|
|
<button class="button" id="retry-button">
|
|
<span class="button-icon">🔄</span>
|
|
<span>Retry Connection</span>
|
|
</button>
|
|
|
|
<div class="card" id="cached-pages">
|
|
<h2>Available Pages</h2>
|
|
<p>Loading cached pages...</p>
|
|
<ul id="cached-pages-list"></ul>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>© 2025 <a href="/">Justin Deal</a> | <a href="javascript:void(0)" id="clear-cache-button">Clear Cache</a></p>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Check if we're actually offline
|
|
function checkConnection() {
|
|
return navigator.onLine;
|
|
}
|
|
|
|
// Update UI based on connection status
|
|
function updateConnectionStatus() {
|
|
const statusIndicator = document.querySelector('.status-indicator');
|
|
const statusIcon = document.querySelector('.status-indicator-icon');
|
|
const statusText = statusIndicator.querySelector('span:last-child');
|
|
|
|
if (checkConnection()) {
|
|
// 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 });
|
|
} else {
|
|
// Still offline
|
|
statusIndicator.classList.remove('online');
|
|
statusIcon.textContent = '⚠️';
|
|
statusText.textContent = 'You are currently offline';
|
|
|
|
document.querySelector('.offline-icon').textContent = '📶';
|
|
document.querySelector('h1').textContent = "No Internet Connection";
|
|
}
|
|
}
|
|
|
|
// Listen for online/offline events
|
|
window.addEventListener('online', updateConnectionStatus);
|
|
window.addEventListener('offline', updateConnectionStatus);
|
|
|
|
// Initial status check
|
|
updateConnectionStatus();
|
|
|
|
// Retry button
|
|
document.getElementById('retry-button').addEventListener('click', () => {
|
|
const button = document.getElementById('retry-button');
|
|
button.innerHTML = '<span class="button-icon">🔄</span><span>Checking...</span>';
|
|
button.disabled = true;
|
|
|
|
document.querySelector('.offline-icon').textContent = '🔄';
|
|
document.querySelector('h1').textContent = "Checking Connection...";
|
|
|
|
// Try to fetch the homepage
|
|
fetch('/', { cache: 'no-store' })
|
|
.then(() => {
|
|
// If successful, we're online
|
|
updateConnectionStatus();
|
|
button.disabled = false;
|
|
})
|
|
.catch(() => {
|
|
// Still offline
|
|
updateConnectionStatus();
|
|
button.disabled = false;
|
|
button.innerHTML = '<span class="button-icon">🔄</span><span>Retry Connection</span>';
|
|
});
|
|
});
|
|
|
|
// 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';
|
|
});
|
|
}).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>';
|
|
});
|
|
}
|
|
|
|
// Load cached pages on page load
|
|
loadCachedPages();
|
|
</script>
|
|
</body>
|
|
</html>
|