feat: add global styles and transitions for dark mode support
- 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.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "Budget Manager PWA",
|
||||
"short_name": "Budget",
|
||||
"description": "Budget Manager application for tracking expenses and income",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#3b82f6",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192px.jpg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512px.jpg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"orientation": "portrait",
|
||||
"categories": ["finance", "productivity", "utilities"],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshots/dashboard.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"platform": "wide",
|
||||
"label": "Dashboard of Budget Manager PWA"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/transactions.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"platform": "wide",
|
||||
"label": "Transactions page of Budget Manager PWA"
|
||||
}
|
||||
],
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false,
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Add Transaction",
|
||||
"short_name": "Add",
|
||||
"description": "Add a new transaction",
|
||||
"url": "/add",
|
||||
"icons": [{ "src": "/icons/add-icon-96x96.png", "sizes": "96x96" }]
|
||||
},
|
||||
{
|
||||
"name": "View Reports",
|
||||
"short_name": "Reports",
|
||||
"description": "View budget reports",
|
||||
"url": "/reports",
|
||||
"icons": [{ "src": "/icons/report-icon-96x96.png", "sizes": "96x96" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user