Skip to content

Commit 194bf73

Browse files
authored
Merge pull request #115 from burgerga/feat_pwa
Add PWA using django-pwa
2 parents 264eabd + 4d45cb8 commit 194bf73

23 files changed

+2933
-16
lines changed

myapp/serviceworker.js

Lines changed: 392 additions & 0 deletions
Large diffs are not rendered by default.
13.7 KB
Loading
10.1 KB
Loading
14.8 KB
Loading
29.8 KB
Loading
43 KB
Loading
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**
2+
* Manual Cache Manager
3+
* User-controlled caching with 48-hour expiration
4+
*/
5+
6+
class ManualCacheManager {
7+
constructor() {
8+
this.CACHE_DURATION = 48 * 60 * 60 * 1000; // 48 hours in milliseconds
9+
this.CACHE_KEY = 'offline_cache_timestamp';
10+
this.VERSION = 'v2.0.0';
11+
this.PAGE_CACHE_NAME = `vouchervault-pages-${this.VERSION}`;
12+
this.init();
13+
}
14+
15+
init() {
16+
this.updateCacheStatus();
17+
18+
// Check cache status every minute to update expiration time
19+
setInterval(() => {
20+
this.updateCacheStatus();
21+
}, 60000); // Update every 60 seconds
22+
23+
console.log('[ManualCache] Initialized');
24+
}
25+
26+
/**
27+
* Check if cache is still valid (within 48 hours)
28+
*/
29+
async isCacheValid() {
30+
const timestamp = localStorage.getItem(this.CACHE_KEY);
31+
if (!timestamp) return false;
32+
33+
const age = Date.now() - parseInt(timestamp);
34+
if (age >= this.CACHE_DURATION) return false;
35+
36+
// Also verify cache actually exists
37+
try {
38+
const cache = await caches.open(this.PAGE_CACHE_NAME);
39+
const keys = await cache.keys();
40+
return keys.length > 0;
41+
} catch (error) {
42+
return false;
43+
}
44+
}
45+
46+
/**
47+
* Get remaining cache time in hours and minutes
48+
*/
49+
getRemainingTime() {
50+
const timestamp = localStorage.getItem(this.CACHE_KEY);
51+
if (!timestamp) return { hours: 0, minutes: 0 };
52+
53+
const age = Date.now() - parseInt(timestamp);
54+
const remaining = Math.max(0, this.CACHE_DURATION - age);
55+
56+
const hours = Math.floor(remaining / (60 * 60 * 1000));
57+
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
58+
59+
return { hours, minutes };
60+
}
61+
62+
/**
63+
* Get remaining cache time in hours (for backwards compatibility)
64+
*/
65+
getRemainingHours() {
66+
return this.getRemainingTime().hours;
67+
}
68+
69+
/**
70+
* Update cache status display
71+
*/
72+
async updateCacheStatus() {
73+
const statusElement = document.getElementById('cache-status');
74+
const buttonTextElement = document.getElementById('cache-button-text');
75+
76+
if (!statusElement) {
77+
console.warn('[ManualCache] Status element not found');
78+
return;
79+
}
80+
81+
const isValid = await this.isCacheValid();
82+
console.log('[ManualCache] Updating status, valid:', isValid);
83+
84+
if (isValid) {
85+
const { hours, minutes } = this.getRemainingTime();
86+
statusElement.textContent = `${hours}h ${minutes}m left`;
87+
statusElement.className = 'badge bg-success ms-2';
88+
if (buttonTextElement) {
89+
buttonTextElement.textContent = 'Refresh Cache';
90+
}
91+
} else {
92+
// If cache doesn't exist but localStorage has timestamp, clear it
93+
if (localStorage.getItem(this.CACHE_KEY)) {
94+
localStorage.removeItem(this.CACHE_KEY);
95+
console.log('[ManualCache] Cleared stale timestamp');
96+
}
97+
statusElement.textContent = 'Not cached';
98+
statusElement.className = 'badge bg-secondary ms-2';
99+
if (buttonTextElement) {
100+
buttonTextElement.textContent = 'Cache for Offline';
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Cache all current page data and items
107+
*/
108+
async cacheData() {
109+
if (!('caches' in window)) {
110+
this.showToast('Error', 'Caching not supported in this browser', 'danger');
111+
return;
112+
}
113+
114+
const buttonTextElement = document.getElementById('cache-button-text');
115+
const originalText = buttonTextElement ? buttonTextElement.textContent : '';
116+
117+
if (buttonTextElement) {
118+
buttonTextElement.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Caching...';
119+
}
120+
121+
try {
122+
// Get current language
123+
const langMatch = window.location.pathname.match(/^\/(en|de|fr|it|es|pt|nl|pl|ru|zh|ja|ko)/);
124+
const currentLang = langMatch ? langMatch[1] : 'en';
125+
126+
const urlsToCache = [];
127+
128+
// Add essential pages
129+
urlsToCache.push(`/${currentLang}/`);
130+
urlsToCache.push(`/${currentLang}/dashboard`);
131+
urlsToCache.push(`/${currentLang}/shared-items/`);
132+
urlsToCache.push(`/${currentLang}/offline/`);
133+
134+
// Add all filter combinations
135+
const types = ['giftcard', 'coupon', 'voucher', 'loyaltycard'];
136+
const statuses = ['available', 'used', 'expired', 'soon_expiring', 'shared_by_me', 'shared_with_me'];
137+
138+
types.forEach(type => {
139+
urlsToCache.push(`/${currentLang}/?type=${type}`);
140+
});
141+
142+
statuses.forEach(status => {
143+
urlsToCache.push(`/${currentLang}/?status=${status}`);
144+
});
145+
146+
types.forEach(type => {
147+
statuses.forEach(status => {
148+
urlsToCache.push(`/${currentLang}/?type=${type}&status=${status}`);
149+
});
150+
});
151+
152+
// Extract all item URLs from the current page
153+
const itemLinks = document.querySelectorAll('a[href*="/items/view/"]');
154+
itemLinks.forEach(link => {
155+
const url = new URL(link.href);
156+
urlsToCache.push(url.pathname);
157+
});
158+
159+
console.log(`[ManualCache] Caching ${urlsToCache.length} URLs...`);
160+
161+
// Cache all URLs
162+
const cache = await caches.open(this.PAGE_CACHE_NAME);
163+
let cached = 0;
164+
let failed = 0;
165+
166+
for (const url of urlsToCache) {
167+
try {
168+
const response = await fetch(url, {
169+
method: 'GET',
170+
credentials: 'same-origin'
171+
});
172+
173+
if (response.ok) {
174+
// Add timestamp header
175+
const blob = await response.clone().blob();
176+
const headers = new Headers(response.headers);
177+
headers.set('sw-cached-time', Date.now().toString());
178+
179+
const timestampedResponse = new Response(blob, {
180+
status: response.status,
181+
statusText: response.statusText,
182+
headers: headers
183+
});
184+
185+
await cache.put(url, timestampedResponse);
186+
cached++;
187+
} else {
188+
failed++;
189+
}
190+
} catch (error) {
191+
console.warn(`[ManualCache] Failed to cache ${url}:`, error);
192+
failed++;
193+
}
194+
}
195+
196+
// Save cache timestamp
197+
localStorage.setItem(this.CACHE_KEY, Date.now().toString());
198+
199+
console.log(`[ManualCache] ✓ Cached ${cached} pages, ${failed} failed`);
200+
this.showToast('Success', `Cached ${cached} pages for offline use (valid for 48 hours)`, 'success');
201+
this.updateCacheStatus();
202+
203+
} catch (error) {
204+
console.error('[ManualCache] Caching failed:', error);
205+
this.showToast('Error', 'Failed to cache data. Please try again.', 'danger');
206+
} finally {
207+
this.updateCacheStatus();
208+
}
209+
}
210+
211+
/**
212+
* Clear all cached data
213+
*/
214+
async clearCache() {
215+
try {
216+
const cacheNames = await caches.keys();
217+
await Promise.all(
218+
cacheNames
219+
.filter(name => name.includes('vouchervault'))
220+
.map(name => caches.delete(name))
221+
);
222+
223+
localStorage.removeItem(this.CACHE_KEY);
224+
this.updateCacheStatus();
225+
226+
this.showToast('Success', 'Cache cleared', 'success');
227+
console.log('[ManualCache] Cache cleared');
228+
} catch (error) {
229+
console.error('[ManualCache] Failed to clear cache:', error);
230+
this.showToast('Error', 'Failed to clear cache', 'danger');
231+
}
232+
}
233+
234+
/**
235+
* Show toast notification
236+
*/
237+
showToast(title, message, type = 'info') {
238+
let toastContainer = document.getElementById('cache-toast-container');
239+
if (!toastContainer) {
240+
toastContainer = document.createElement('div');
241+
toastContainer.id = 'cache-toast-container';
242+
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
243+
toastContainer.style.zIndex = '11';
244+
document.body.appendChild(toastContainer);
245+
}
246+
247+
const toastId = 'toast-' + Date.now();
248+
const iconClass = type === 'success' ? 'bi-check-circle-fill' :
249+
type === 'danger' ? 'bi-exclamation-triangle-fill' :
250+
'bi-info-circle-fill';
251+
252+
const bgClass = type === 'success' ? 'bg-success' :
253+
type === 'danger' ? 'bg-danger' :
254+
'bg-info';
255+
256+
const toastHtml = `
257+
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
258+
<div class="toast-header ${bgClass} text-white">
259+
<i class="bi ${iconClass} me-2"></i>
260+
<strong class="me-auto">${title}</strong>
261+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
262+
</div>
263+
<div class="toast-body">
264+
${message}
265+
</div>
266+
</div>
267+
`;
268+
269+
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
270+
271+
const toastElement = document.getElementById(toastId);
272+
const toast = new bootstrap.Toast(toastElement, {
273+
autohide: true,
274+
delay: 5000
275+
});
276+
toast.show();
277+
278+
toastElement.addEventListener('hidden.bs.toast', () => {
279+
toastElement.remove();
280+
});
281+
}
282+
}
283+
284+
// Initialize when DOM is ready
285+
let manualCacheManager;
286+
if (document.readyState === 'loading') {
287+
document.addEventListener('DOMContentLoaded', () => {
288+
manualCacheManager = new ManualCacheManager();
289+
});
290+
} else {
291+
manualCacheManager = new ManualCacheManager();
292+
}

0 commit comments

Comments
 (0)