- Created style.css for base styles and color variables for light and dark themes. - Added tailwind.css for Tailwind CSS integration and custom theme variables. - Introduced transitions.css to manage global transitions and reduce motion preferences. - Updated tsconfig.app.json to include path aliases for easier imports. - Modified tsconfig.json to set baseUrl and paths for module resolution. - Added typed-router.d.ts for auto-generated route types. - Enhanced vite.config.ts with Vue Router plugin, Tailwind CSS integration, and server proxy settings.
216 lines
5.5 KiB
JavaScript
216 lines
5.5 KiB
JavaScript
// Service Worker for Budget Manager PWA
|
|
|
|
const CACHE_NAME = "budget-manager-v1";
|
|
|
|
// Assets to precache
|
|
const precacheResources = [
|
|
"/",
|
|
"/index.html",
|
|
"/src/main.ts",
|
|
"/src/App.vue",
|
|
"/styles/tailwind.css",
|
|
"/styles/transitions.css",
|
|
"/styles/style.css",
|
|
"/icons/icon-192px.jpg",
|
|
"/icons/icon-512px.jpg",
|
|
];
|
|
|
|
// Install event - precache assets
|
|
self.addEventListener("install", (event) => {
|
|
console.log("Service worker install event");
|
|
event.waitUntil(
|
|
caches
|
|
.open(CACHE_NAME)
|
|
.then((cache) => {
|
|
return cache.addAll(precacheResources);
|
|
})
|
|
.then(() => {
|
|
// Skip waiting to activate immediately
|
|
return self.skipWaiting();
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener("activate", (event) => {
|
|
console.log("Service worker 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);
|
|
}
|
|
}),
|
|
);
|
|
})
|
|
.then(() => {
|
|
// Take control of all clients
|
|
return self.clients.claim();
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Fetch event - serve from cache first, then network
|
|
self.addEventListener("fetch", (event) => {
|
|
// Skip cross-origin requests
|
|
if (
|
|
!event.request.url.startsWith(self.location.origin) ||
|
|
event.request.url.includes("/api/")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
event.respondWith(
|
|
caches.match(event.request).then((cachedResponse) => {
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
return fetch(event.request)
|
|
.then((response) => {
|
|
// Cache successful responses
|
|
if (
|
|
response &&
|
|
response.status === 200 &&
|
|
response.type === "basic"
|
|
) {
|
|
const responseToCache = response.clone();
|
|
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(event.request, responseToCache);
|
|
});
|
|
}
|
|
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
// If fetch fails (offline), return a fallback
|
|
if (event.request.mode === "navigate") {
|
|
return caches.match("/");
|
|
}
|
|
|
|
return null;
|
|
});
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Background sync for offline transactions
|
|
self.addEventListener("sync", (event) => {
|
|
if (event.tag === "sync-transactions") {
|
|
event.waitUntil(syncTransactions());
|
|
} else if (event.tag === "sync-categories") {
|
|
event.waitUntil(syncCategories());
|
|
}
|
|
});
|
|
|
|
// Push notification handler
|
|
self.addEventListener("push", (event) => {
|
|
const data = event.data.json();
|
|
|
|
const options = {
|
|
body: data.body,
|
|
icon: "/icons/icon-192px.jpg",
|
|
badge: "/icons/badge-96x96.png",
|
|
data: data.data,
|
|
actions: data.actions || [],
|
|
};
|
|
|
|
event.waitUntil(self.registration.showNotification(data.title, options));
|
|
});
|
|
|
|
// Notification click handler
|
|
self.addEventListener("notificationclick", (event) => {
|
|
event.notification.close();
|
|
|
|
event.waitUntil(clients.openWindow("/"));
|
|
});
|
|
|
|
// Sync transactions with the server
|
|
async function syncTransactions() {
|
|
try {
|
|
const pendingTransactions = await getPendingItems("pendingTransactions");
|
|
const token = await getAuthToken();
|
|
|
|
if (!token || pendingTransactions.length === 0) return;
|
|
|
|
for (const transaction of pendingTransactions) {
|
|
const response = await fetch("http://localhost:3000/api/transactions", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(transaction),
|
|
});
|
|
|
|
if (response.ok) {
|
|
await removePendingItem("pendingTransactions", transaction.id);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error syncing transactions:", error);
|
|
}
|
|
}
|
|
|
|
// Sync categories with the server
|
|
async function syncCategories() {
|
|
try {
|
|
const pendingCategories = await getPendingItems("pendingCategories");
|
|
const token = await getAuthToken();
|
|
|
|
if (!token || pendingCategories.length === 0) return;
|
|
|
|
for (const category of pendingCategories) {
|
|
const response = await fetch("http://localhost:3000/api/categories", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(category),
|
|
});
|
|
|
|
if (response.ok) {
|
|
await removePendingItem("pendingCategories", category.id);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error syncing categories:", error);
|
|
}
|
|
}
|
|
|
|
// Helper function to get pending items from IndexedDB
|
|
async function getPendingItems(storeName) {
|
|
return []; // Placeholder - in a real app, this would access IndexedDB
|
|
}
|
|
|
|
// Helper function to remove a pending item from IndexedDB
|
|
async function removePendingItem(storeName, id) {
|
|
// Placeholder - in a real app, this would access IndexedDB
|
|
}
|
|
|
|
// Helper function to get auth token from client
|
|
async function getAuthToken() {
|
|
const clients = await self.clients.matchAll();
|
|
|
|
if (clients.length === 0) {
|
|
return localStorage.getItem("token");
|
|
}
|
|
|
|
// Ask client for token
|
|
const client = clients[0];
|
|
return new Promise((resolve) => {
|
|
const channel = new MessageChannel();
|
|
channel.port1.onmessage = (event) => {
|
|
resolve(event.data.token);
|
|
};
|
|
|
|
client.postMessage({ type: "GET_AUTH_TOKEN" }, [channel.port2]);
|
|
});
|
|
}
|