feat: minimal pwa

This commit is contained in:
2025-11-15 13:59:57 +01:00
parent bd4ada7c18
commit 3ab8cdcc37
7 changed files with 153 additions and 1 deletions

View File

@@ -31,8 +31,24 @@ document.addEventListener('DOMContentLoaded', () => {
setupMobileAddLink(); setupMobileAddLink();
setupLayoutToggle(); setupLayoutToggle();
applyLayout(currentLayout); applyLayout(currentLayout);
registerServiceWorker();
}); });
// Register service worker for PWA
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered successfully:', registration.scope);
})
.catch((error) => {
console.log('Service Worker registration failed:', error);
});
});
}
}
// Event listeners // Event listeners
function setupEventListeners() { function setupEventListeners() {
linkForm.addEventListener('submit', handleAddLink); linkForm.addEventListener('submit', handleAddLink);

BIN
public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

22
public/icon.svg Normal file
View File

@@ -0,0 +1,22 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Transparent background -->
<rect width="512" height="512" fill="none"/>
<!-- Square with gradient, no rounded corners -->
<rect x="0" y="0" width="512" height="512" fill="url(#grad)"/>
<g transform="translate(256, 256)">
<!-- Link icon - two connected circles -->
<circle cx="-60" cy="0" r="50" fill="none" stroke="white" stroke-width="40" stroke-linecap="round"/>
<circle cx="60" cy="0" r="50" fill="none" stroke="white" stroke-width="40" stroke-linecap="round"/>
<!-- Connecting lines -->
<line x1="-10" y1="0" x2="10" y2="0" stroke="white" stroke-width="40" stroke-linecap="round"/>
<!-- Small detail circles inside -->
<circle cx="-60" cy="0" r="20" fill="white" opacity="0.3"/>
<circle cx="60" cy="0" r="20" fill="white" opacity="0.3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -2,8 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="LinkDing - Your Link Collection">
<meta name="theme-color" content="#6366f1">
<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="LinkDing">
<title>LinkDing - Your Link Collection</title> <title>LinkDing - Your Link Collection</title>
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icon-192.png">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "LinkDing",
"short_name": "LinkDing",
"description": "Your Link Collection",
"start_url": "/",
"display": "fullscreen",
"background_color": "#0f172a",
"theme_color": "#6366f1",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

82
public/sw.js Normal file
View File

@@ -0,0 +1,82 @@
const CACHE_NAME = 'linkding-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icon-192.png',
'/icon-512.png'
];
// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
.catch((error) => {
console.error('Cache install failed:', error);
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
return self.clients.claim();
});
// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
// API requests - always fetch from network, don't cache
if (event.request.url.includes('/api/')) {
event.respondWith(fetch(event.request));
return;
}
// Static assets - cache first, then network
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request).then((response) => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
.catch(() => {
// If both cache and network fail, return offline page if available
if (event.request.destination === 'document') {
return caches.match('/index.html');
}
})
);
});