This commit is contained in:
cpu
2025-03-29 03:45:26 +01:00
parent 5763407edc
commit 832f19235f
31 changed files with 373 additions and 139 deletions

View File

@@ -7,8 +7,10 @@ node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
Dockerfile
# Development files
.dockerignore
.editorconfig
.eslintrc
.stylelintrc
@@ -18,8 +20,7 @@ yarn-error.log
*.swp
*.swo
# Documentation
docs/
# docs/
README.md
LICENSE
CHANGELOG.md
@@ -45,11 +46,13 @@ dist/
build/
# Environment files
.env
# We need .env for our application
#.env
.env.*
*.env
# Project specific files
screenshots/
labels.example
virt-game-timer.service
package.json
package-lock.json

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# Environment Variables Example for Game Timer Application
# Copy this file to .env and fill in your own values
# Public VAPID key for push notifications
# Generate your own VAPID keys for production:
# https://github.com/web-push-libs/web-push#generatevapidkeys
PUBLIC_VAPID_KEY=your_public_vapid_key_here
# Backend URL for your push notification server
BACKEND_URL=https://your-push-server.example.com
# Other environment variables can be added here

View File

@@ -1,12 +1,14 @@
# Use the official Nginx image as the base image
# Use a lightweight server
FROM nginx:alpine
# Remove the default Nginx static files
RUN rm -rf /usr/share/nginx/html/*
# Set working directory
WORKDIR /usr/share/nginx/html
# Copy public directory contents into the Nginx directory
COPY public/ /usr/share/nginx/html/
COPY src/ /usr/share/nginx/html/src/
# Copy all the application files
COPY . .
# Copy the .env file
COPY .env .
# Expose port 80
EXPOSE 80

107
README.md
View File

@@ -6,27 +6,57 @@ Multi-player game-timer timer with carousel navigation
```
game-timer/
├── docs/ # Documentation and project resources
│ └── screenshots/ # Application screenshots
├── public/ # Static assets and public-facing resources
├── css/ # CSS stylesheets
├── images/ # Images used by the application
│ ├── icons/ # App icons for PWA
│ ├── audio/ # Audio files
│ ├── index.html # Main HTML entry point
│ ├── manifest.json # PWA manifest
│ └── sw.js # Service Worker
├── css/ # CSS stylesheets
├── icons/ # App icons
├── images/ # Image assets
├── js/ # Symbolic link to src/js for compatibility
├── index.html # Main HTML entry point
├── manifest.json # PWA manifest
├── sw.js # Service Worker
├── src/ # Source code
│ └── js/ # JavaScript files
│ ├── core/ # Core application logic
│ ├── ui/ # UI-related code
── services/ # External services integration
│ └── utils/ # Utility functions
├── Dockerfile # Docker container definition
── services/ # External services integration
├── Dockerfile # Docker container definition (nginx)
├── .dockerignore # Files to exclude from Docker build
── package.json # Project metadata and dependencies
── .env # Environment variables for production
├── .env.example # Example environment variables template
└── package.json # Project metadata and deployment scripts
```
## Environment Variables
The application uses environment variables for configuration. These are loaded from a `.env` file at runtime.
### Setting Up Environment Variables
1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
2. Edit the `.env` file with your own values:
```
# Public VAPID key for push notifications
PUBLIC_VAPID_KEY=your_public_vapid_key_here
# Backend URL for push notifications
BACKEND_URL=https://your-push-server.example.com
```
3. For security, never commit your `.env` file to version control. It's already included in `.gitignore`.
### Generating VAPID Keys
For push notifications, you need to generate your own VAPID keys:
```bash
npx web-push generate-vapid-keys
```
Use the public key in your `.env` file and keep the private key secure for your backend server.
# PWA Containerized Deployment
This document provides step-by-step instructions to pull the source code and deploy the Progressive Web App (PWA) using Docker on a production server.
@@ -56,12 +86,24 @@ From the repository root, run the following command to build your Docker image:
docker build -t 'game-timer:latest' .
```
### 3. Run the Docker Container
Once the image is built, run the container on port 8080 with:
or use the npm script:
```bash
docker run -d -p 8080:80 --name game-timer-container game-timer:latest
npm run docker:build
```
### 3. Run the Docker Container
Once the image is built, run the container on port 80 with:
```bash
docker run -d -p 80:80 --name game-timer game-timer:latest
```
or use the npm script:
```bash
npm run start
```
### 4. Verify the Deployment
@@ -75,29 +117,42 @@ docker ps
View logs (if needed):
```bash
docker logs game-timer-container
docker logs game-timer
```
After running the container, open your web browser and navigate to:
```bash
http://localhost:8080
```
http://localhost
```
### 5. Terminate
To stop your running game-timer-container, use:
To stop your running game-timer container, use:
```bash
docker stop game-timer-container
docker stop game-timer
docker rm game-timer
```
or use the npm script:
```bash
npm run stop
```
## Development
For local development without Docker, you can use:
For local development without Docker, you can use any static file server such as:
```bash
npm run dev
python -m http.server
```
This will start a local development server and open the application in your browser.
or
```bash
npx serve
```
This will start a local development server and you can access the application in your browser.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 770 B

BIN
images/screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

BIN
images/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

View File

@@ -5,9 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50">
<title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<!-- Favicon links -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<!-- Web app manifest -->
<link rel="manifest" href="/manifest.json">
<!-- App icons for various platforms -->
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<!-- CSS stylesheets -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
@@ -102,10 +113,10 @@
</div>
<!-- Main application script -->
<script type="module" src="../src/js/app.js"></script>
<script type="module" src="/js/app.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js")
navigator.serviceWorker.register("/sw.js")
.then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err));
}

1
js Symbolic link
View File

@@ -0,0 +1 @@
src/js

View File

@@ -2,54 +2,50 @@
"name": "Game Timer PWA",
"short_name": "Game Timer",
"description": "Multi-player chess-like timer with carousel navigation",
"start_url": "./index.html",
"start_url": "/index.html",
"id": "/index.html",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"background_color": "#f5f5f5",
"theme_color": "#2c3e50",
"icons": [
{
"src": "./icons/android-chrome-192x192.png",
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icons/android-chrome-512x512.png",
"src": "/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./icons/apple-touch-icon.png",
"src": "/icons/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./icons/favicon-32x32.png",
"src": "/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./icons/favicon-16x16.png",
"src": "/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
],
"screenshots": [
{
"src": "../docs/screenshots/screenshot1.png",
"sizes": "2604x2269",
"src": "/images/screenshot1.png",
"sizes": "2560x1860",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "../docs/screenshots/screenshot2.png",
"sizes": "1082x2402",
"src": "/images/screenshot2.png",
"sizes": "750x1594",
"type": "image/png"
}
],
"url_handlers": [
{
"origin": "https://game-timer.virtonline.eu"
}
],
"gcm_sender_id": "103953800507"
]
}

View File

@@ -1,17 +1,13 @@
{
"name": "countdown",
"name": "game-timer",
"version": "1.0.0",
"description": "Multi-player chess timer with carousel navigation",
"main": "src/js/app.js",
"scripts": {
"start": "docker run -d -p 8080:80 --name game-timer-container game-timer:latest",
"stop": "docker stop game-timer-container && docker rm game-timer-container",
"build": "docker build -t 'game-timer:latest' .",
"rebuild": "npm run stop || true && npm run build && npm run start",
"logs": "docker logs game-timer-container",
"status": "docker ps | grep game-timer-container",
"dev": "npx http-server . -o /public",
"clean": "docker system prune -f"
"docker:build": "docker build -t 'game-timer:latest' .",
"start": "docker run -d -p 80:80 --name game-timer game-timer:latest",
"stop": "docker stop game-timer && docker rm game-timer",
"rebuild": "npm run stop || true && npm run docker:build && npm run start"
},
"keywords": [
"timer",

View File

@@ -1,19 +0,0 @@
{
"name": "Game Timer",
"short_name": "Game Timer",
"icons": [
{
"src": "./icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -6,6 +6,7 @@ import * as timer from './core/timer.js';
import camera from './ui/camera.js'; // Default export
import audioManager from './ui/audio.js';
import * as pushFlic from './services/pushFlicIntegration.js';
import { initEnv } from './env-loader.js';
// Import externalized modules
import * as gameActions from './core/gameActions.js';
@@ -15,9 +16,17 @@ import * as serviceWorkerManager from './services/serviceWorkerManager.js';
// --- Initialization ---
function initialize() {
async function initialize() {
console.log("Initializing Game Timer App...");
// 0. Wait for environment variables to load
try {
await initEnv();
console.log("Environment variables loaded");
} catch (error) {
console.warn("Failed to load environment variables, using defaults:", error);
}
// 1. Load saved state or defaults
state.loadData();
@@ -88,4 +97,11 @@ function initialize() {
}
// --- Start the application ---
initialize();
// We need to use an async IIFE to await the async initialize function
(async () => {
try {
await initialize();
} catch (error) {
console.error("Error initializing application:", error);
}
})();

View File

@@ -1,6 +1,17 @@
// config.js
export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
export const BACKEND_URL = 'https://webpush.virtonline.eu';
import { getEnv } from './env-loader.js';
export function getPublicVapidKey() {
// Get the VAPID key from environment variables through the env-loader
return getEnv('PUBLIC_VAPID_KEY', 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E');
}
// The VAPID key should not be exposed directly in the source code
// Use the getter function instead: getPublicVapidKey()
// export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
// Get backend URL from environment variables
export const BACKEND_URL = getEnv('BACKEND_URL', 'https://webpush.virtonline.eu');
export const FLIC_BUTTON_ID = 'game-button'; // Example ID, might need configuration
export const LOCAL_STORAGE_KEY = 'gameTimerData';
export const FLIC_BATTERY_THRESHOLD = 50; // Battery percentage threshold for low battery warning

View File

@@ -7,18 +7,23 @@ let timerInterval = null;
let onTimerTickCallback = null; // Callback for UI updates
let onPlayerSwitchCallback = null; // Callback for when player switches due to time running out
let onGameOverCallback = null; // Callback for when all players run out of time
let timeExpiredFlagsById = new Map(); // Track which players have had their timeout sound played
export function initTimer(options) {
onTimerTickCallback = options.onTimerTick;
onPlayerSwitchCallback = options.onPlayerSwitch;
onGameOverCallback = options.onGameOver;
timeExpiredFlagsById.clear(); // Reset flags on init
}
export function startTimer() {
if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any
// Stop any previous sounds (like low time warning) before starting fresh
audioManager.stopAllSounds(); // Consider if this is too aggressive
audioManager.stopAllSounds();
// Reset the expired sound flags when starting a new timer
timeExpiredFlagsById.clear();
timerInterval = setInterval(() => {
const currentPlayerIndex = state.getCurrentPlayerIndex();
@@ -35,32 +40,24 @@ export function startTimer() {
const newTime = currentPlayer.remainingTime - 1;
state.updatePlayerTime(currentPlayerIndex, newTime); // Update state
// Play timer sounds
// Play timer sounds - ensure we're not leaking audio resources
audioManager.playTimerSound(newTime);
// Notify UI to update
if (onTimerTickCallback) onTimerTickCallback();
} else { // Current player's time just hit 0 or was already 0
// Ensure time is exactly 0 if it somehow went negative (unlikely with check above)
// Ensure time is exactly 0 if it somehow went negative
if(currentPlayer.remainingTime < 0) {
state.updatePlayerTime(currentPlayerIndex, 0);
}
// Stop this player's timer tick sound if it was playing
// audioManager.stop('timerTick'); // Or specific low time sound
// Play time expired sound (only once)
// Check if we just hit zero to avoid playing repeatedly
// This logic might be complex, audioManager could handle idempotency
if (currentPlayer.remainingTime === 0 && !currentPlayer.timeExpiredSoundPlayed) {
// Play time expired sound (only once per player per game)
if (!timeExpiredFlagsById.has(currentPlayer.id)) {
audioManager.playTimerExpired();
// We need a way to mark that the sound played for this player this turn.
// This might require adding a temporary flag to the player state,
// or handling it within the audioManager. Let's assume audioManager handles it for now.
timeExpiredFlagsById.set(currentPlayer.id, true);
}
// Check if the game should end or switch player
if (state.areAllTimersFinished()) {
stopTimer();
@@ -69,10 +66,11 @@ export function startTimer() {
// Find the *next* player who still has time
const nextPlayerIndex = state.findNextPlayerWithTime(); // This finds ANY player with time
if (nextPlayerIndex !== -1 && nextPlayerIndex !== currentPlayerIndex) {
// Switch player
// Switch player and ensure we stop any sounds from current player
audioManager.stopTimerSounds(); // Stop specific timer sounds before switching
if (onPlayerSwitchCallback) onPlayerSwitchCallback(nextPlayerIndex);
// Play switch sound (might be handled in app.js based on state change)
// audioManager.play('playerSwitch'); // Or let app.js handle sounds based on actions
// Immediately update UI after switch
if (onTimerTickCallback) onTimerTickCallback();
} else if (nextPlayerIndex === -1) {
@@ -81,7 +79,7 @@ export function startTimer() {
stopTimer(); // Stop timer if state is inconsistent
if (onGameOverCallback) onGameOverCallback(); // Treat as game over
}
// If nextPlayerIndex is the same as currentPlayerIndex, means others are out of time, let this timer continue (or rather, stop ticking down as remainingTime is 0)
// If nextPlayerIndex is the same as currentPlayerIndex, means others are out of time, let this timer continue
}
}
}, 1000);
@@ -90,10 +88,17 @@ export function startTimer() {
export function stopTimer() {
clearInterval(timerInterval);
timerInterval = null;
// Optionally stop timer sounds here if needed
// audioManager.stop('timerTick');
// Stop all timer-related sounds to prevent them from continuing to play
audioManager.stopTimerSounds();
}
export function isTimerRunning() {
return timerInterval !== null;
}
// Clean up resources when the application is closing or component unmounts
export function cleanup() {
stopTimer();
timeExpiredFlagsById.clear();
audioManager.stopAllSounds();
}

88
src/js/env-loader.js Normal file
View File

@@ -0,0 +1,88 @@
// env-loader.js
// This module is responsible for loading environment variables from .env file
// Store environment variables in a global object
window.ENV_CONFIG = {};
// Function to load environment variables from .env file
async function loadEnvVariables() {
try {
// Fetch the .env file as text
const response = await fetch('/.env');
if (!response.ok) {
console.warn('Could not load .env file. Using default values.');
setDefaultEnvValues();
return;
}
const envText = await response.text();
// Parse the .env file content
const envVars = parseEnvFile(envText);
// Store in the global ENV_CONFIG object
window.ENV_CONFIG = { ...window.ENV_CONFIG, ...envVars };
console.log('Environment variables loaded successfully');
} catch (error) {
console.error('Error loading environment variables:', error);
setDefaultEnvValues();
}
}
// Parse .env file content into key-value pairs
function parseEnvFile(envText) {
const envVars = {};
// Split by lines and process each line
envText.split('\n').forEach(line => {
// Skip empty lines and comments
if (!line || line.trim().startsWith('#')) return;
// Extract key-value pairs
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
if (match) {
const key = match[1];
let value = match[2] || '';
// Remove quotes if present
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
envVars[key] = value;
}
});
return envVars;
}
// Set default values for required environment variables
function setDefaultEnvValues() {
window.ENV_CONFIG = {
...window.ENV_CONFIG,
PUBLIC_VAPID_KEY: 'your_public_vapid_key_here',
BACKEND_URL: 'https://your-push-server.example.com'
};
console.log('Using default environment values');
}
// Export function to initialize environment variables
export async function initEnv() {
await loadEnvVariables();
return window.ENV_CONFIG;
}
// Auto-initialize when imported
initEnv();
// Export access functions for environment variables
export function getEnv(key, defaultValue = '') {
return window.ENV_CONFIG[key] || defaultValue;
}
export default {
initEnv,
getEnv
};

View File

@@ -1,5 +1,5 @@
// pushFlicIntegration.js
import { PUBLIC_VAPID_KEY, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js';
import { getPublicVapidKey, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js';
let pushSubscription = null; // Keep track locally if needed
let actionHandlers = {}; // Store handlers for different Flic actions
@@ -139,7 +139,7 @@ async function subscribeToPush() {
if (existingSubscription) {
const existingKey = existingSubscription.options?.applicationServerKey;
if (!existingKey || arrayBufferToBase64(existingKey) !== PUBLIC_VAPID_KEY) {
if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
console.log('VAPID key mismatch or missing. Unsubscribing old subscription.');
await existingSubscription.unsubscribe();
existingSubscription = null;
@@ -153,7 +153,7 @@ async function subscribeToPush() {
let finalSubscription = existingSubscription;
if (needsResubscribe) {
console.log('Subscribing for push notifications...');
const applicationServerKey = urlBase64ToUint8Array(PUBLIC_VAPID_KEY);
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
finalSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey

View File

@@ -32,7 +32,7 @@ export function handleServiceWorkerMessage(event) {
export function setupServiceWorker(messageHandler) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/public/sw.js')
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered successfully.');

View File

@@ -302,6 +302,14 @@ const audioManager = {
this.lastTickTime = 0;
},
// Stop timer-specific sounds (tick, low time warning)
stopTimerSounds() {
// Reset tick fade timer when stopping timer sounds
this.lastTickTime = 0;
// In this implementation, sounds are so short-lived that
// they don't need to be explicitly stopped, just fade prevention is enough
},
// Reset the tick fading (call this when timer is paused or player changes)
resetTickFade() {
this.lastTickTime = 0;

View File

@@ -1,20 +1,33 @@
// Service Worker version
const CACHE_VERSION = 'v1.0.0';
const CACHE_VERSION = 'v1.0.2';
const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
// Files to cache
const CACHE_FILES = [
'./',
'./index.html',
'../src/js/app.js',
'../src/js/ui/audio.js',
'./css/styles.css',
'./manifest.json',
'./icons/android-chrome-192x192.png',
'./icons/android-chrome-512x512.png',
'./icons/apple-touch-icon.png',
'./icons/favicon-32x32.png',
'./icons/favicon-16x16.png'
'/',
'/sw.js',
'/index.html',
'/manifest.json',
'/css/styles.css',
'/favicon.ico',
'/icons/android-chrome-192x192.png',
'/icons/android-chrome-512x512.png',
'/icons/apple-touch-icon.png',
'/icons/favicon-32x32.png',
'/icons/favicon-16x16.png',
'/js/app.js',
'/js/config.js',
'/js/env-loader.js',
'/js/ui/audio.js',
'/js/ui/camera.js',
'/js/ui/ui.js',
'/js/core/state.js',
'/js/core/timer.js',
'/js/core/gameActions.js',
'/js/core/playerManager.js',
'/js/core/eventHandlers.js',
'/js/services/pushFlicIntegration.js',
'/js/services/serviceWorkerManager.js'
];
// Install event - Cache files
@@ -124,7 +137,7 @@ self.addEventListener('push', event => {
// If no window is open, we MUST show a notification
return self.registration.showNotification(pushData.title, {
body: pushData.body,
icon: './icons/android-chrome-192x192.png', // Optional: path to an icon
icon: '/icons/android-chrome-192x192.png', // Updated path
data: pushData.data // Pass data if needed when notification is clicked
});
}
@@ -144,7 +157,7 @@ self.addEventListener('push', event => {
if (!messageSent) { // Only show notification if no message was sent? Or always show?
return self.registration.showNotification(pushData.title, {
body: pushData.body,
icon: './icons/android-chrome-192x192.png',
icon: '/icons/android-chrome-192x192.png',
data: pushData.data
});
}
@@ -185,7 +198,7 @@ self.addEventListener('notificationclick', event => {
}
if (self.clients.openWindow) {
return self.clients.openWindow('./');
return self.clients.openWindow('/');
}
})
);

36
test.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker Test</title>
</head>
<body>
<h1>Service Worker Test</h1>
<p>This page tests if the service worker is registered correctly.</p>
<div id="status">Checking service worker registration...</div>
<script>
const statusDiv = document.getElementById('status');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
statusDiv.innerHTML = 'Service worker registered successfully!<br>' +
'Scope: ' + registration.scope;
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(error => {
statusDiv.innerHTML = 'Service worker registration failed: ' + error;
console.error('ServiceWorker registration failed: ', error);
});
navigator.serviceWorker.ready.then(registration => {
statusDiv.innerHTML += '<br><br>Service worker is ready!';
});
} else {
statusDiv.innerHTML = 'Service workers are not supported in this browser.';
}
</script>
</body>
</html>