From 97908db3e07a8081861fc0e5218f31f736534b24 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Fri, 23 May 2025 17:36:26 +0200 Subject: [PATCH] refactor: migrate project to Vue 3 with TypeScript and Vite - Updated index.html to use Vite and Vue 3. - Changed package.json to reflect new dependencies and build scripts. - Removed old favicon.svg and added new vite.svg. - Deleted unused PWA assets configuration. - Created new App.vue component and HelloWorld.vue component. - Added Vue logo and Vite logo to App.vue. - Removed old JavaScript files and replaced with TypeScript files. - Updated styles in style.css for better layout. - Added TypeScript configuration files for better type checking. - Migrated Vite configuration from JavaScript to TypeScript. --- .prettierrc | 5 + backend/src/config/app.ts | 3 + backend/src/controllers/auth.ts | 210 +++++++++++++---- backend/src/controllers/categories.ts | 310 ++++++++++++------------- backend/src/controllers/protected.ts | 85 +++---- backend/src/middleware/auth.ts | 1 + backend/src/routes/index.ts | 8 + frontend/.gitignore | 1 - frontend/.vscode/extensions.json | 3 + frontend/README.md | 5 + frontend/bun.lock | 224 +++++++----------- frontend/index.html | 8 +- frontend/package.json | 22 +- frontend/public/favicon.svg | 130 ----------- frontend/public/vite.svg | 1 + frontend/pwa-assets.config.js | 12 - frontend/src/App.vue | 30 +++ frontend/src/assets/vue.svg | 1 + frontend/src/components/HelloWorld.vue | 41 ++++ frontend/src/counter.js | 16 -- frontend/src/javascript.svg | 1 - frontend/src/main.js | 49 ---- frontend/src/main.ts | 5 + frontend/src/notification.js | 25 -- frontend/src/pwa.js | 103 -------- frontend/src/style.css | 78 +------ frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.app.json | 15 ++ frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 25 ++ frontend/vite.config.js | 35 --- frontend/vite.config.ts | 11 + 32 files changed, 630 insertions(+), 841 deletions(-) create mode 100644 .prettierrc create mode 100644 backend/src/config/app.ts create mode 100644 frontend/.vscode/extensions.json create mode 100644 frontend/README.md delete mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/vite.svg delete mode 100644 frontend/pwa-assets.config.js create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/components/HelloWorld.vue delete mode 100644 frontend/src/counter.js delete mode 100644 frontend/src/javascript.svg delete mode 100644 frontend/src/main.js create mode 100644 frontend/src/main.ts delete mode 100644 frontend/src/notification.js delete mode 100644 frontend/src/pwa.js create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json delete mode 100644 frontend/vite.config.js create mode 100644 frontend/vite.config.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9a64ca0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 80 +} diff --git a/backend/src/config/app.ts b/backend/src/config/app.ts new file mode 100644 index 0000000..7252683 --- /dev/null +++ b/backend/src/config/app.ts @@ -0,0 +1,3 @@ +export default { + adminKey: process.env.ADMIN_KEY || 'admin-secret-key-change-this-in-production' +} \ No newline at end of file diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index e8a0677..43bfe0f 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,15 +1,22 @@ -import type { Context } from 'koa'; import bcrypt from 'bcrypt'; -import jwt, { type Secret } from 'jsonwebtoken'; +import jwt, { type SignOptions } from 'jsonwebtoken'; +import type { Context } from 'koa'; +import appConfig from '../config/app'; +import dbConfig from '../config/database'; import jwtConfig from '../config/jwt'; +import type { User } from '../models'; +import DatabaseService from '../services/database'; -// Mock user database (in a real app, use a proper database) -const users = new Map(); +const db = DatabaseService.getInstance(); export default { // User registration async register(ctx: Context) { - const { username, password } = ctx.request.body as { username: string, password: string }; + const { username, password, email } = ctx.request.body as { + username: string, + password: string, + email?: string + }; // Validate input if (!username || !password) { @@ -18,28 +25,117 @@ export default { return; } - // Check if user already exists - if (users.has(username)) { - ctx.status = 409; - ctx.body = { success: false, message: 'Username already exists' }; + try { + const usersCollection = db.getCollection(dbConfig.collections.users); + + // Check if user already exists + const existingUser = await usersCollection.findOne({ username }); + if (existingUser) { + ctx.status = 409; + ctx.body = { success: false, message: 'Username already exists' }; + return; + } + + // Hash password before saving + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + // Create new user + const now = new Date(); + const newUser: User = { + username, + password: hashedPassword, + email, + createdAt: now, + updatedAt: now + }; + + // Save user to database + const result = await usersCollection.insertOne(newUser); + + ctx.status = 201; + ctx.body = { + success: true, + message: 'User registered successfully', + userId: result.insertedId + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error registering user', + error: (error as Error).message + }; + } + }, + + // Admin registration + async registerAdmin(ctx: Context) { + const { username, password, email, adminKey } = ctx.request.body as { + username: string, + password: string, + email?: string, + adminKey: string + }; + + // Validate input + if (!username || !password) { + ctx.status = 400; + ctx.body = { success: false, message: 'Username and password are required' }; return; } - // Hash password before saving - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); + // Validate admin key + const validAdminKey = appConfig.adminKey; + if (adminKey !== validAdminKey) { + ctx.status = 403; + ctx.body = { success: false, message: 'Invalid admin key' }; + return; + } - // Save user to database - users.set(username, { - username, - password: hashedPassword - }); + try { + const usersCollection = db.getCollection(dbConfig.collections.users); - ctx.status = 201; - ctx.body = { - success: true, - message: 'User registered successfully' - }; + // Check if user already exists + const existingUser = await usersCollection.findOne({ username }); + if (existingUser) { + ctx.status = 409; + ctx.body = { success: false, message: 'Username already exists' }; + return; + } + + // Hash password before saving + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + // Create new admin user + const now = new Date(); + const newUser: User = { + username, + password: hashedPassword, + email, + role: 'admin', // Add admin role + createdAt: now, + updatedAt: now + }; + + // Save user to database + const result = await usersCollection.insertOne(newUser); + + ctx.status = 201; + ctx.body = { + success: true, + message: 'Admin registered successfully', + userId: result.insertedId + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error registering admin', + error: (error as Error).message + }; + } }, // User login @@ -53,33 +149,49 @@ export default { return; } - // Check if user exists - const user = users.get(username); - if (!user) { - ctx.status = 401; - ctx.body = { success: false, message: 'Invalid credentials' }; - return; + try { + const usersCollection = db.getCollection(dbConfig.collections.users); + + // Check if user exists + const user = await usersCollection.findOne({ username }); + if (!user) { + ctx.status = 401; + ctx.body = { success: false, message: 'Invalid credentials' }; + return; + } + + // Verify password + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + ctx.status = 401; + ctx.body = { success: false, message: 'Invalid credentials' }; + return; + } + + // Generate JWT token + const token = jwt.sign( + { + id: user._id?.toString(), + username: user.username + }, + jwtConfig.secret, + { expiresIn: jwtConfig.expiresIn } as SignOptions + ); + + ctx.body = { + success: true, + message: 'Login successful', + userId: user._id, + username: user.username, + token + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error during login', + error: (error as Error).message + }; } - - // Verify password - const isPasswordValid = await bcrypt.compare(password, user.password); - if (!isPasswordValid) { - ctx.status = 401; - ctx.body = { success: false, message: 'Invalid credentials' }; - return; - } - - // Generate JWT token - const token = jwt.sign( - { username: user.username }, - jwtConfig.secret as Secret, - { expiresIn: jwtConfig.expiresIn } - ); - - ctx.body = { - success: true, - message: 'Login successful', - token - }; } }; diff --git a/backend/src/controllers/categories.ts b/backend/src/controllers/categories.ts index 6878416..4395e13 100644 --- a/backend/src/controllers/categories.ts +++ b/backend/src/controllers/categories.ts @@ -1,166 +1,166 @@ import type { Context } from 'koa'; -import DatabaseService from '../services/database'; +import { ObjectId } from 'mongodb'; import dbConfig from '../config/database'; import type { Category } from '../models'; -import { ObjectId } from 'mongodb'; +import DatabaseService from '../services/database'; const db = DatabaseService.getInstance(); export default { - // Get categories for a user - async getUserCategories(ctx: Context) { - const user = (ctx.state as any).user; - const userId = user.id; - - try { - const categoriesCollection = db.getCollection(dbConfig.collections.categories); - - const categories = await categoriesCollection.find({ - userId: new ObjectId(userId) - }).toArray(); - - ctx.body = { - success: true, - data: categories - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - message: 'Error retrieving categories', - error: (error as Error).message - }; - } - }, - - // Add new category - async addCategory(ctx: Context) { - const user = (ctx.state as any).user; - const userId = user.id; - const { name, icon, color } = ctx.request.body as { - name: string; - icon?: string; - color?: string; - }; - - if (!name) { - ctx.status = 400; - ctx.body = { - success: false, - message: 'Category name is required' - }; - return; - } - - try { - const categoriesCollection = db.getCollection(dbConfig.collections.categories); - - // Check if category already exists - const existingCategory = await categoriesCollection.findOne({ - userId: new ObjectId(userId), - name - }); - - if (existingCategory) { - ctx.status = 409; - ctx.body = { - success: false, - message: 'Category already exists' + // Get categories for a user + async getUserCategories(ctx: Context) { + const user = (ctx.state as any).user; + const userId = user.id; + + try { + const categoriesCollection = db.getCollection(dbConfig.collections.categories); + + const categories = await categoriesCollection.find({ + userId: new ObjectId(userId) + }).toArray(); + + ctx.body = { + success: true, + data: categories + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error retrieving categories', + error: (error as Error).message + }; + } + }, + + // Add new category + async addCategory(ctx: Context) { + const user = (ctx.state as any).user; + const userId = user.id; + const { name, icon, color } = ctx.request.body as { + name: string; + icon?: string; + color?: string; }; - return; - } - - const now = new Date(); - const newCategory: Category = { - userId: new ObjectId(userId), - name, - icon, - color, - createdAt: now, - updatedAt: now - }; - - const result = await categoriesCollection.insertOne(newCategory); - - ctx.status = 201; - ctx.body = { - success: true, - message: 'Category added successfully', - categoryId: result.insertedId - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - message: 'Error adding category', - error: (error as Error).message - }; - } - }, - - // Update category - async updateCategory(ctx: Context) { - const user = (ctx.state as any).user; - const userId = user.id; - const categoryId = ctx.params.id; - - const { name, icon, color } = ctx.request.body as { - name?: string; - icon?: string; - color?: string; - }; - - if (!categoryId || !ObjectId.isValid(categoryId)) { - ctx.status = 400; - ctx.body = { - success: false, - message: 'Valid category ID is required' - }; - return; - } - - try { - const categoriesCollection = db.getCollection(dbConfig.collections.categories); - - // Make sure category belongs to user - const category = await categoriesCollection.findOne({ - _id: new ObjectId(categoryId), - userId: new ObjectId(userId) - }); - - if (!category) { - ctx.status = 404; - ctx.body = { - success: false, - message: 'Category not found' + + if (!name) { + ctx.status = 400; + ctx.body = { + success: false, + message: 'Category name is required' + }; + return; + } + + try { + const categoriesCollection = db.getCollection(dbConfig.collections.categories); + + // Check if category already exists + const existingCategory = await categoriesCollection.findOne({ + userId: new ObjectId(userId), + name + }); + + if (existingCategory) { + ctx.status = 409; + ctx.body = { + success: false, + message: 'Category already exists' + }; + return; + } + + const now = new Date(); + const newCategory: Category = { + userId: new ObjectId(userId), + name, + icon, + color, + createdAt: now, + updatedAt: now + }; + + const result = await categoriesCollection.insertOne(newCategory); + + ctx.status = 201; + ctx.body = { + success: true, + message: 'Category added successfully', + categoryId: result.insertedId + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error adding category', + error: (error as Error).message + }; + } + }, + + // Update category + async updateCategory(ctx: Context) { + const user = (ctx.state as any).user; + const userId = user.id; + const categoryId = ctx.params.id; + + const { name, icon, color } = ctx.request.body as { + name?: string; + icon?: string; + color?: string; }; - return; - } - - const updateData: Partial = { - updatedAt: new Date() - }; - - if (name) updateData.name = name; - if (icon) updateData.icon = icon; - if (color) updateData.color = color; - - await categoriesCollection.updateOne( - { _id: new ObjectId(categoryId) }, - { $set: updateData } - ); - - ctx.body = { - success: true, - message: 'Category updated successfully' - }; - } catch (error) { - ctx.status = 500; - ctx.body = { - success: false, - message: 'Error updating category', - error: (error as Error).message - }; + + if (!categoryId || !ObjectId.isValid(categoryId)) { + ctx.status = 400; + ctx.body = { + success: false, + message: 'Valid category ID is required' + }; + return; + } + + try { + const categoriesCollection = db.getCollection(dbConfig.collections.categories); + + // Make sure category belongs to user + const category = await categoriesCollection.findOne({ + _id: new ObjectId(categoryId), + userId: new ObjectId(userId) + }); + + if (!category) { + ctx.status = 404; + ctx.body = { + success: false, + message: 'Category not found' + }; + return; + } + + const updateData: Partial = { + updatedAt: new Date() + }; + + if (name) updateData.name = name; + if (icon) updateData.icon = icon; + if (color) updateData.color = color; + + await categoriesCollection.updateOne( + { _id: new ObjectId(categoryId) }, + { $set: updateData } + ); + + ctx.body = { + success: true, + message: 'Category updated successfully' + }; + } catch (error) { + ctx.status = 500; + ctx.body = { + success: false, + message: 'Error updating category', + error: (error as Error).message + }; + } } - } }; diff --git a/backend/src/controllers/protected.ts b/backend/src/controllers/protected.ts index 2837b4e..ff4b562 100644 --- a/backend/src/controllers/protected.ts +++ b/backend/src/controllers/protected.ts @@ -1,8 +1,8 @@ -import type { Context } from 'koa'; -import DatabaseService from '../services/database'; -import dbConfig from '../config/database'; -import type { Transaction } from '../models'; -import { ObjectId } from 'mongodb'; +import type { Context } from "koa"; +import { ObjectId } from "mongodb"; +import dbConfig from "../config/database"; +import type { Transaction } from "../models"; +import DatabaseService from "../services/database"; const db = DatabaseService.getInstance(); @@ -11,100 +11,105 @@ export default { async getProtectedData(ctx: Context) { // The user property is added by koa-jwt middleware const user = (ctx.state as any).user; - + ctx.body = { success: true, - message: 'You have access to protected data', + message: "You have access to protected data", data: { - secret: 'This is protected data', + secret: "This is protected data", timestamp: new Date().toISOString(), user: user.username, - userId: user.id - } + userId: user.id, + }, }; }, - + // Get user's transactions async getUserTransactions(ctx: Context) { const user = (ctx.state as any).user; const userId = user.id; - + try { - const transactionsCollection = db.getCollection(dbConfig.collections.transactions); - + const transactionsCollection = db.getCollection( + dbConfig.collections.transactions + ); + // Get the latest 10 transactions for the user - const transactions = await transactionsCollection.find({ - userId: new ObjectId(userId) - }) - .sort({ date: -1 }) - .limit(10) - .toArray(); - + const transactions = await transactionsCollection + .find({ + userId: new ObjectId(String(userId)), + }) + .sort({ date: -1 }) + .limit(10) + .toArray(); + ctx.body = { success: true, - data: transactions + data: transactions, }; } catch (error) { ctx.status = 500; ctx.body = { success: false, - message: 'Error retrieving transactions', - error: (error as Error).message + message: "Error retrieving transactions", + error: (error as Error).message, }; } }, - + // Add new transaction async addTransaction(ctx: Context) { const user = (ctx.state as any).user; const userId = user.id; const { amount, type, categoryId, description } = ctx.request.body as { amount: number; - type: 'income' | 'expense'; + type: "income" | "expense"; categoryId: string; description: string; }; - + // Validate input if (!amount || !type || !categoryId) { ctx.status = 400; ctx.body = { success: false, - message: 'Amount, type and category are required' + message: "Amount, type and category are required", }; return; } - + try { - const transactionsCollection = db.getCollection(dbConfig.collections.transactions); - + const transactionsCollection = db.getCollection( + dbConfig.collections.transactions + ); + const now = new Date(); const newTransaction: Transaction = { userId: new ObjectId(userId), amount, type, categoryId: new ObjectId(categoryId), - description: description || '', + description: description || "", date: now, createdAt: now, - updatedAt: now + updatedAt: now, }; - + const result = await transactionsCollection.insertOne(newTransaction); - + ctx.status = 201; ctx.body = { success: true, - message: 'Transaction added successfully', - transactionId: result.insertedId + message: "Transaction added successfully", + transactionId: result.insertedId, }; } catch (error) { ctx.status = 500; ctx.body = { success: false, - message: 'Error adding transaction', - error: (error as Error).message + message: "Error adding transaction", + error: (error as Error).message, }; } - } + }, }; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 7e563fb..a2d78a2 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -17,6 +17,7 @@ export const authenticate: Middleware = jwt({ // Error handling middleware for JWT authentication export const handleJwtError: Middleware = async (ctx, next) => { try { + // Next middleware have to be jwt call. await next(); } catch (err: any) { if (err.status === 401) { diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index b11ca91..837fec0 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -5,11 +5,19 @@ import categoriesController from '../controllers/categories'; const router = new Router({ prefix: '/api' }); +/* + Middleware requires authentication for all routes. + Exceptions are defined in the auth middleware `backend/src/middleware/auth.ts` + There are defined exceptions for auth routes +*/ + +// --- Public routes --- // Auth routes router.post('/auth/register', authController.register); router.post('/auth/register-admin', authController.registerAdmin); router.post('/auth/login', authController.login); +// --- All below routes require authentication ---- // Protected routes router.get('/protected', protectedController.getProtectedData); diff --git a/frontend/.gitignore b/frontend/.gitignore index 6d6ae5a..a547bf3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,7 +10,6 @@ lerna-debug.log* node_modules dist dist-ssr -dev-dist *.local # Editor directories and files diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + diff --git a/frontend/package.json b/frontend/package.json index 0ed7fc3..ac331d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,21 +1,23 @@ { - "name": "budget-manager", + "name": "frontend", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "vue-tsc -b && vite build", "preview": "vite preview" }, - "devDependencies": { - "@vite-pwa/assets-generator": "^0.2.6", - "vite": "^6.0.11", - "vite-plugin-pwa": "^0.21.1", - "workbox-window": "^7.3.0" + "dependencies": { + "vue": "^3.5.13" }, - "resolutions": { - "sharp": "0.32.6", - "sharp-ico": "0.1.5" + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.3", + "@vue/tsconfig": "^0.7.0", + "prettier": "^3.5.3", + "typescript": "~5.8.3", + "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.0", + "vue-tsc": "^2.2.8" } } diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 733f4fb..0000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/pwa-assets.config.js b/frontend/pwa-assets.config.js deleted file mode 100644 index 452b31f..0000000 --- a/frontend/pwa-assets.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { - defineConfig, - minimal2023Preset as preset, -} from '@vite-pwa/assets-generator/config' - -export default defineConfig({ - headLinkOptions: { - preset: '2023', - }, - preset, - images: ['public/favicon.svg'], -}) diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..58b0f21 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/counter.js b/frontend/src/counter.js deleted file mode 100644 index fbeff42..0000000 --- a/frontend/src/counter.js +++ /dev/null @@ -1,16 +0,0 @@ -import { showNotification } from "./notification.js"; - -export function setupCounter(element) { - let counter = 0; - const setCounter = (count) => { - counter = count; - element.innerHTML = `count is ${counter}`; - }; - element.addEventListener("click", () => { - setCounter(counter + 1); - if (counter == 5) { - showNotification("Counter reached 5!"); - } - }); - setCounter(0); -} diff --git a/frontend/src/javascript.svg b/frontend/src/javascript.svg deleted file mode 100644 index f9abb2b..0000000 --- a/frontend/src/javascript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js deleted file mode 100644 index c2a3003..0000000 --- a/frontend/src/main.js +++ /dev/null @@ -1,49 +0,0 @@ -import "./style.css"; -import javascriptLogo from "./javascript.svg"; -import appLogo from "/favicon.svg"; -import { setupCounter } from "./counter.js"; -import { showNotification } from "./notification.js"; -import { initPWA } from "./pwa.js"; - -const app = document.querySelector("#app"); -app.innerHTML = ` -
- - - - - - -

budget-manager

-
- - -
-

- Click on the Vite logo to learn more -

-
- -`; - -setupCounter(document.querySelector("#counter")); -document.getElementById("notificationBtn").addEventListener("click", () => { - showNotification(); -}); -initPWA(app); diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/frontend/src/notification.js b/frontend/src/notification.js deleted file mode 100644 index 59bf059..0000000 --- a/frontend/src/notification.js +++ /dev/null @@ -1,25 +0,0 @@ -export function showNotification(message) { - // This function will be called when button clicked and should show the notification using service worker - // Check if the browser supports notifications - if (!('Notification' in window)) { - console.log('Browser does not support notifications.'); - return; - } - // Request permission to show notifications - Notification.requestPermission().then((permission) => { - if (permission === 'granted') { - // Show the notification - const notification = new Notification(message ?? 'Hello!', { - body: 'This is a notification from your app.', - icon: '/pwa-192x192.png', - }); - // Add click event to the notification - notification.onclick = () => { - window.focus(); - notification.close(); - }; - } else { - console.log('Notification permission denied.'); - } - }); -} \ No newline at end of file diff --git a/frontend/src/pwa.js b/frontend/src/pwa.js deleted file mode 100644 index d220a64..0000000 --- a/frontend/src/pwa.js +++ /dev/null @@ -1,103 +0,0 @@ -import { registerSW } from 'virtual:pwa-register' - -/**@param app {HTMLDivElement}*/ -export function initPWA(app) { - /**@type {HTMLDivElement}*/ - const pwaToast = app.querySelector('#pwa-toast') - /**@type {HTMLDivElement}*/ - const pwaToastMessage = pwaToast.querySelector('.message #toast-message') - /**@type {HTMLButtonElement}*/ - const pwaCloseBtn = pwaToast.querySelector('#pwa-close') - /**@type {HTMLButtonElement}*/ - const pwaRefreshBtn = pwaToast.querySelector('#pwa-refresh') - - /**@type {(reloadPage?: boolean) => Promise}*/ - let refreshSW - - const refreshCallback = () => refreshSW?.(true) - - /**@param raf {boolean}*/ - function hidePwaToast (raf) { - if (raf) { - requestAnimationFrame(() => hidePwaToast(false)) - return - } - if (pwaToast.classList.contains('refresh')) - pwaRefreshBtn.removeEventListener('click', refreshCallback) - - pwaToast.classList.remove('show', 'refresh') - } - /**@param offline {boolean}*/ - function showPwaToast(offline) { - if (!offline) - pwaRefreshBtn.addEventListener('click', refreshCallback) - requestAnimationFrame(() => { - hidePwaToast(false) - if (!offline) - pwaToast.classList.add('refresh') - pwaToast.classList.add('show') - }) - } - - let swActivated = false - // check for updates every hour - const period = 60 * 60 * 1000 - - window.addEventListener('load', () => { - pwaCloseBtn.addEventListener('click', () => hidePwaToast(true)) - refreshSW = registerSW({ - immediate: true, - onOfflineReady() { - pwaToastMessage.innerHTML = 'App ready to work offline' - showPwaToast(true) - }, - onNeedRefresh() { - pwaToastMessage.innerHTML = 'New content available, click on reload button to update' - showPwaToast(false) - }, - onRegisteredSW(swUrl, r) { - if (period <= 0) return - if (r?.active?.state === 'activated') { - swActivated = true - registerPeriodicSync(period, swUrl, r) - } - else if (r?.installing) { - r.installing.addEventListener('statechange', (e) => { - /**@type {ServiceWorker}*/ - const sw = e.target - swActivated = sw.state === 'activated' - if (swActivated) - registerPeriodicSync(period, swUrl, r) - }) - } - }, - }) - }) -} - -/** - * This function will register a periodic sync check every hour, you can modify the interval as needed. - * - * @param period {number} - * @param swUrl {string} - * @param r {ServiceWorkerRegistration} - */ -function registerPeriodicSync(period, swUrl, r) { - if (period <= 0) return - - setInterval(async () => { - if ('onLine' in navigator && !navigator.onLine) - return - - const resp = await fetch(swUrl, { - cache: 'no-store', - headers: { - 'cache': 'no-store', - 'cache-control': 'no-cache', - }, - }) - - if (resp?.status === 200) - await r.update() - }, period) -} diff --git a/frontend/src/style.css b/frontend/src/style.css index 17364ff..f691315 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,5 +1,5 @@ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; @@ -35,34 +35,6 @@ h1 { line-height: 1.1; } -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #f7df1eaa); -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - button { border-radius: 8px; border: 1px solid transparent; @@ -82,6 +54,17 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + @media (prefers-color-scheme: light) { :root { color: #213547; @@ -94,40 +77,3 @@ button:focus-visible { background-color: #f9f9f9; } } - -#pwa-toast { - visibility: hidden; - position: fixed; - right: 0; - bottom: 0; - margin: 16px; - padding: 12px; - border: 1px solid #8885; - border-radius: 4px; - z-index: 1; - text-align: left; - box-shadow: 3px 4px 5px 0 #8885; - display: grid; -} -#pwa-toast .message { - margin-bottom: 8px; -} -#pwa-toast .buttons { - display: flex; -} -#pwa-toast button { - border: 1px solid #8885; - outline: none; - margin-right: 5px; - border-radius: 2px; - padding: 3px 10px; -} -#pwa-toast.show { - visibility: visible; -} -button#pwa-refresh { - display: none; -} -#pwa-toast.show.refresh button#pwa-refresh { - display: block; -} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..3dbbc45 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..9728af2 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js deleted file mode 100644 index 290d60f..0000000 --- a/frontend/vite.config.js +++ /dev/null @@ -1,35 +0,0 @@ -import { VitePWA } from 'vite-plugin-pwa'; -import { defineConfig } from 'vite' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [VitePWA({ - registerType: 'autoUpdate', - injectRegister: 'script-defer', - - pwaAssets: { - disabled: false, - config: true, - }, - - manifest: { - name: 'budget-manager', - short_name: 'BM', - description: 'Application for finance management', - theme_color: '#ffffff', - }, - - workbox: { - globPatterns: ['**/*.{js,css,html,svg,png,ico}'], - cleanupOutdatedCaches: true, - clientsClaim: true, - }, - - devOptions: { - enabled: true, - // navigateFallback: 'index.html', - // suppressWarnings: true, - type: 'classic', - }, - })], -}) \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..de43c9f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; +import { VitePWA } from "vite-plugin-pwa"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + VitePWA({ registerType: "autoUpdate", devOptions: { enabled: true } }), + ], +});